From d324f6eacf482bdb3f85c15811753207cfe72d0e Mon Sep 17 00:00:00 2001 From: Malena Horstmann Date: Tue, 30 Sep 2025 10:34:33 +0200 Subject: [PATCH 01/67] testclass file push to check access rights --- .../model/transformer/ComputeFormulaTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java diff --git a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java new file mode 100644 index 00000000..056f87fe --- /dev/null +++ b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java @@ -0,0 +1,14 @@ +package de.featjar.feature.model.transformer; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class ComputeFormulaTest { + + @Test + void test() { + fail("Not yet implemented"); + } + +} From e556c10f08dd2ac9d440b93a836ecfcb7f7273b4 Mon Sep 17 00:00:00 2001 From: Malena Horstmann Date: Tue, 30 Sep 2025 11:02:03 +0200 Subject: [PATCH 02/67] main method work around to debug cause tests in eclipse do not run --- .../featjar/feature/model/transformer/ComputeFormulaTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java index 056f87fe..86d0480d 100644 --- a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java +++ b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java @@ -5,7 +5,9 @@ import org.junit.jupiter.api.Test; class ComputeFormulaTest { - +public static void main(String[] args) { + new ComputeFormulaTest().test(); +} @Test void test() { fail("Not yet implemented"); From 4ca36bd08fadb1e02ff60a206c6e37840d0b2af3 Mon Sep 17 00:00:00 2001 From: Malena Horstmann Date: Tue, 30 Sep 2025 15:19:23 +0200 Subject: [PATCH 03/67] ComputeFormula first Unit Tests for only root and one feature --- .../model/transformer/ComputeFormulaTest.java | 79 +++++++++++++++++-- 1 file changed, 73 insertions(+), 6 deletions(-) diff --git a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java index 86d0480d..7bb52e34 100644 --- a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java +++ b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java @@ -1,16 +1,83 @@ package de.featjar.feature.model.transformer; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import de.featjar.base.computation.Computations; +import de.featjar.base.computation.ComputeConstant; +import de.featjar.base.computation.Progress; +import de.featjar.base.data.Result; +import de.featjar.base.data.identifier.Identifiers; +import de.featjar.feature.model.FeatureModel; +import de.featjar.feature.model.FeatureTest; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.IFeatureTree; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.connective.And; +import de.featjar.formula.structure.connective.BiImplies; +import de.featjar.formula.structure.connective.Implies; +import de.featjar.formula.structure.connective.Not; +import de.featjar.formula.structure.connective.Or; +import de.featjar.formula.structure.connective.Reference; +import de.featjar.formula.structure.predicate.Literal; + class ComputeFormulaTest { -public static void main(String[] args) { - new ComputeFormulaTest().test(); -} + private IFeatureModel featureModel; + private IFormula expected; + + @BeforeEach + public void createFeatureModel() { + featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + + } + @Test - void test() { - fail("Not yet implemented"); + void onlyRoot() { + + // root and nothing else + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + + // root must be selected + expected = new Reference(new And(new Literal("root"))); + + executeTest(); } + + @Test + void oneFeature () { + + // root + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAndGroup(); + + // TODO: check if setting root feature is missing here or if compute misses adding root feature literal + // create and add our only child + IFeature childFeature = featureModel.mutate().addFeature("Test1"); + rootTree.mutate().addFeatureBelow(childFeature); + + expected = new Reference( new And ( + new Literal ("root"), + new Implies( new Literal("Test1") , new Literal ("root") ) + )); + + executeTest(); + } + + private void executeTest() { + + ComputeConstant computeConstant = new ComputeConstant(featureModel); + ComputeFormula computeFormula = new ComputeFormula(computeConstant); + + IFormula resultFormula = computeFormula.computeResult().get(); + + // assert + assertEquals(expected, resultFormula); + } } From c4cb5ab47a6ec960a170546983fe2ebcbbbc0c34 Mon Sep 17 00:00:00 2001 From: Malena Horstmann Date: Tue, 30 Sep 2025 15:45:56 +0200 Subject: [PATCH 04/67] formatting, cleanup and a TODO --- .../model/transformer/ComputeFormulaTest.java | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java index 7bb52e34..5dd1b8d8 100644 --- a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java +++ b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java @@ -2,27 +2,18 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.List; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import de.featjar.base.computation.Computations; import de.featjar.base.computation.ComputeConstant; -import de.featjar.base.computation.Progress; -import de.featjar.base.data.Result; import de.featjar.base.data.identifier.Identifiers; import de.featjar.feature.model.FeatureModel; -import de.featjar.feature.model.FeatureTest; import de.featjar.feature.model.IFeature; import de.featjar.feature.model.IFeatureModel; import de.featjar.feature.model.IFeatureTree; import de.featjar.formula.structure.IFormula; import de.featjar.formula.structure.connective.And; -import de.featjar.formula.structure.connective.BiImplies; import de.featjar.formula.structure.connective.Implies; -import de.featjar.formula.structure.connective.Not; -import de.featjar.formula.structure.connective.Or; import de.featjar.formula.structure.connective.Reference; import de.featjar.formula.structure.predicate.Literal; @@ -43,7 +34,9 @@ void onlyRoot() { featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); // root must be selected - expected = new Reference(new And(new Literal("root"))); + expected = new Reference(new And( + new Literal("root") + )); executeTest(); } @@ -62,6 +55,7 @@ void oneFeature () { IFeature childFeature = featureModel.mutate().addFeature("Test1"); rootTree.mutate().addFeatureBelow(childFeature); + // TODO: check order if bug is fixed expected = new Reference( new And ( new Literal ("root"), new Implies( new Literal("Test1") , new Literal ("root") ) From b4893a09cc2f9d800c58f47c5c38084ca77a3944 Mon Sep 17 00:00:00 2001 From: Malena Horstmann Date: Tue, 30 Sep 2025 16:19:23 +0200 Subject: [PATCH 05/67] gradle spotless and license --- .../feature/configuration/Configuration.java | 934 +++++++++--------- .../configuration/io/FeatureIDEFormat.java | 358 +++---- .../feature/model/AFeatureModelElement.java | 204 ++-- .../de/featjar/feature/model/Attributes.java | 196 ++-- .../de/featjar/feature/model/Constraint.java | 168 ++-- .../de/featjar/feature/model/Feature.java | 164 +-- .../featjar/feature/model/FeatureModel.java | 546 +++++----- .../de/featjar/feature/model/FeatureTree.java | 576 +++++------ .../de/featjar/feature/model/IConstraint.java | 172 ++-- .../de/featjar/feature/model/IFeature.java | 264 ++--- .../featjar/feature/model/IFeatureModel.java | 180 ++-- .../feature/model/IFeatureModelElement.java | 56 +- .../featjar/feature/model/IFeatureTree.java | 510 +++++----- .../featjar/feature/model/io/AttributeIO.java | 236 ++--- .../feature/model/io/FeatureModelFormats.java | 74 +- .../io/xml/GraphVizFeatureModelFormat.java | 290 +++--- .../model/io/xml/XMLFeatureModelFormat.java | 150 +-- .../model/io/xml/XMLFeatureModelParser.java | 458 ++++----- .../model/io/xml/XMLFeatureModelWriter.java | 534 +++++----- .../model/mixins/IHasCommonAttributes.java | 98 +- .../feature/model/mixins/IHasConstraints.java | 112 +-- .../feature/model/mixins/IHasFeatureTree.java | 192 ++-- .../model/transformer/ComputeFormula.java | 308 +++--- .../featjar/feature/model/AttributeTest.java | 234 ++--- .../feature/model/FeatureModelTest.java | 204 ++-- .../de/featjar/feature/model/FeatureTest.java | 234 ++--- .../featjar/feature/model/IdentifierTest.java | 174 ++-- .../configuration/ConfigurationTest.java | 712 ++++++------- .../io/GraphVizFeatureModelFormatTest.java | 108 +- .../io/XMLFeatureModelFormulaFormatTest.java | 318 +++--- .../model/transformer/ComputeFormulaTest.java | 121 ++- 31 files changed, 4449 insertions(+), 4436 deletions(-) diff --git a/src/main/java/de/featjar/feature/configuration/Configuration.java b/src/main/java/de/featjar/feature/configuration/Configuration.java index fc06f307..c6f6bac9 100644 --- a/src/main/java/de/featjar/feature/configuration/Configuration.java +++ b/src/main/java/de/featjar/feature/configuration/Configuration.java @@ -1,467 +1,467 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.configuration; - -import de.featjar.base.FeatJAR; -import de.featjar.base.data.Result; -import de.featjar.feature.model.IFeature; -import de.featjar.feature.model.IFeatureModel; -import de.featjar.formula.VariableMap; -import de.featjar.formula.assignment.BooleanAssignment; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.ListIterator; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Represents a configuration and provides operations for the configuration process. - */ -public class Configuration implements Cloneable { - - public static final class IllegalSelectionTypeException extends RuntimeException { - private static final long serialVersionUID = 1793844229871267311L; - - public IllegalSelectionTypeException(Class otherType, Class thisType) { - super(String.format( - "Trying to set values of type %s to a feature of type %s", - String.valueOf(otherType), String.valueOf(thisType))); - } - - public IllegalSelectionTypeException(Selection feature, Object selection) { - super(String.format( - "Trying to set the value %s (of type %s) to a feature of type %s", - String.valueOf(selection), String.valueOf(selection.getClass()), feature.getType())); - } - } - - public static final class SelectionNotPossibleException extends RuntimeException { - private static final long serialVersionUID = 1793844229871267311L; - - public SelectionNotPossibleException(Object selection) { - super(String.format("Feature cannot be set to %s", String.valueOf(selection))); - } - } - - /** - * The value of a variable. - * Has an automatic and manual value, which can be set independently. - * - * @param the type of possible values - */ - public static class Selection { - - private final Class type; - private T manual, automatic; - - /** - * Constructs a new selection. - * @param type the type of the selection - */ - public Selection(Class type) { - this.type = type; - } - - private Selection(Selection oldSelectableFeature) { - type = oldSelectableFeature.type; - } - - /** - * {@return type of this selection} - */ - public Class getType() { - return type; - } - - /** - * {@return the combined automatic and manual selection} - * If an automatic value is set, this is returned. Otherwise the manual value is returned. - */ - public T getSelection() { - return automatic == null ? manual : automatic; - } - - /** - * {@return the manual selection} - */ - public T getManual() { - return manual; - } - - /** - * {@return the automatic selection} - */ - public T getAutomatic() { - return automatic; - } - - /** - * Sets the manual value. - * @param selection the value - * - * @throws IllegalSelectionTypeException if the given value is of a different type than this selection - * @throws SelectionNotPossibleException if the value contradicts the automatic value - */ - @SuppressWarnings("unchecked") - public void setManual(Object selection) { - checkIfSelectionPossible(selection, automatic); - manual = (T) selection; - } - - /** - * Sets the automatic value. - * @param selection the value - * - * @throws IllegalSelectionTypeException if the given value is of a different type than this selection - * @throws SelectionNotPossibleException if the value contradicts the manual value - */ - @SuppressWarnings("unchecked") - public void setAutomatic(Object selection) { - checkIfSelectionPossible(selection, manual); - automatic = (T) selection; - } - - private void checkIfSelectionPossible(Object selection, Object current) { - if (selection != null) { - if (!type.isInstance(selection)) { - throw new IllegalSelectionTypeException(this, selection); - } - if ((current != null) && (current != selection)) { - throw new SelectionNotPossibleException(selection); - } - } - } - - /** - * Converts the automatic selection into a manual selection. - */ - public void makeManual() { - if (automatic != null) { - manual = automatic; - automatic = null; - } - } - - /** - * Adopts the values from a given selection. - * @param selection the other selection - * @throws IllegalSelectionTypeException if the type of the given selection is different from this selection's type - */ - @SuppressWarnings("unchecked") - public void adopt(Selection selection) { - if (selection.type != type) { - throw new IllegalSelectionTypeException(selection.type, type); - } - manual = (T) selection.manual; - automatic = (T) selection.automatic; - } - - /** - * Sets manual and automatic values to {@code null}. - */ - public void reset() { - manual = null; - automatic = null; - } - /** - * Sets automatic value to {@code null}. - */ - public void resetAutomatic() { - automatic = null; - } - - @SuppressWarnings("unchecked") - @Override - public Selection clone() { - if (!this.getClass().equals(Selection.class)) { - try { - return (Selection) super.clone(); - } catch (final CloneNotSupportedException e) { - FeatJAR.log().error(e); - throw new RuntimeException("Cloning is not supported for " + this.getClass()); - } - } - Selection selectableFeature = new Selection<>(this); - selectableFeature.manual = this.manual; - selectableFeature.automatic = this.automatic; - return selectableFeature; - } - } - - private VariableMap variableMap; - private ArrayList> selections; - - /** - * Creates an empty configuration. - */ - public Configuration() { - variableMap = new VariableMap(); - selections = new ArrayList<>(); - } - - /** - * Creates a configuration with the same features as the given feature model. - * - * @param featureModel the underlying feature model. - */ - public Configuration(IFeatureModel featureModel) { - variableMap = new VariableMap(); - selections = new ArrayList<>(featureModel.getNumberOfFeatures()); - for (final IFeature child : featureModel.getFeatures()) { - String featureName = child.getName().get(); - int index = variableMap.add(featureName); - for (int i = selections.size(); i <= index; i++) { - selections.add(null); - } - selections.add(index, new Selection<>(child.getType())); - } - } - - /** - * Copy constructor. Copies the status of a given configuration. - * - * @param configuration The configuration to clone - */ - protected Configuration(Configuration configuration) { - variableMap = configuration.variableMap.clone(); - selections = new ArrayList<>(configuration.selections.size()); - for (Selection selection : configuration.selections) { - selections.add(selection != null ? selection.clone() : null); - } - } - - /** - * Creates configuration from literal set. - * - * @param booleanAssignment contains literals with truth values. - * @param variableMap mapping of variable names to indices. Is used to link a literal index in a {@link BooleanAssignment}. - */ - public Configuration(BooleanAssignment booleanAssignment, VariableMap variableMap) { - variableMap = variableMap.clone(); - selections = new ArrayList<>(variableMap.maxIndex()); - adopt(booleanAssignment, variableMap); - } - - /** - * Adopts the values from this assignment. - * - * @param assignment the assignment to adopt - * @param variableMap maps the literals in the assignments to feature names - */ - public void adopt(BooleanAssignment assignment, VariableMap variableMap) { - for (int literal : assignment.get()) { - if (literal != 0) { - int adapedLiteral = variableMap.adapt(literal, this.variableMap, true); - if (adapedLiteral != 0) { - int index = Math.abs(adapedLiteral); - for (int i = selections.size(); i <= index; i++) { - selections.add(null); - } - Selection selection = selections.get(index); - if (selection == null) { - selection = new Selection<>(Boolean.class); - selections.add(index, selection); - } - selection.setManual(adapedLiteral > 0); - } - } - } - } - - /** - * Adopts the values from this assignment. - * Assumes that the variable map of the given assignment is the same as in this configuration. - * - * @param assignment the assignment to adopt - */ - public void adopt(BooleanAssignment assignment) { - for (int literal : assignment.get()) { - if (literal != 0) { - int index = Math.abs(literal); - for (int i = selections.size(); i <= index; i++) { - selections.add(null); - } - Selection selection = selections.get(index); - if (selection == null) { - selection = new Selection<>(Boolean.class); - selections.add(index, selection); - } - selection.setManual(literal > 0); - } - } - } - - /** - * Adopts the values from the given configuration. - * Features not - * - * @param configuration the configuration to adopt - */ - public void adopt(Configuration configuration) { - ListIterator> it = configuration.selections.listIterator(); - while (it.hasNext()) { - Selection otherSelection = it.next(); - if (otherSelection != null) { - int adapedIndex = configuration.variableMap.adapt(it.previousIndex(), variableMap, true); - if (adapedIndex != 0) { - for (int i = selections.size(); i <= adapedIndex; i++) { - selections.add(null); - } - Selection selection = selections.get(adapedIndex); - if (selection == null) { - selection = otherSelection.clone(); - selections.add(adapedIndex, selection); - } else { - selection.adopt(otherSelection); - } - } - } - } - } - - public List> select(Collection features) { - return select(features.stream()).collect(Collectors.toList()); - } - - public Stream> select(Stream features) { - return features.map(this::getSelection).filter(Result::isPresent).map(Result::get); - } - - public VariableMap getVariableMap() { - return variableMap; - } - - public void adapt(VariableMap newVariableMap) { - ArrayList> newSelections = new ArrayList<>(selections.size()); - ListIterator> it = selections.listIterator(); - while (it.hasNext()) { - Selection otherSelection = it.next(); - if (otherSelection != null) { - int adapedIndex = variableMap.adapt(it.previousIndex(), newVariableMap, true); - if (adapedIndex != 0) { - for (int i = selections.size(); i <= adapedIndex; i++) { - newSelections.add(null); - } - newSelections.add(adapedIndex, otherSelection.clone()); - } - } - } - selections.clear(); - selections.addAll(newSelections); - newSelections.clear(); - this.variableMap = newVariableMap; - } - - public List> getSelections() { - return Collections.unmodifiableList(selections); - } - - /** - * {@return a list of all features that have a manual and no automatic value} - */ - public List> getManualFeatures() { - return getSelectionStream() - .filter(f -> f.getAutomatic() == null && f.getManual() != null) - .collect(Collectors.toList()); - } - - /** - * {@return a list of all features that have a automatic value} - */ - public List> getAutomaticFeatures() { - return getSelectionStream().filter(f -> f.getAutomatic() != null).collect(Collectors.toList()); - } - - private Stream> getSelectionStream() { - return selections.stream().filter(Objects::nonNull); - } - - public Result> getSelection(String name) { - return Result.ofNullable(name).flatMap(variableMap::get).map(selections::get); - } - - public Selection get(String name) { - return getSelection(name).orElseThrow(); - } - - public Result> getSelection(IFeature feature) { - return Result.ofNullable(feature) - .flatMap(IFeature::getName) - .flatMap(variableMap::get) - .map(selections::get); - } - - /** - * Turns all automatic into manual values. - */ - public void makeManual() { - getSelectionStream().forEach(Selection::makeManual); - } - - /** - * Resets all values to undefined. - */ - public void reset() { - getSelectionStream().forEach(Selection::reset); - } - - /** - * Resets all automatic values to undefined. - */ - public void resetAutomatic() { - getSelectionStream().forEach(Selection::resetAutomatic); - } - - /** - * Resets automatic values that equal the given selection. - * - * @param selection the selection to reset - */ - public void resetAutomatic(Object selection) { - getSelectionStream().filter(f -> f.getAutomatic() == selection).forEach(Selection::resetAutomatic); - } - - /** - * Creates and returns a copy of this configuration. - * - * @return configuration a clone of this configuration. - */ - @Override - public Configuration clone() { - if (!this.getClass().equals(Configuration.class)) { - try { - return (Configuration) super.clone(); - } catch (final CloneNotSupportedException e) { - FeatJAR.log().error(e); - throw new RuntimeException("Cloning is not supported for " + this.getClass()); - } - } - return new Configuration(this); - } - - @Override - public String toString() { - return getSelectionStream().map(Selection::toString).collect(Collectors.joining("\n")); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.configuration; + +import de.featjar.base.FeatJAR; +import de.featjar.base.data.Result; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.formula.VariableMap; +import de.featjar.formula.assignment.BooleanAssignment; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Represents a configuration and provides operations for the configuration process. + */ +public class Configuration implements Cloneable { + + public static final class IllegalSelectionTypeException extends RuntimeException { + private static final long serialVersionUID = 1793844229871267311L; + + public IllegalSelectionTypeException(Class otherType, Class thisType) { + super(String.format( + "Trying to set values of type %s to a feature of type %s", + String.valueOf(otherType), String.valueOf(thisType))); + } + + public IllegalSelectionTypeException(Selection feature, Object selection) { + super(String.format( + "Trying to set the value %s (of type %s) to a feature of type %s", + String.valueOf(selection), String.valueOf(selection.getClass()), feature.getType())); + } + } + + public static final class SelectionNotPossibleException extends RuntimeException { + private static final long serialVersionUID = 1793844229871267311L; + + public SelectionNotPossibleException(Object selection) { + super(String.format("Feature cannot be set to %s", String.valueOf(selection))); + } + } + + /** + * The value of a variable. + * Has an automatic and manual value, which can be set independently. + * + * @param the type of possible values + */ + public static class Selection { + + private final Class type; + private T manual, automatic; + + /** + * Constructs a new selection. + * @param type the type of the selection + */ + public Selection(Class type) { + this.type = type; + } + + private Selection(Selection oldSelectableFeature) { + type = oldSelectableFeature.type; + } + + /** + * {@return type of this selection} + */ + public Class getType() { + return type; + } + + /** + * {@return the combined automatic and manual selection} + * If an automatic value is set, this is returned. Otherwise the manual value is returned. + */ + public T getSelection() { + return automatic == null ? manual : automatic; + } + + /** + * {@return the manual selection} + */ + public T getManual() { + return manual; + } + + /** + * {@return the automatic selection} + */ + public T getAutomatic() { + return automatic; + } + + /** + * Sets the manual value. + * @param selection the value + * + * @throws IllegalSelectionTypeException if the given value is of a different type than this selection + * @throws SelectionNotPossibleException if the value contradicts the automatic value + */ + @SuppressWarnings("unchecked") + public void setManual(Object selection) { + checkIfSelectionPossible(selection, automatic); + manual = (T) selection; + } + + /** + * Sets the automatic value. + * @param selection the value + * + * @throws IllegalSelectionTypeException if the given value is of a different type than this selection + * @throws SelectionNotPossibleException if the value contradicts the manual value + */ + @SuppressWarnings("unchecked") + public void setAutomatic(Object selection) { + checkIfSelectionPossible(selection, manual); + automatic = (T) selection; + } + + private void checkIfSelectionPossible(Object selection, Object current) { + if (selection != null) { + if (!type.isInstance(selection)) { + throw new IllegalSelectionTypeException(this, selection); + } + if ((current != null) && (current != selection)) { + throw new SelectionNotPossibleException(selection); + } + } + } + + /** + * Converts the automatic selection into a manual selection. + */ + public void makeManual() { + if (automatic != null) { + manual = automatic; + automatic = null; + } + } + + /** + * Adopts the values from a given selection. + * @param selection the other selection + * @throws IllegalSelectionTypeException if the type of the given selection is different from this selection's type + */ + @SuppressWarnings("unchecked") + public void adopt(Selection selection) { + if (selection.type != type) { + throw new IllegalSelectionTypeException(selection.type, type); + } + manual = (T) selection.manual; + automatic = (T) selection.automatic; + } + + /** + * Sets manual and automatic values to {@code null}. + */ + public void reset() { + manual = null; + automatic = null; + } + /** + * Sets automatic value to {@code null}. + */ + public void resetAutomatic() { + automatic = null; + } + + @SuppressWarnings("unchecked") + @Override + public Selection clone() { + if (!this.getClass().equals(Selection.class)) { + try { + return (Selection) super.clone(); + } catch (final CloneNotSupportedException e) { + FeatJAR.log().error(e); + throw new RuntimeException("Cloning is not supported for " + this.getClass()); + } + } + Selection selectableFeature = new Selection<>(this); + selectableFeature.manual = this.manual; + selectableFeature.automatic = this.automatic; + return selectableFeature; + } + } + + private VariableMap variableMap; + private ArrayList> selections; + + /** + * Creates an empty configuration. + */ + public Configuration() { + variableMap = new VariableMap(); + selections = new ArrayList<>(); + } + + /** + * Creates a configuration with the same features as the given feature model. + * + * @param featureModel the underlying feature model. + */ + public Configuration(IFeatureModel featureModel) { + variableMap = new VariableMap(); + selections = new ArrayList<>(featureModel.getNumberOfFeatures()); + for (final IFeature child : featureModel.getFeatures()) { + String featureName = child.getName().get(); + int index = variableMap.add(featureName); + for (int i = selections.size(); i <= index; i++) { + selections.add(null); + } + selections.add(index, new Selection<>(child.getType())); + } + } + + /** + * Copy constructor. Copies the status of a given configuration. + * + * @param configuration The configuration to clone + */ + protected Configuration(Configuration configuration) { + variableMap = configuration.variableMap.clone(); + selections = new ArrayList<>(configuration.selections.size()); + for (Selection selection : configuration.selections) { + selections.add(selection != null ? selection.clone() : null); + } + } + + /** + * Creates configuration from literal set. + * + * @param booleanAssignment contains literals with truth values. + * @param variableMap mapping of variable names to indices. Is used to link a literal index in a {@link BooleanAssignment}. + */ + public Configuration(BooleanAssignment booleanAssignment, VariableMap variableMap) { + variableMap = variableMap.clone(); + selections = new ArrayList<>(variableMap.maxIndex()); + adopt(booleanAssignment, variableMap); + } + + /** + * Adopts the values from this assignment. + * + * @param assignment the assignment to adopt + * @param variableMap maps the literals in the assignments to feature names + */ + public void adopt(BooleanAssignment assignment, VariableMap variableMap) { + for (int literal : assignment.get()) { + if (literal != 0) { + int adapedLiteral = variableMap.adapt(literal, this.variableMap, true); + if (adapedLiteral != 0) { + int index = Math.abs(adapedLiteral); + for (int i = selections.size(); i <= index; i++) { + selections.add(null); + } + Selection selection = selections.get(index); + if (selection == null) { + selection = new Selection<>(Boolean.class); + selections.add(index, selection); + } + selection.setManual(adapedLiteral > 0); + } + } + } + } + + /** + * Adopts the values from this assignment. + * Assumes that the variable map of the given assignment is the same as in this configuration. + * + * @param assignment the assignment to adopt + */ + public void adopt(BooleanAssignment assignment) { + for (int literal : assignment.get()) { + if (literal != 0) { + int index = Math.abs(literal); + for (int i = selections.size(); i <= index; i++) { + selections.add(null); + } + Selection selection = selections.get(index); + if (selection == null) { + selection = new Selection<>(Boolean.class); + selections.add(index, selection); + } + selection.setManual(literal > 0); + } + } + } + + /** + * Adopts the values from the given configuration. + * Features not + * + * @param configuration the configuration to adopt + */ + public void adopt(Configuration configuration) { + ListIterator> it = configuration.selections.listIterator(); + while (it.hasNext()) { + Selection otherSelection = it.next(); + if (otherSelection != null) { + int adapedIndex = configuration.variableMap.adapt(it.previousIndex(), variableMap, true); + if (adapedIndex != 0) { + for (int i = selections.size(); i <= adapedIndex; i++) { + selections.add(null); + } + Selection selection = selections.get(adapedIndex); + if (selection == null) { + selection = otherSelection.clone(); + selections.add(adapedIndex, selection); + } else { + selection.adopt(otherSelection); + } + } + } + } + } + + public List> select(Collection features) { + return select(features.stream()).collect(Collectors.toList()); + } + + public Stream> select(Stream features) { + return features.map(this::getSelection).filter(Result::isPresent).map(Result::get); + } + + public VariableMap getVariableMap() { + return variableMap; + } + + public void adapt(VariableMap newVariableMap) { + ArrayList> newSelections = new ArrayList<>(selections.size()); + ListIterator> it = selections.listIterator(); + while (it.hasNext()) { + Selection otherSelection = it.next(); + if (otherSelection != null) { + int adapedIndex = variableMap.adapt(it.previousIndex(), newVariableMap, true); + if (adapedIndex != 0) { + for (int i = selections.size(); i <= adapedIndex; i++) { + newSelections.add(null); + } + newSelections.add(adapedIndex, otherSelection.clone()); + } + } + } + selections.clear(); + selections.addAll(newSelections); + newSelections.clear(); + this.variableMap = newVariableMap; + } + + public List> getSelections() { + return Collections.unmodifiableList(selections); + } + + /** + * {@return a list of all features that have a manual and no automatic value} + */ + public List> getManualFeatures() { + return getSelectionStream() + .filter(f -> f.getAutomatic() == null && f.getManual() != null) + .collect(Collectors.toList()); + } + + /** + * {@return a list of all features that have a automatic value} + */ + public List> getAutomaticFeatures() { + return getSelectionStream().filter(f -> f.getAutomatic() != null).collect(Collectors.toList()); + } + + private Stream> getSelectionStream() { + return selections.stream().filter(Objects::nonNull); + } + + public Result> getSelection(String name) { + return Result.ofNullable(name).flatMap(variableMap::get).map(selections::get); + } + + public Selection get(String name) { + return getSelection(name).orElseThrow(); + } + + public Result> getSelection(IFeature feature) { + return Result.ofNullable(feature) + .flatMap(IFeature::getName) + .flatMap(variableMap::get) + .map(selections::get); + } + + /** + * Turns all automatic into manual values. + */ + public void makeManual() { + getSelectionStream().forEach(Selection::makeManual); + } + + /** + * Resets all values to undefined. + */ + public void reset() { + getSelectionStream().forEach(Selection::reset); + } + + /** + * Resets all automatic values to undefined. + */ + public void resetAutomatic() { + getSelectionStream().forEach(Selection::resetAutomatic); + } + + /** + * Resets automatic values that equal the given selection. + * + * @param selection the selection to reset + */ + public void resetAutomatic(Object selection) { + getSelectionStream().filter(f -> f.getAutomatic() == selection).forEach(Selection::resetAutomatic); + } + + /** + * Creates and returns a copy of this configuration. + * + * @return configuration a clone of this configuration. + */ + @Override + public Configuration clone() { + if (!this.getClass().equals(Configuration.class)) { + try { + return (Configuration) super.clone(); + } catch (final CloneNotSupportedException e) { + FeatJAR.log().error(e); + throw new RuntimeException("Cloning is not supported for " + this.getClass()); + } + } + return new Configuration(this); + } + + @Override + public String toString() { + return getSelectionStream().map(Selection::toString).collect(Collectors.joining("\n")); + } +} diff --git a/src/main/java/de/featjar/feature/configuration/io/FeatureIDEFormat.java b/src/main/java/de/featjar/feature/configuration/io/FeatureIDEFormat.java index d4b1406e..580ad731 100644 --- a/src/main/java/de/featjar/feature/configuration/io/FeatureIDEFormat.java +++ b/src/main/java/de/featjar/feature/configuration/io/FeatureIDEFormat.java @@ -1,179 +1,179 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.configuration.io; - -import de.featjar.base.data.Problem; -import de.featjar.base.data.Problem.Severity; -import de.featjar.base.data.Result; -import de.featjar.base.io.format.IFormat; -import de.featjar.base.io.format.ParseProblem; -import de.featjar.base.io.input.AInputMapper; -import de.featjar.feature.configuration.Configuration; -import de.featjar.feature.configuration.Configuration.Selection; -import de.featjar.feature.configuration.Configuration.SelectionNotPossibleException; -import java.io.BufferedReader; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -/** - * Extended configuration format for FeatureIDE projects.
Lists all features and indicates the manual and automatic selection. - * - * @author Sebastian Krieter - */ -public class FeatureIDEFormat implements IFormat { - - private static final String NEWLINE = System.lineSeparator(); - - /** - * Parses a String representation of a FeatureIDE Format into a Configuration. - * - * @param inputmapper the input mapper - * @return Configuration inside the Result wrapper - */ - @Override - public Result parse(AInputMapper inputmapper) { - Configuration configuration = new Configuration(); - List warnings = new ArrayList<>(); - - String line = null; - int lineNumber = 1; - try (BufferedReader reader = inputmapper.get().getReader(); ) { - while ((line = reader.readLine()) != null) { - if (line.startsWith("#")) { - continue; - } - line = line.trim(); - if (!line.isEmpty()) { - Boolean manual = null; - Boolean automatic = null; - try { - switch (Integer.parseInt(line.substring(0, 1))) { - case 0: - manual = Boolean.FALSE; - break; - case 1: - manual = Boolean.TRUE; - break; - case 2: - break; - default: - warnings.add(new ParseProblem(line, Severity.WARNING, lineNumber)); - break; - } - switch (Integer.parseInt(line.substring(1, 2))) { - case 0: - automatic = Boolean.FALSE; - break; - case 1: - automatic = Boolean.TRUE; - break; - case 2: - break; - default: - warnings.add(new ParseProblem(line, Severity.WARNING, lineNumber)); - break; - } - } catch (final NumberFormatException e) { - warnings.add(new ParseProblem(e, lineNumber)); - } - - final String name = line.substring(2); - - final Result> feature = configuration.getSelection(name); - if (feature.isEmpty()) { - warnings.add(new ParseProblem(name, Severity.ERROR, lineNumber)); - } else { - try { - feature.get().setManual(manual); - feature.get().setAutomatic(automatic); - } catch (final SelectionNotPossibleException e) { - warnings.add(new ParseProblem(e, lineNumber)); - } - } - } - lineNumber++; - } - } catch (final IOException e) { - warnings.add(new Problem(e)); - } - - return Result.of(configuration, warnings); - } - - /** - * Returns the String representation of a Configuration in the FeatureIDE Format. - * - * @param configuration the object - * @return String representation of the Configuration inside the Result wrapper - */ - @Override - public Result serialize(Configuration configuration) { - final StringBuilder buffer = new StringBuilder(); - buffer.append( - "# Lists all features from the model with manual (first digit) and automatic (second digit) selection"); - buffer.append(NEWLINE); - buffer.append("# 0 = deselected, 1 = selected, 2 = undefined"); - buffer.append(NEWLINE); - - for (final String name : configuration.getVariableMap().getVariableNames()) { - Selection selection = configuration.get(name); - buffer.append(Integer.toString(getSelectionCode((Boolean) selection.getManual()))); - buffer.append(Integer.toString(getSelectionCode((Boolean) selection.getAutomatic()))); - buffer.append(name); - buffer.append(NEWLINE); - } - - return Result.of(buffer.toString()); - } - - private int getSelectionCode(Boolean selection) { - if (selection == null) { - return 2; - } else if (selection == Boolean.TRUE) { - return 1; - } else if (selection == Boolean.FALSE) { - return 0; - } else { - return 3; - } - } - - @Override - public String getFileExtension() { - return ".config"; - } - - @Override - public boolean supportsParse() { - return true; - } - - @Override - public boolean supportsWrite() { - return true; - } - - @Override - public String getName() { - return "FeatureIDE-Internal"; - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.configuration.io; + +import de.featjar.base.data.Problem; +import de.featjar.base.data.Problem.Severity; +import de.featjar.base.data.Result; +import de.featjar.base.io.format.IFormat; +import de.featjar.base.io.format.ParseProblem; +import de.featjar.base.io.input.AInputMapper; +import de.featjar.feature.configuration.Configuration; +import de.featjar.feature.configuration.Configuration.Selection; +import de.featjar.feature.configuration.Configuration.SelectionNotPossibleException; +import java.io.BufferedReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Extended configuration format for FeatureIDE projects.
Lists all features and indicates the manual and automatic selection. + * + * @author Sebastian Krieter + */ +public class FeatureIDEFormat implements IFormat { + + private static final String NEWLINE = System.lineSeparator(); + + /** + * Parses a String representation of a FeatureIDE Format into a Configuration. + * + * @param inputmapper the input mapper + * @return Configuration inside the Result wrapper + */ + @Override + public Result parse(AInputMapper inputmapper) { + Configuration configuration = new Configuration(); + List warnings = new ArrayList<>(); + + String line = null; + int lineNumber = 1; + try (BufferedReader reader = inputmapper.get().getReader(); ) { + while ((line = reader.readLine()) != null) { + if (line.startsWith("#")) { + continue; + } + line = line.trim(); + if (!line.isEmpty()) { + Boolean manual = null; + Boolean automatic = null; + try { + switch (Integer.parseInt(line.substring(0, 1))) { + case 0: + manual = Boolean.FALSE; + break; + case 1: + manual = Boolean.TRUE; + break; + case 2: + break; + default: + warnings.add(new ParseProblem(line, Severity.WARNING, lineNumber)); + break; + } + switch (Integer.parseInt(line.substring(1, 2))) { + case 0: + automatic = Boolean.FALSE; + break; + case 1: + automatic = Boolean.TRUE; + break; + case 2: + break; + default: + warnings.add(new ParseProblem(line, Severity.WARNING, lineNumber)); + break; + } + } catch (final NumberFormatException e) { + warnings.add(new ParseProblem(e, lineNumber)); + } + + final String name = line.substring(2); + + final Result> feature = configuration.getSelection(name); + if (feature.isEmpty()) { + warnings.add(new ParseProblem(name, Severity.ERROR, lineNumber)); + } else { + try { + feature.get().setManual(manual); + feature.get().setAutomatic(automatic); + } catch (final SelectionNotPossibleException e) { + warnings.add(new ParseProblem(e, lineNumber)); + } + } + } + lineNumber++; + } + } catch (final IOException e) { + warnings.add(new Problem(e)); + } + + return Result.of(configuration, warnings); + } + + /** + * Returns the String representation of a Configuration in the FeatureIDE Format. + * + * @param configuration the object + * @return String representation of the Configuration inside the Result wrapper + */ + @Override + public Result serialize(Configuration configuration) { + final StringBuilder buffer = new StringBuilder(); + buffer.append( + "# Lists all features from the model with manual (first digit) and automatic (second digit) selection"); + buffer.append(NEWLINE); + buffer.append("# 0 = deselected, 1 = selected, 2 = undefined"); + buffer.append(NEWLINE); + + for (final String name : configuration.getVariableMap().getVariableNames()) { + Selection selection = configuration.get(name); + buffer.append(Integer.toString(getSelectionCode((Boolean) selection.getManual()))); + buffer.append(Integer.toString(getSelectionCode((Boolean) selection.getAutomatic()))); + buffer.append(name); + buffer.append(NEWLINE); + } + + return Result.of(buffer.toString()); + } + + private int getSelectionCode(Boolean selection) { + if (selection == null) { + return 2; + } else if (selection == Boolean.TRUE) { + return 1; + } else if (selection == Boolean.FALSE) { + return 0; + } else { + return 3; + } + } + + @Override + public String getFileExtension() { + return ".config"; + } + + @Override + public boolean supportsParse() { + return true; + } + + @Override + public boolean supportsWrite() { + return true; + } + + @Override + public String getName() { + return "FeatureIDE-Internal"; + } +} diff --git a/src/main/java/de/featjar/feature/model/AFeatureModelElement.java b/src/main/java/de/featjar/feature/model/AFeatureModelElement.java index ee281c43..cc87ab75 100644 --- a/src/main/java/de/featjar/feature/model/AFeatureModelElement.java +++ b/src/main/java/de/featjar/feature/model/AFeatureModelElement.java @@ -1,102 +1,102 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.data.Attribute; -import de.featjar.base.data.IAttributable.IMutatableAttributable; -import de.featjar.base.data.IAttribute; -import de.featjar.base.data.identifier.AIdentifier; -import de.featjar.base.data.identifier.IIdentifier; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; - -/** - * Implements identification and attribute valuation. - * Each {@link FeatureModel} and all its {@link Feature features} and {@link Constraint constraints} are - * uniquely identified by some {@link AIdentifier}. - * Also, each element can be annotated with arbitrary {@link Attribute attributes}. - * - * @author Elias Kuiter - */ -public abstract class AFeatureModelElement implements IFeatureModelElement, IMutatableAttributable { - protected final IFeatureModel featureModel; - protected final IIdentifier identifier; - protected final LinkedHashMap, Object> attributeValues; - - public AFeatureModelElement(IFeatureModel featureModel) { - this.featureModel = Objects.requireNonNull(featureModel); - identifier = featureModel.getNewIdentifier(); - attributeValues = new LinkedHashMap<>(4); - } - - protected AFeatureModelElement(AFeatureModelElement otherElement, IFeatureModel featureModel) { - this.featureModel = featureModel; - identifier = otherElement.getNewIdentifier(); - attributeValues = otherElement.cloneAttributes(); - } - - @Override - public IIdentifier getIdentifier() { - return identifier; - } - - @Override - public Optional, Object>> getAttributes() { - return Optional.of(Collections.unmodifiableMap(attributeValues)); - } - - @Override - public void setAttributeValue(Attribute attribute, S value) { - if (value == null) { - removeAttributeValue(attribute); - return; - } - checkType(attribute, value); - validate(attribute, value); - attributeValues.put(attribute, value); - } - - @Override - @SuppressWarnings("unchecked") - public S removeAttributeValue(Attribute attribute) { - return (S) attributeValues.remove(attribute); - } - - @Override - public IFeatureModel getFeatureModel() { - return featureModel; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - return getIdentifier().equals(((AFeatureModelElement) o).getIdentifier()); - } - - @Override - public int hashCode() { - return Objects.hash(getIdentifier()); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.data.Attribute; +import de.featjar.base.data.IAttributable.IMutatableAttributable; +import de.featjar.base.data.IAttribute; +import de.featjar.base.data.identifier.AIdentifier; +import de.featjar.base.data.identifier.IIdentifier; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * Implements identification and attribute valuation. + * Each {@link FeatureModel} and all its {@link Feature features} and {@link Constraint constraints} are + * uniquely identified by some {@link AIdentifier}. + * Also, each element can be annotated with arbitrary {@link Attribute attributes}. + * + * @author Elias Kuiter + */ +public abstract class AFeatureModelElement implements IFeatureModelElement, IMutatableAttributable { + protected final IFeatureModel featureModel; + protected final IIdentifier identifier; + protected final LinkedHashMap, Object> attributeValues; + + public AFeatureModelElement(IFeatureModel featureModel) { + this.featureModel = Objects.requireNonNull(featureModel); + identifier = featureModel.getNewIdentifier(); + attributeValues = new LinkedHashMap<>(4); + } + + protected AFeatureModelElement(AFeatureModelElement otherElement, IFeatureModel featureModel) { + this.featureModel = featureModel; + identifier = otherElement.getNewIdentifier(); + attributeValues = otherElement.cloneAttributes(); + } + + @Override + public IIdentifier getIdentifier() { + return identifier; + } + + @Override + public Optional, Object>> getAttributes() { + return Optional.of(Collections.unmodifiableMap(attributeValues)); + } + + @Override + public void setAttributeValue(Attribute attribute, S value) { + if (value == null) { + removeAttributeValue(attribute); + return; + } + checkType(attribute, value); + validate(attribute, value); + attributeValues.put(attribute, value); + } + + @Override + @SuppressWarnings("unchecked") + public S removeAttributeValue(Attribute attribute) { + return (S) attributeValues.remove(attribute); + } + + @Override + public IFeatureModel getFeatureModel() { + return featureModel; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return getIdentifier().equals(((AFeatureModelElement) o).getIdentifier()); + } + + @Override + public int hashCode() { + return Objects.hash(getIdentifier()); + } +} diff --git a/src/main/java/de/featjar/feature/model/Attributes.java b/src/main/java/de/featjar/feature/model/Attributes.java index 8c5838a2..fb0ada1a 100644 --- a/src/main/java/de/featjar/feature/model/Attributes.java +++ b/src/main/java/de/featjar/feature/model/Attributes.java @@ -1,98 +1,98 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.data.Attribute; -import de.featjar.base.data.Sets; -import de.featjar.base.data.identifier.IIdentifiable; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Set; - -/** - * Defines useful {@link Attribute attributes} for {@link FeatureModel feature models}, - * {@link Feature features}, and {@link Constraint constraints}. - * - * @author Elias Kuiter - * @author Sebastian Krieter - */ -public class Attributes { - - private static final LinkedHashMap, Attribute> attributeSet = new LinkedHashMap<>(); - - public static final String NAMESPACE = Attributes.class.getCanonicalName(); - - public static final Attribute NAME = get(NAMESPACE, "name", String.class) - .setDefaultValueFunction(identifiable -> - "@" + ((IIdentifiable) identifiable).getIdentifier().toString()) - .setValidator( - (element, name) -> // TODO: can also be name of feature model or constraint, but this validates only - // feature name uniqueness - ((AFeatureModelElement) element) - .getFeatureModel() - .getFeature((String) name) - .isEmpty()); - - public static final Attribute DESCRIPTION = get(NAMESPACE, "description", String.class); - - @SuppressWarnings({"rawtypes", "unchecked"}) - public static final Attribute> TAGS = getRaw(NAMESPACE, "tags", LinkedHashSet.class) - .setDefaultValueFunction(attributable -> Sets.empty()) - .setCopyValueFunction(set -> new LinkedHashSet((Collection) set)); - - public static final Attribute HIDDEN = - get(NAMESPACE, "hidden", Boolean.class).setDefaultValue(false); - - public static final Attribute ABSTRACT = - get(NAMESPACE, "abstract", Boolean.class).setDefaultValue(false); - - public static Set> getAllAttributes() { - return Collections.unmodifiableSet(attributeSet.keySet()); - } - - public static Attribute get(String name, Class type) { - return get(NAMESPACE, name, type); - } - - @SuppressWarnings("unchecked") - public static Attribute get(String namespace, String name, Class type) { - return getRaw(namespace, name, type); - } - - @SuppressWarnings("rawtypes") - public static Attribute getRaw(String namespace, String name, Class type) { - Attribute attribute = new Attribute<>(namespace, name, type); - Attribute cachedAttribute = attributeSet.get(attribute); - if (cachedAttribute == null) { - attributeSet.put(attribute, attribute); - return attribute; - } else { - if (type != cachedAttribute.getType()) { - throw new IllegalArgumentException(String.format( - "Cannot create attribute for type %s. Attribute already defined for type %s.", - type.toString(), cachedAttribute.getType())); - } - return cachedAttribute; - } - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.data.Attribute; +import de.featjar.base.data.Sets; +import de.featjar.base.data.identifier.IIdentifiable; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Defines useful {@link Attribute attributes} for {@link FeatureModel feature models}, + * {@link Feature features}, and {@link Constraint constraints}. + * + * @author Elias Kuiter + * @author Sebastian Krieter + */ +public class Attributes { + + private static final LinkedHashMap, Attribute> attributeSet = new LinkedHashMap<>(); + + public static final String NAMESPACE = Attributes.class.getCanonicalName(); + + public static final Attribute NAME = get(NAMESPACE, "name", String.class) + .setDefaultValueFunction(identifiable -> + "@" + ((IIdentifiable) identifiable).getIdentifier().toString()) + .setValidator( + (element, name) -> // TODO: can also be name of feature model or constraint, but this validates only + // feature name uniqueness + ((AFeatureModelElement) element) + .getFeatureModel() + .getFeature((String) name) + .isEmpty()); + + public static final Attribute DESCRIPTION = get(NAMESPACE, "description", String.class); + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static final Attribute> TAGS = getRaw(NAMESPACE, "tags", LinkedHashSet.class) + .setDefaultValueFunction(attributable -> Sets.empty()) + .setCopyValueFunction(set -> new LinkedHashSet((Collection) set)); + + public static final Attribute HIDDEN = + get(NAMESPACE, "hidden", Boolean.class).setDefaultValue(false); + + public static final Attribute ABSTRACT = + get(NAMESPACE, "abstract", Boolean.class).setDefaultValue(false); + + public static Set> getAllAttributes() { + return Collections.unmodifiableSet(attributeSet.keySet()); + } + + public static Attribute get(String name, Class type) { + return get(NAMESPACE, name, type); + } + + @SuppressWarnings("unchecked") + public static Attribute get(String namespace, String name, Class type) { + return getRaw(namespace, name, type); + } + + @SuppressWarnings("rawtypes") + public static Attribute getRaw(String namespace, String name, Class type) { + Attribute attribute = new Attribute<>(namespace, name, type); + Attribute cachedAttribute = attributeSet.get(attribute); + if (cachedAttribute == null) { + attributeSet.put(attribute, attribute); + return attribute; + } else { + if (type != cachedAttribute.getType()) { + throw new IllegalArgumentException(String.format( + "Cannot create attribute for type %s. Attribute already defined for type %s.", + type.toString(), cachedAttribute.getType())); + } + return cachedAttribute; + } + } +} diff --git a/src/main/java/de/featjar/feature/model/Constraint.java b/src/main/java/de/featjar/feature/model/Constraint.java index 4c6a9a78..71c44a8d 100644 --- a/src/main/java/de/featjar/feature/model/Constraint.java +++ b/src/main/java/de/featjar/feature/model/Constraint.java @@ -1,84 +1,84 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.tree.Trees; -import de.featjar.feature.model.IConstraint.IMutableConstraint; -import de.featjar.formula.structure.IFormula; -import java.util.LinkedHashSet; - -public class Constraint extends AFeatureModelElement implements IMutableConstraint { - protected IFormula formula; - - protected Constraint(IFeatureModel featureModel, IFormula formula) { - super(featureModel); - setFormula(formula); - } - - protected Constraint(Constraint otherConstraint) { - this(otherConstraint, otherConstraint.featureModel); - } - - protected Constraint(Constraint otherConstraint, IFeatureModel newFeatureModel) { - super(otherConstraint, newFeatureModel); - setFormula(Trees.clone(otherConstraint.formula)); - } - - @Override - public Constraint clone() { - return new Constraint(this); - } - - @Override - public Constraint clone(IFeatureModel newFeatureModel) { - return new Constraint(this); - } - - @Override - public IFormula getFormula() { - return formula; - } - - @Override - public LinkedHashSet getReferencedFeatures() { - return IConstraint.getReferencedFeatures(formula, featureModel); - } - - @Override - public String toString() { - return String.format("Constraint{formula=%s}", formula); - } - - @Override - public void setFormula(IFormula formula) { - this.formula = formula; - } - - @Override - public void setName(String name) { - attributeValues.put(Attributes.NAME, name); - } - - @Override - public void setDescription(String description) { - attributeValues.put(Attributes.DESCRIPTION, description); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.tree.Trees; +import de.featjar.feature.model.IConstraint.IMutableConstraint; +import de.featjar.formula.structure.IFormula; +import java.util.LinkedHashSet; + +public class Constraint extends AFeatureModelElement implements IMutableConstraint { + protected IFormula formula; + + protected Constraint(IFeatureModel featureModel, IFormula formula) { + super(featureModel); + setFormula(formula); + } + + protected Constraint(Constraint otherConstraint) { + this(otherConstraint, otherConstraint.featureModel); + } + + protected Constraint(Constraint otherConstraint, IFeatureModel newFeatureModel) { + super(otherConstraint, newFeatureModel); + setFormula(Trees.clone(otherConstraint.formula)); + } + + @Override + public Constraint clone() { + return new Constraint(this); + } + + @Override + public Constraint clone(IFeatureModel newFeatureModel) { + return new Constraint(this); + } + + @Override + public IFormula getFormula() { + return formula; + } + + @Override + public LinkedHashSet getReferencedFeatures() { + return IConstraint.getReferencedFeatures(formula, featureModel); + } + + @Override + public String toString() { + return String.format("Constraint{formula=%s}", formula); + } + + @Override + public void setFormula(IFormula formula) { + this.formula = formula; + } + + @Override + public void setName(String name) { + attributeValues.put(Attributes.NAME, name); + } + + @Override + public void setDescription(String description) { + attributeValues.put(Attributes.DESCRIPTION, description); + } +} diff --git a/src/main/java/de/featjar/feature/model/Feature.java b/src/main/java/de/featjar/feature/model/Feature.java index 3b4266a3..01fd4a9a 100644 --- a/src/main/java/de/featjar/feature/model/Feature.java +++ b/src/main/java/de/featjar/feature/model/Feature.java @@ -1,82 +1,82 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.data.Result; -import de.featjar.feature.model.IFeature.IMutableFeature; - -public class Feature extends AFeatureModelElement implements IMutableFeature { - protected Class type; - - protected Feature(IFeatureModel featureModel) { - super(featureModel); - type = Boolean.class; - } - - protected Feature(Feature otherFeature) { - this(otherFeature, otherFeature.featureModel); - } - - protected Feature(Feature otherFeature, IFeatureModel newFeatureModel) { - super(otherFeature, newFeatureModel); - type = otherFeature.type; - } - - @Override - public Feature clone() { - return new Feature(this); - } - - @Override - public Feature clone(IFeatureModel newFeatureModel) { - return new Feature(this); - } - - @Override - public Class getType() { - return type; - } - - @Override - public Result getFeatureTree() { - return featureModel.getFeatureTree(this); - } - - @Override - public void setType(Class type) { - this.type = type; - } - - @Override - public String toString() { - return String.format("Feature{name=%s}", getName().orElse("")); - } - - @Override - public void setName(String name) { - attributeValues.put(Attributes.NAME, name); - } - - @Override - public void setDescription(String description) { - attributeValues.put(Attributes.DESCRIPTION, description); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.data.Result; +import de.featjar.feature.model.IFeature.IMutableFeature; + +public class Feature extends AFeatureModelElement implements IMutableFeature { + protected Class type; + + protected Feature(IFeatureModel featureModel) { + super(featureModel); + type = Boolean.class; + } + + protected Feature(Feature otherFeature) { + this(otherFeature, otherFeature.featureModel); + } + + protected Feature(Feature otherFeature, IFeatureModel newFeatureModel) { + super(otherFeature, newFeatureModel); + type = otherFeature.type; + } + + @Override + public Feature clone() { + return new Feature(this); + } + + @Override + public Feature clone(IFeatureModel newFeatureModel) { + return new Feature(this); + } + + @Override + public Class getType() { + return type; + } + + @Override + public Result getFeatureTree() { + return featureModel.getFeatureTree(this); + } + + @Override + public void setType(Class type) { + this.type = type; + } + + @Override + public String toString() { + return String.format("Feature{name=%s}", getName().orElse("")); + } + + @Override + public void setName(String name) { + attributeValues.put(Attributes.NAME, name); + } + + @Override + public void setDescription(String description) { + attributeValues.put(Attributes.DESCRIPTION, description); + } +} diff --git a/src/main/java/de/featjar/feature/model/FeatureModel.java b/src/main/java/de/featjar/feature/model/FeatureModel.java index 6921d5cd..312efeaf 100644 --- a/src/main/java/de/featjar/feature/model/FeatureModel.java +++ b/src/main/java/de/featjar/feature/model/FeatureModel.java @@ -1,273 +1,273 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.data.Attribute; -import de.featjar.base.data.IAttributable.IMutatableAttributable; -import de.featjar.base.data.IAttribute; -import de.featjar.base.data.Maps; -import de.featjar.base.data.Result; -import de.featjar.base.data.identifier.IIdentifier; -import de.featjar.base.data.identifier.UUIDIdentifier; -import de.featjar.base.tree.Trees; -import de.featjar.feature.model.IFeatureModel.IMutableFeatureModel; -import de.featjar.formula.structure.IFormula; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; - -public class FeatureModel implements IMutableFeatureModel, IMutatableAttributable { - - protected final IIdentifier identifier; - - protected final List featureTreeRoots; - protected final LinkedHashMap features; - protected final LinkedHashMap constraints; - - protected final LinkedHashMap, Object> attributeValues; - - public FeatureModel() { - this(UUIDIdentifier.newInstance()); - } - - public FeatureModel(IIdentifier identifier) { - this.identifier = Objects.requireNonNull(identifier); - featureTreeRoots = new ArrayList<>(1); - features = Maps.empty(); - constraints = Maps.empty(); - attributeValues = new LinkedHashMap<>(4); - } - - protected FeatureModel(FeatureModel otherFeatureModel) { - identifier = otherFeatureModel.getNewIdentifier(); - - featureTreeRoots = new ArrayList<>(otherFeatureModel.featureTreeRoots.size()); - otherFeatureModel.featureTreeRoots.stream().forEach(t -> featureTreeRoots.add(Trees.clone(t))); - - features = new LinkedHashMap<>((int) (otherFeatureModel.features.size() * 1.5)); - otherFeatureModel.features.entrySet().stream() - .map(e -> e.getValue().clone(this)) - .forEach(f -> features.put(f.getIdentifier(), f)); - - constraints = new LinkedHashMap<>((int) (otherFeatureModel.constraints.size() * 1.5)); - otherFeatureModel.constraints.entrySet().stream() - .map(e -> e.getValue().clone(this)) - .forEach(c -> constraints.put(c.getIdentifier(), c)); - - attributeValues = otherFeatureModel.cloneAttributes(); - } - - @Override - public FeatureModel clone() { - return new FeatureModel(this); - } - - @Override - public FeatureModel getFeatureModel() { - return this; - } - - @Override - public List getRoots() { - return featureTreeRoots; - } - - @Override - public Collection getFeatures() { - return Collections.unmodifiableCollection(features.values()); - } - - @Override - public Result getFeature(IIdentifier identifier) { - return Result.of(features.get(Objects.requireNonNull(identifier))); - } - - @Override - public Collection getConstraints() { - return Collections.unmodifiableCollection(constraints.values()); - } - - @Override - public Result getConstraint(IIdentifier identifier) { - return Result.of(constraints.get(Objects.requireNonNull(identifier))); - } - - @Override - public boolean hasConstraint(IIdentifier identifier) { - return constraints.containsKey(identifier); - } - - @Override - public boolean hasConstraint(IConstraint constraint) { - return constraints.containsKey(constraint.getIdentifier()); - } - - @Override - public int getNumberOfConstraints() { - return constraints.size(); - } - - @Override - public IIdentifier getIdentifier() { - return identifier; - } - - @Override - public Optional, Object>> getAttributes() { - return Optional.of(Collections.unmodifiableMap(attributeValues)); - } - - @Override - public void setAttributeValue(Attribute attribute, S value) { - if (value == null) { - removeAttributeValue(attribute); - return; - } - checkType(attribute, value); - validate(attribute, value); - attributeValues.put(attribute, value); - } - - @Override - @SuppressWarnings("unchecked") - public S removeAttributeValue(Attribute attribute) { - return (S) attributeValues.remove(attribute); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - return getIdentifier().equals(((FeatureModel) o).getIdentifier()); - } - - @Override - public int hashCode() { - return Objects.hash(getIdentifier()); - } - - @Override - public String toString() { - StringBuilder featureString = new StringBuilder(); - for (IFeatureTree root : featureTreeRoots) { - featureString.append(root.print()); - featureString.append('\n'); - } - return String.format( - "FeatureModel{features=%s, constraints=%s}", featureString.toString(), constraints.toString()); - } - - @Override - public void setName(String name) { - attributeValues.put(Attributes.NAME, name); - } - - @Override - public void setDescription(String description) { - attributeValues.put(Attributes.DESCRIPTION, description); - } - - @Override - public IFeatureTree addFeatureTreeRoot(IFeature feature) { - FeatureTree newTree = new FeatureTree(feature); - featureTreeRoots.add(newTree); - return newTree; - } - - @Override - public void addFeatureTreeRoot(IFeatureTree featureTree) { - featureTreeRoots.add(featureTree); - } - - @Override - public void removeFeatureTreeRoot(IFeature feature) { - for (Iterator it = featureTreeRoots.listIterator(); it.hasNext(); ) { - if (it.next().getFeature().equals(feature)) { - it.remove(); - } - } - } - - @Override - public void removeFeatureTreeRoot(IFeatureTree featureTree) { - for (Iterator it = featureTreeRoots.listIterator(); it.hasNext(); ) { - if (it.next() == featureTree) { - it.remove(); - } - } - } - - @Override - public IConstraint addConstraint(IFormula formula) { - IConstraint newConstraint = new Constraint(this, Trees.clone(formula)); - constraints.put(newConstraint.getIdentifier(), newConstraint); - return newConstraint; - } - - @Override - public boolean removeConstraint(IConstraint constraint) { - Objects.requireNonNull(constraint); - return constraints.remove(constraint.getIdentifier()) != null; - } - - @Override - public IFeature addFeature(String name) { - Objects.requireNonNull(name); - Feature feature = new Feature(this); - feature.setName(name); - features.put(feature.getIdentifier(), feature); - return feature; - } - - @Override - public boolean removeFeature(IFeature feature) { - return features.remove(feature.getIdentifier()) != null; - } - - @Override - public int getNumberOfFeatures() { - return features.size(); - } - - @Override - public Result getFeature(String name) { - return Result.ofOptional(features.entrySet().stream() - .map(e -> e.getValue()) - .filter(f -> f.getName().valueEquals(name)) - .findFirst()); - } - - @Override - public boolean hasFeature(IIdentifier identifier) { - return features.containsKey(identifier); - } - - @Override - public boolean hasFeature(IFeature feature) { - return features.containsKey(feature.getIdentifier()); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.data.Attribute; +import de.featjar.base.data.IAttributable.IMutatableAttributable; +import de.featjar.base.data.IAttribute; +import de.featjar.base.data.Maps; +import de.featjar.base.data.Result; +import de.featjar.base.data.identifier.IIdentifier; +import de.featjar.base.data.identifier.UUIDIdentifier; +import de.featjar.base.tree.Trees; +import de.featjar.feature.model.IFeatureModel.IMutableFeatureModel; +import de.featjar.formula.structure.IFormula; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class FeatureModel implements IMutableFeatureModel, IMutatableAttributable { + + protected final IIdentifier identifier; + + protected final List featureTreeRoots; + protected final LinkedHashMap features; + protected final LinkedHashMap constraints; + + protected final LinkedHashMap, Object> attributeValues; + + public FeatureModel() { + this(UUIDIdentifier.newInstance()); + } + + public FeatureModel(IIdentifier identifier) { + this.identifier = Objects.requireNonNull(identifier); + featureTreeRoots = new ArrayList<>(1); + features = Maps.empty(); + constraints = Maps.empty(); + attributeValues = new LinkedHashMap<>(4); + } + + protected FeatureModel(FeatureModel otherFeatureModel) { + identifier = otherFeatureModel.getNewIdentifier(); + + featureTreeRoots = new ArrayList<>(otherFeatureModel.featureTreeRoots.size()); + otherFeatureModel.featureTreeRoots.stream().forEach(t -> featureTreeRoots.add(Trees.clone(t))); + + features = new LinkedHashMap<>((int) (otherFeatureModel.features.size() * 1.5)); + otherFeatureModel.features.entrySet().stream() + .map(e -> e.getValue().clone(this)) + .forEach(f -> features.put(f.getIdentifier(), f)); + + constraints = new LinkedHashMap<>((int) (otherFeatureModel.constraints.size() * 1.5)); + otherFeatureModel.constraints.entrySet().stream() + .map(e -> e.getValue().clone(this)) + .forEach(c -> constraints.put(c.getIdentifier(), c)); + + attributeValues = otherFeatureModel.cloneAttributes(); + } + + @Override + public FeatureModel clone() { + return new FeatureModel(this); + } + + @Override + public FeatureModel getFeatureModel() { + return this; + } + + @Override + public List getRoots() { + return featureTreeRoots; + } + + @Override + public Collection getFeatures() { + return Collections.unmodifiableCollection(features.values()); + } + + @Override + public Result getFeature(IIdentifier identifier) { + return Result.of(features.get(Objects.requireNonNull(identifier))); + } + + @Override + public Collection getConstraints() { + return Collections.unmodifiableCollection(constraints.values()); + } + + @Override + public Result getConstraint(IIdentifier identifier) { + return Result.of(constraints.get(Objects.requireNonNull(identifier))); + } + + @Override + public boolean hasConstraint(IIdentifier identifier) { + return constraints.containsKey(identifier); + } + + @Override + public boolean hasConstraint(IConstraint constraint) { + return constraints.containsKey(constraint.getIdentifier()); + } + + @Override + public int getNumberOfConstraints() { + return constraints.size(); + } + + @Override + public IIdentifier getIdentifier() { + return identifier; + } + + @Override + public Optional, Object>> getAttributes() { + return Optional.of(Collections.unmodifiableMap(attributeValues)); + } + + @Override + public void setAttributeValue(Attribute attribute, S value) { + if (value == null) { + removeAttributeValue(attribute); + return; + } + checkType(attribute, value); + validate(attribute, value); + attributeValues.put(attribute, value); + } + + @Override + @SuppressWarnings("unchecked") + public S removeAttributeValue(Attribute attribute) { + return (S) attributeValues.remove(attribute); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return getIdentifier().equals(((FeatureModel) o).getIdentifier()); + } + + @Override + public int hashCode() { + return Objects.hash(getIdentifier()); + } + + @Override + public String toString() { + StringBuilder featureString = new StringBuilder(); + for (IFeatureTree root : featureTreeRoots) { + featureString.append(root.print()); + featureString.append('\n'); + } + return String.format( + "FeatureModel{features=%s, constraints=%s}", featureString.toString(), constraints.toString()); + } + + @Override + public void setName(String name) { + attributeValues.put(Attributes.NAME, name); + } + + @Override + public void setDescription(String description) { + attributeValues.put(Attributes.DESCRIPTION, description); + } + + @Override + public IFeatureTree addFeatureTreeRoot(IFeature feature) { + FeatureTree newTree = new FeatureTree(feature); + featureTreeRoots.add(newTree); + return newTree; + } + + @Override + public void addFeatureTreeRoot(IFeatureTree featureTree) { + featureTreeRoots.add(featureTree); + } + + @Override + public void removeFeatureTreeRoot(IFeature feature) { + for (Iterator it = featureTreeRoots.listIterator(); it.hasNext(); ) { + if (it.next().getFeature().equals(feature)) { + it.remove(); + } + } + } + + @Override + public void removeFeatureTreeRoot(IFeatureTree featureTree) { + for (Iterator it = featureTreeRoots.listIterator(); it.hasNext(); ) { + if (it.next() == featureTree) { + it.remove(); + } + } + } + + @Override + public IConstraint addConstraint(IFormula formula) { + IConstraint newConstraint = new Constraint(this, Trees.clone(formula)); + constraints.put(newConstraint.getIdentifier(), newConstraint); + return newConstraint; + } + + @Override + public boolean removeConstraint(IConstraint constraint) { + Objects.requireNonNull(constraint); + return constraints.remove(constraint.getIdentifier()) != null; + } + + @Override + public IFeature addFeature(String name) { + Objects.requireNonNull(name); + Feature feature = new Feature(this); + feature.setName(name); + features.put(feature.getIdentifier(), feature); + return feature; + } + + @Override + public boolean removeFeature(IFeature feature) { + return features.remove(feature.getIdentifier()) != null; + } + + @Override + public int getNumberOfFeatures() { + return features.size(); + } + + @Override + public Result getFeature(String name) { + return Result.ofOptional(features.entrySet().stream() + .map(e -> e.getValue()) + .filter(f -> f.getName().valueEquals(name)) + .findFirst()); + } + + @Override + public boolean hasFeature(IIdentifier identifier) { + return features.containsKey(identifier); + } + + @Override + public boolean hasFeature(IFeature feature) { + return features.containsKey(feature.getIdentifier()); + } +} diff --git a/src/main/java/de/featjar/feature/model/FeatureTree.java b/src/main/java/de/featjar/feature/model/FeatureTree.java index 889c05c2..7ba8dc89 100644 --- a/src/main/java/de/featjar/feature/model/FeatureTree.java +++ b/src/main/java/de/featjar/feature/model/FeatureTree.java @@ -1,288 +1,288 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.data.Attribute; -import de.featjar.base.data.IAttribute; -import de.featjar.base.data.Range; -import de.featjar.base.tree.structure.ARootedTree; -import de.featjar.base.tree.structure.ITree; -import de.featjar.feature.model.IFeatureTree.IMutableFeatureTree; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; - -public class FeatureTree extends ARootedTree implements IMutableFeatureTree { - - public final class Group { - private Range groupCardinality; - - private Group(int lowerBound, int upperBound) { - this.groupCardinality = Range.of(lowerBound, upperBound); - } - - public Group(Range groupRange) { - this.groupCardinality = Range.copy(groupRange); - } - - private Group(Group otherGroup) { - this.groupCardinality = Range.copy(otherGroup.groupCardinality); - } - - private void setBounds(int lowerBound, int upperBound) { - groupCardinality.setBounds(lowerBound, upperBound); - } - - public int getLowerBound() { - return groupCardinality.getLowerBound(); - } - - public int getUpperBound() { - return groupCardinality.getUpperBound(); - } - - public boolean isCardinalityGroup() { - return !isAlternative() && !isOr() && !isAnd(); - } - - public boolean isAlternative() { - return groupCardinality.is(1, 1); - } - - public boolean isOr() { - return groupCardinality.is(1, Range.OPEN); - } - - public boolean isAnd() { - return groupCardinality.is(0, Range.OPEN); - } - - public boolean allowsZero() { - return groupCardinality.getLowerBound() <= 0; - } - - public List getGroupSiblings() { - IFeatureTree parent = getParent().orElse(null); - return (parent == null) - ? List.of() - : parent.getChildren().stream() - .filter(t -> t.getParentGroupID() == parentGroupID) - .collect(Collectors.toList()); - } - - @Override - public int hashCode() { - return System.identityHashCode(this); - } - - @Override - public boolean equals(Object obj) { - return this == obj; - } - - @Override - protected Group clone() { - return new Group(this); - } - - @Override - public String toString() { - return groupCardinality.toString(); - } - } - - protected final IFeature feature; - - protected int parentGroupID; - - protected Range cardinality; - protected ArrayList childrenGroups; - - protected LinkedHashMap, Object> attributeValues; - - protected FeatureTree(IFeature feature) { - this.feature = Objects.requireNonNull(feature); - cardinality = Range.of(0, 1); - childrenGroups = new ArrayList<>(1); - childrenGroups.add(new Group(Range.atLeast(0))); - } - - protected FeatureTree(FeatureTree otherFeatureTree) { - feature = otherFeatureTree.feature; - parentGroupID = otherFeatureTree.parentGroupID; - cardinality = otherFeatureTree.cardinality.clone(); - otherFeatureTree.childrenGroups.stream().map(Group::clone).forEach(childrenGroups::add); - attributeValues = otherFeatureTree.cloneAttributes(); - } - - @Override - public IFeature getFeature() { - return feature; - } - - @Override - public int getParentGroupID() { - return parentGroupID; - } - - @Override - public Optional getParentGroup() { - return parent == null ? Optional.empty() : parent.getChildrenGroup(parentGroupID); - } - - @Override - public List getChildrenGroups() { - return Collections.unmodifiableList(childrenGroups); - } - - @Override - public Optional getChildrenGroup(int groupID) { - return Optional.ofNullable(getChildrenGroups().get(groupID)); - } - - @Override - public List getChildren(int groupID) { - return getChildren().stream() - .filter(c -> c.getParentGroupID() == groupID) - .collect(Collectors.toList()); - } - - @Override - public String toString() { - return feature.getName().orElse(""); - } - - @Override - public Optional, Object>> getAttributes() { - return attributeValues == null ? Optional.empty() : Optional.of(Collections.unmodifiableMap(attributeValues)); - } - - @Override - public List getRoots() { - return List.of(this); - } - - @Override - public int getFeatureCardinalityLowerBound() { - return cardinality.getLowerBound(); - } - - @Override - public int getFeatureCardinalityUpperBound() { - return cardinality.getUpperBound(); - } - - @Override - public ITree cloneNode() { - return new FeatureTree(this); - } - - @Override - public boolean equalsNode(IFeatureTree other) { - if (this == other) return true; - if (other == null || getClass() != other.getClass()) return false; - FeatureTree otherFeatureTree = (FeatureTree) other; - return parentGroupID == otherFeatureTree.parentGroupID - && Objects.equals(feature, otherFeatureTree.feature) - && Objects.equals(childrenGroups, otherFeatureTree.childrenGroups); - } - - @Override - public int hashCodeNode() { - return Objects.hash(feature, parentGroupID, childrenGroups); - } - - @Override - public int addCardinalityGroup(int lowerBound, int upperBound) { - childrenGroups.add(new Group(lowerBound, upperBound)); - return childrenGroups.size() - 1; - } - - @Override - public int addCardinalityGroup(Range groupRange) { - childrenGroups.add(new Group(groupRange)); - return childrenGroups.size() - 1; - } - - public void setParentGroupID(int groupID) { - if (parent == null) throw new IllegalArgumentException("Cannot set groupID for root feature!"); - if (groupID < 0) throw new IllegalArgumentException(String.format("groupID must be positive (%d)", groupID)); - if (groupID >= parent.getChildrenGroups().size()) - throw new IllegalArgumentException( - String.format("groupID must be smaller than number of groups in parent feature (%d)", groupID)); - this.parentGroupID = groupID; - } - - @Override - public void setFeatureCardinality(Range featureCardinality) { - this.cardinality = Range.copy(featureCardinality); - } - - @Override - public void makeMandatory() { - if (cardinality.getUpperBound() == 0) { - cardinality = Range.exactly(1); - } else { - cardinality.setLowerBound(1); - } - } - - @Override - public void makeOptional() { - cardinality.setLowerBound(0); - } - - @Override - public void setAttributeValue(Attribute attribute, S value) { - if (value == null) { - removeAttributeValue(attribute); - return; - } - checkType(attribute, value); - validate(attribute, value); - if (attributeValues == null) { - attributeValues = new LinkedHashMap<>(); - } - attributeValues.put(attribute, value); - } - - @Override - @SuppressWarnings("unchecked") - public S removeAttributeValue(Attribute attribute) { - if (attributeValues == null) { - attributeValues = new LinkedHashMap<>(); - } - return (S) attributeValues.remove(attribute); - } - - @Override - public void toCardinalityGroup(int groupID, int lowerBound, int upperBound) { - Group group = getChildrenGroups().get(groupID); - if (group != null) { - group.setBounds(lowerBound, upperBound); - } - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.data.Attribute; +import de.featjar.base.data.IAttribute; +import de.featjar.base.data.Range; +import de.featjar.base.tree.structure.ARootedTree; +import de.featjar.base.tree.structure.ITree; +import de.featjar.feature.model.IFeatureTree.IMutableFeatureTree; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +public class FeatureTree extends ARootedTree implements IMutableFeatureTree { + + public final class Group { + private Range groupCardinality; + + private Group(int lowerBound, int upperBound) { + this.groupCardinality = Range.of(lowerBound, upperBound); + } + + public Group(Range groupRange) { + this.groupCardinality = Range.copy(groupRange); + } + + private Group(Group otherGroup) { + this.groupCardinality = Range.copy(otherGroup.groupCardinality); + } + + private void setBounds(int lowerBound, int upperBound) { + groupCardinality.setBounds(lowerBound, upperBound); + } + + public int getLowerBound() { + return groupCardinality.getLowerBound(); + } + + public int getUpperBound() { + return groupCardinality.getUpperBound(); + } + + public boolean isCardinalityGroup() { + return !isAlternative() && !isOr() && !isAnd(); + } + + public boolean isAlternative() { + return groupCardinality.is(1, 1); + } + + public boolean isOr() { + return groupCardinality.is(1, Range.OPEN); + } + + public boolean isAnd() { + return groupCardinality.is(0, Range.OPEN); + } + + public boolean allowsZero() { + return groupCardinality.getLowerBound() <= 0; + } + + public List getGroupSiblings() { + IFeatureTree parent = getParent().orElse(null); + return (parent == null) + ? List.of() + : parent.getChildren().stream() + .filter(t -> t.getParentGroupID() == parentGroupID) + .collect(Collectors.toList()); + } + + @Override + public int hashCode() { + return System.identityHashCode(this); + } + + @Override + public boolean equals(Object obj) { + return this == obj; + } + + @Override + protected Group clone() { + return new Group(this); + } + + @Override + public String toString() { + return groupCardinality.toString(); + } + } + + protected final IFeature feature; + + protected int parentGroupID; + + protected Range cardinality; + protected ArrayList childrenGroups; + + protected LinkedHashMap, Object> attributeValues; + + protected FeatureTree(IFeature feature) { + this.feature = Objects.requireNonNull(feature); + cardinality = Range.of(0, 1); + childrenGroups = new ArrayList<>(1); + childrenGroups.add(new Group(Range.atLeast(0))); + } + + protected FeatureTree(FeatureTree otherFeatureTree) { + feature = otherFeatureTree.feature; + parentGroupID = otherFeatureTree.parentGroupID; + cardinality = otherFeatureTree.cardinality.clone(); + otherFeatureTree.childrenGroups.stream().map(Group::clone).forEach(childrenGroups::add); + attributeValues = otherFeatureTree.cloneAttributes(); + } + + @Override + public IFeature getFeature() { + return feature; + } + + @Override + public int getParentGroupID() { + return parentGroupID; + } + + @Override + public Optional getParentGroup() { + return parent == null ? Optional.empty() : parent.getChildrenGroup(parentGroupID); + } + + @Override + public List getChildrenGroups() { + return Collections.unmodifiableList(childrenGroups); + } + + @Override + public Optional getChildrenGroup(int groupID) { + return Optional.ofNullable(getChildrenGroups().get(groupID)); + } + + @Override + public List getChildren(int groupID) { + return getChildren().stream() + .filter(c -> c.getParentGroupID() == groupID) + .collect(Collectors.toList()); + } + + @Override + public String toString() { + return feature.getName().orElse(""); + } + + @Override + public Optional, Object>> getAttributes() { + return attributeValues == null ? Optional.empty() : Optional.of(Collections.unmodifiableMap(attributeValues)); + } + + @Override + public List getRoots() { + return List.of(this); + } + + @Override + public int getFeatureCardinalityLowerBound() { + return cardinality.getLowerBound(); + } + + @Override + public int getFeatureCardinalityUpperBound() { + return cardinality.getUpperBound(); + } + + @Override + public ITree cloneNode() { + return new FeatureTree(this); + } + + @Override + public boolean equalsNode(IFeatureTree other) { + if (this == other) return true; + if (other == null || getClass() != other.getClass()) return false; + FeatureTree otherFeatureTree = (FeatureTree) other; + return parentGroupID == otherFeatureTree.parentGroupID + && Objects.equals(feature, otherFeatureTree.feature) + && Objects.equals(childrenGroups, otherFeatureTree.childrenGroups); + } + + @Override + public int hashCodeNode() { + return Objects.hash(feature, parentGroupID, childrenGroups); + } + + @Override + public int addCardinalityGroup(int lowerBound, int upperBound) { + childrenGroups.add(new Group(lowerBound, upperBound)); + return childrenGroups.size() - 1; + } + + @Override + public int addCardinalityGroup(Range groupRange) { + childrenGroups.add(new Group(groupRange)); + return childrenGroups.size() - 1; + } + + public void setParentGroupID(int groupID) { + if (parent == null) throw new IllegalArgumentException("Cannot set groupID for root feature!"); + if (groupID < 0) throw new IllegalArgumentException(String.format("groupID must be positive (%d)", groupID)); + if (groupID >= parent.getChildrenGroups().size()) + throw new IllegalArgumentException( + String.format("groupID must be smaller than number of groups in parent feature (%d)", groupID)); + this.parentGroupID = groupID; + } + + @Override + public void setFeatureCardinality(Range featureCardinality) { + this.cardinality = Range.copy(featureCardinality); + } + + @Override + public void makeMandatory() { + if (cardinality.getUpperBound() == 0) { + cardinality = Range.exactly(1); + } else { + cardinality.setLowerBound(1); + } + } + + @Override + public void makeOptional() { + cardinality.setLowerBound(0); + } + + @Override + public void setAttributeValue(Attribute attribute, S value) { + if (value == null) { + removeAttributeValue(attribute); + return; + } + checkType(attribute, value); + validate(attribute, value); + if (attributeValues == null) { + attributeValues = new LinkedHashMap<>(); + } + attributeValues.put(attribute, value); + } + + @Override + @SuppressWarnings("unchecked") + public S removeAttributeValue(Attribute attribute) { + if (attributeValues == null) { + attributeValues = new LinkedHashMap<>(); + } + return (S) attributeValues.remove(attribute); + } + + @Override + public void toCardinalityGroup(int groupID, int lowerBound, int upperBound) { + Group group = getChildrenGroups().get(groupID); + if (group != null) { + group.setBounds(lowerBound, upperBound); + } + } +} diff --git a/src/main/java/de/featjar/feature/model/IConstraint.java b/src/main/java/de/featjar/feature/model/IConstraint.java index a398f972..456d4bc6 100644 --- a/src/main/java/de/featjar/feature/model/IConstraint.java +++ b/src/main/java/de/featjar/feature/model/IConstraint.java @@ -1,86 +1,86 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.data.*; -import de.featjar.feature.model.mixins.IHasCommonAttributes; -import de.featjar.formula.structure.IFormula; -import de.featjar.formula.structure.term.value.Variable; -import java.util.LinkedHashSet; - -/** - * A constraint describes some restriction on the valid configurations represented by a {@link FeatureModel}. - * It is attached to a {@link FeatureModel} and represented as a {@link IFormula} over {@link Feature} variables. - * For safe mutation, rely only on the methods of {@link IMutableConstraint}. - * - * @author Elias Kuiter - */ -public interface IConstraint extends IFeatureModelElement, IHasCommonAttributes { - - IConstraint clone(); - - IConstraint clone(IFeatureModel newFeatureModel); - - IFormula getFormula(); - - static LinkedHashSet getReferencedFeatures(IFormula formula, IFeatureModel featureModel) { - return formula.getVariableStream() - .map(Variable::getName) - .map(name -> { - Result feature = featureModel.getFeature(name); - if (feature.isEmpty()) throw new RuntimeException("encountered unknown feature " + name); - return feature.get(); - }) - .collect(Sets.toSet()); - } - - default LinkedHashSet getReferencedFeatures() { - return getReferencedFeatures(getFormula(), getFeatureModel()); - } - - default LinkedHashSet getTags() { - return getAttributeValue(Attributes.TAGS).get(); - } - - default IMutableConstraint mutate() { - return (IMutableConstraint) this; - } - - static interface IMutableConstraint extends IConstraint, IHasMutableCommonAttributes { - void setFormula(IFormula formula); - - default void remove() { - getFeatureModel().mutate().removeConstraint(this); - } - - default void setTags(LinkedHashSet tags) { - setAttributeValue(Attributes.TAGS, tags); - } - - default boolean addTag(String tag) { - return getTags().add(tag); - } - - default boolean removeTag(String tag) { - return getTags().remove(tag); - } - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.data.*; +import de.featjar.feature.model.mixins.IHasCommonAttributes; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.term.value.Variable; +import java.util.LinkedHashSet; + +/** + * A constraint describes some restriction on the valid configurations represented by a {@link FeatureModel}. + * It is attached to a {@link FeatureModel} and represented as a {@link IFormula} over {@link Feature} variables. + * For safe mutation, rely only on the methods of {@link IMutableConstraint}. + * + * @author Elias Kuiter + */ +public interface IConstraint extends IFeatureModelElement, IHasCommonAttributes { + + IConstraint clone(); + + IConstraint clone(IFeatureModel newFeatureModel); + + IFormula getFormula(); + + static LinkedHashSet getReferencedFeatures(IFormula formula, IFeatureModel featureModel) { + return formula.getVariableStream() + .map(Variable::getName) + .map(name -> { + Result feature = featureModel.getFeature(name); + if (feature.isEmpty()) throw new RuntimeException("encountered unknown feature " + name); + return feature.get(); + }) + .collect(Sets.toSet()); + } + + default LinkedHashSet getReferencedFeatures() { + return getReferencedFeatures(getFormula(), getFeatureModel()); + } + + default LinkedHashSet getTags() { + return getAttributeValue(Attributes.TAGS).get(); + } + + default IMutableConstraint mutate() { + return (IMutableConstraint) this; + } + + static interface IMutableConstraint extends IConstraint, IHasMutableCommonAttributes { + void setFormula(IFormula formula); + + default void remove() { + getFeatureModel().mutate().removeConstraint(this); + } + + default void setTags(LinkedHashSet tags) { + setAttributeValue(Attributes.TAGS, tags); + } + + default boolean addTag(String tag) { + return getTags().add(tag); + } + + default boolean removeTag(String tag) { + return getTags().remove(tag); + } + } +} diff --git a/src/main/java/de/featjar/feature/model/IFeature.java b/src/main/java/de/featjar/feature/model/IFeature.java index cabc2a68..735a09a4 100644 --- a/src/main/java/de/featjar/feature/model/IFeature.java +++ b/src/main/java/de/featjar/feature/model/IFeature.java @@ -1,132 +1,132 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.data.Problem; -import de.featjar.base.data.Result; -import de.featjar.base.data.Sets; -import de.featjar.base.data.identifier.AIdentifier; -import de.featjar.feature.model.mixins.IHasCommonAttributes; -import java.util.LinkedHashSet; - -/** - * A feature in a {@link FeatureModel} describes some functionality of a software system. - * It is attached to a {@link FeatureModel} and labels a {@link FeatureTree}. - * For safe mutation, rely only on the methods of {@link IMutableFeature}. - * A {@link Feature} is uniquely determined by its immutable {@link AIdentifier} - * or name (obtained with {@link IHasCommonAttributes#getName()}). - * In contrast to a feature's identifier, its name is mutable and should therefore be used sparsely - * to avoid cache invalidation and renaming issues. - * - * @author Elias Kuiter - */ -public interface IFeature extends IFeatureModelElement, IHasCommonAttributes { - - Result getFeatureTree(); - - Class getType(); - - IFeature clone(); - - IFeature clone(IFeatureModel newFeatureModel); - - default boolean isAbstract() { - return (boolean) getAttributeValue(Attributes.ABSTRACT).get(); - } - - default boolean isConcrete() { - return !isAbstract(); - } - - default boolean isHidden() { - return (boolean) getAttributeValue(Attributes.HIDDEN).get(); - } - - /** - * Checks if there is a hidden feature higher up in the hierarchy. - * Does not only check the direct parent, but all parents above. - * @return {@code true} if any of this features's parents are hidden, {@code false} otherwise. - */ - default boolean hasHiddenParent() { - IFeatureTree parentTreeElement = - getFeatureTree().orElseThrow(Problem::toException).getParent().orElse(null); - - while (parentTreeElement != null) { - if (parentTreeElement.getFeature().isHidden()) { - return true; - } - parentTreeElement = parentTreeElement.getParent().orElse(null); - } - return false; - } - - default boolean isVisible() { - return !isHidden(); - } - - default LinkedHashSet getReferencingConstraints() { - return getFeatureModel().getConstraints().stream() - .filter(constraint -> - constraint.getReferencedFeatures().stream().anyMatch(this::equals)) - .collect(Sets.toSet()); - } - - default IMutableFeature mutate() { - return (IMutableFeature) this; - } - - static interface IMutableFeature extends IFeature, IHasMutableCommonAttributes { - - void setType(Class type); - - default void setAbstract(boolean value) { - setAttributeValue(Attributes.ABSTRACT, value); - } - - default boolean toggleAbstract() { - return toggleAttributeValue(Attributes.ABSTRACT); - } - - default void setAbstract() { - setAbstract(true); - } - - default void setConcrete() { - setAbstract(false); - } - - default void setHidden(boolean value) { - setAttributeValue(Attributes.HIDDEN, value); - } - - default boolean toggleHidden() { - return toggleAttributeValue(Attributes.HIDDEN); - } - - default void setHidden() { - setHidden(true); - } - - default void setVisible() { - setHidden(false); - } - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.data.Problem; +import de.featjar.base.data.Result; +import de.featjar.base.data.Sets; +import de.featjar.base.data.identifier.AIdentifier; +import de.featjar.feature.model.mixins.IHasCommonAttributes; +import java.util.LinkedHashSet; + +/** + * A feature in a {@link FeatureModel} describes some functionality of a software system. + * It is attached to a {@link FeatureModel} and labels a {@link FeatureTree}. + * For safe mutation, rely only on the methods of {@link IMutableFeature}. + * A {@link Feature} is uniquely determined by its immutable {@link AIdentifier} + * or name (obtained with {@link IHasCommonAttributes#getName()}). + * In contrast to a feature's identifier, its name is mutable and should therefore be used sparsely + * to avoid cache invalidation and renaming issues. + * + * @author Elias Kuiter + */ +public interface IFeature extends IFeatureModelElement, IHasCommonAttributes { + + Result getFeatureTree(); + + Class getType(); + + IFeature clone(); + + IFeature clone(IFeatureModel newFeatureModel); + + default boolean isAbstract() { + return (boolean) getAttributeValue(Attributes.ABSTRACT).get(); + } + + default boolean isConcrete() { + return !isAbstract(); + } + + default boolean isHidden() { + return (boolean) getAttributeValue(Attributes.HIDDEN).get(); + } + + /** + * Checks if there is a hidden feature higher up in the hierarchy. + * Does not only check the direct parent, but all parents above. + * @return {@code true} if any of this features's parents are hidden, {@code false} otherwise. + */ + default boolean hasHiddenParent() { + IFeatureTree parentTreeElement = + getFeatureTree().orElseThrow(Problem::toException).getParent().orElse(null); + + while (parentTreeElement != null) { + if (parentTreeElement.getFeature().isHidden()) { + return true; + } + parentTreeElement = parentTreeElement.getParent().orElse(null); + } + return false; + } + + default boolean isVisible() { + return !isHidden(); + } + + default LinkedHashSet getReferencingConstraints() { + return getFeatureModel().getConstraints().stream() + .filter(constraint -> + constraint.getReferencedFeatures().stream().anyMatch(this::equals)) + .collect(Sets.toSet()); + } + + default IMutableFeature mutate() { + return (IMutableFeature) this; + } + + static interface IMutableFeature extends IFeature, IHasMutableCommonAttributes { + + void setType(Class type); + + default void setAbstract(boolean value) { + setAttributeValue(Attributes.ABSTRACT, value); + } + + default boolean toggleAbstract() { + return toggleAttributeValue(Attributes.ABSTRACT); + } + + default void setAbstract() { + setAbstract(true); + } + + default void setConcrete() { + setAbstract(false); + } + + default void setHidden(boolean value) { + setAttributeValue(Attributes.HIDDEN, value); + } + + default boolean toggleHidden() { + return toggleAttributeValue(Attributes.HIDDEN); + } + + default void setHidden() { + setHidden(true); + } + + default void setVisible() { + setHidden(false); + } + } +} diff --git a/src/main/java/de/featjar/feature/model/IFeatureModel.java b/src/main/java/de/featjar/feature/model/IFeatureModel.java index 1c5b80a4..356777d1 100644 --- a/src/main/java/de/featjar/feature/model/IFeatureModel.java +++ b/src/main/java/de/featjar/feature/model/IFeatureModel.java @@ -1,90 +1,90 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.data.Result; -import de.featjar.base.data.identifier.IIdentifier; -import de.featjar.feature.model.mixins.IHasCommonAttributes; -import de.featjar.feature.model.mixins.IHasConstraints; -import de.featjar.feature.model.mixins.IHasFeatureTree; -import de.featjar.formula.structure.IFormula; -import java.util.Collection; - -/** - * A feature model represents the configuration space of a software system. - * We equate feature models with feature diagrams - * (i.e., a {@link FeatureTree} labeled with features and a list of {@link Constraint constraints}). - * For safe mutation, rely only on the methods of {@link IMutableFeatureModel}. - * - * cache assumes that features/constraints are only added/deleted through the mutator, not manually - * - * @author Elias Kuiter - */ -public interface IFeatureModel extends IFeatureModelElement, IHasCommonAttributes, IHasFeatureTree, IHasConstraints { - // TODO put flattened fm into store (maybe dispatch mutators of flattened model to original models) - - // TODO: we allow all kinds of modeling constructs, but not all analyses/computations support all constructs. - // e.g., multiplicities are difficult to map to SAT. somehow, this should be checked. - // maybe store required/incompatible capabilities for computations? eg., incompatible with - // Plaisted-Greenbaum/multiplicities/...? - // and then implement different alternative algorithms with different capabilities. - // maybe this could be encoded first-class as a feature model. - // this could even be used to generate query plans (e.g., find some configuration that counts my formula). - // every plugin defines a feature model (uvl) that restricts what its extensions can and cannot do (replacing - // extensions.xml) - - IFeatureModel clone(); - - Collection getFeatures(); - - int getNumberOfFeatures(); - - Result getFeature(IIdentifier identifier); - - Result getFeature(String name); - - boolean hasFeature(IIdentifier identifier); - - boolean hasFeature(IFeature feature); - - default IMutableFeatureModel mutate() { - return (IMutableFeatureModel) this; - } - - static interface IMutableFeatureModel extends IFeatureModel, IHasMutableCommonAttributes { - - IFeature addFeature(String name); - - boolean removeFeature(IFeature feature); - - IConstraint addConstraint(IFormula formula); - - boolean removeConstraint(IConstraint constraint); - - IFeatureTree addFeatureTreeRoot(IFeature feature); - - void addFeatureTreeRoot(IFeatureTree featureTree); - - void removeFeatureTreeRoot(IFeatureTree featureTree); - - void removeFeatureTreeRoot(IFeature feature); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.data.Result; +import de.featjar.base.data.identifier.IIdentifier; +import de.featjar.feature.model.mixins.IHasCommonAttributes; +import de.featjar.feature.model.mixins.IHasConstraints; +import de.featjar.feature.model.mixins.IHasFeatureTree; +import de.featjar.formula.structure.IFormula; +import java.util.Collection; + +/** + * A feature model represents the configuration space of a software system. + * We equate feature models with feature diagrams + * (i.e., a {@link FeatureTree} labeled with features and a list of {@link Constraint constraints}). + * For safe mutation, rely only on the methods of {@link IMutableFeatureModel}. + * + * cache assumes that features/constraints are only added/deleted through the mutator, not manually + * + * @author Elias Kuiter + */ +public interface IFeatureModel extends IFeatureModelElement, IHasCommonAttributes, IHasFeatureTree, IHasConstraints { + // TODO put flattened fm into store (maybe dispatch mutators of flattened model to original models) + + // TODO: we allow all kinds of modeling constructs, but not all analyses/computations support all constructs. + // e.g., multiplicities are difficult to map to SAT. somehow, this should be checked. + // maybe store required/incompatible capabilities for computations? eg., incompatible with + // Plaisted-Greenbaum/multiplicities/...? + // and then implement different alternative algorithms with different capabilities. + // maybe this could be encoded first-class as a feature model. + // this could even be used to generate query plans (e.g., find some configuration that counts my formula). + // every plugin defines a feature model (uvl) that restricts what its extensions can and cannot do (replacing + // extensions.xml) + + IFeatureModel clone(); + + Collection getFeatures(); + + int getNumberOfFeatures(); + + Result getFeature(IIdentifier identifier); + + Result getFeature(String name); + + boolean hasFeature(IIdentifier identifier); + + boolean hasFeature(IFeature feature); + + default IMutableFeatureModel mutate() { + return (IMutableFeatureModel) this; + } + + static interface IMutableFeatureModel extends IFeatureModel, IHasMutableCommonAttributes { + + IFeature addFeature(String name); + + boolean removeFeature(IFeature feature); + + IConstraint addConstraint(IFormula formula); + + boolean removeConstraint(IConstraint constraint); + + IFeatureTree addFeatureTreeRoot(IFeature feature); + + void addFeatureTreeRoot(IFeatureTree featureTree); + + void removeFeatureTreeRoot(IFeatureTree featureTree); + + void removeFeatureTreeRoot(IFeature feature); + } +} diff --git a/src/main/java/de/featjar/feature/model/IFeatureModelElement.java b/src/main/java/de/featjar/feature/model/IFeatureModelElement.java index 68ca8bd6..2fa60399 100644 --- a/src/main/java/de/featjar/feature/model/IFeatureModelElement.java +++ b/src/main/java/de/featjar/feature/model/IFeatureModelElement.java @@ -1,28 +1,28 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.data.IAttributable; -import de.featjar.base.data.identifier.IIdentifiable; - -public interface IFeatureModelElement extends IIdentifiable, IAttributable, Cloneable { - IFeatureModel getFeatureModel(); -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.data.IAttributable; +import de.featjar.base.data.identifier.IIdentifiable; + +public interface IFeatureModelElement extends IIdentifiable, IAttributable, Cloneable { + IFeatureModel getFeatureModel(); +} diff --git a/src/main/java/de/featjar/feature/model/IFeatureTree.java b/src/main/java/de/featjar/feature/model/IFeatureTree.java index e1c1da0e..803d0c54 100644 --- a/src/main/java/de/featjar/feature/model/IFeatureTree.java +++ b/src/main/java/de/featjar/feature/model/IFeatureTree.java @@ -1,255 +1,255 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.data.IAttributable; -import de.featjar.base.data.Range; -import de.featjar.base.data.Result; -import de.featjar.base.tree.structure.ARootedTree; -import de.featjar.base.tree.structure.IRootedTree; -import de.featjar.feature.model.FeatureTree.Group; -import de.featjar.feature.model.mixins.IHasFeatureTree; -import java.util.List; -import java.util.Optional; - -/** - * An ordered {@link ARootedTree} labeled with {@link Feature features}. - * Implements some concepts from feature-oriented domain analysis, such as - * mandatory/optional features and groups. - * - * @author Elias Kuiter - * @author Sebastian Krieter - */ -public interface IFeatureTree extends IRootedTree, IAttributable, IHasFeatureTree { - - IFeature getFeature(); - - /** - * {@return the groups of this feature's children.} - * This list may contain {@code null}. - */ - List getChildrenGroups(); - - /** - * {@return the group of this feature's children with the given id.} - * @param groupID the groupID - */ - Optional getChildrenGroup(int groupID); - - /** - * {@return all children within the group with the given id.} - * @param groupID the groupID - */ - List getChildren(int groupID); - - /** - * {@return the group of this feature. (The group of this feature's parent, in which this feature is contained.)} - * @see #getParentGroupID() - */ - Optional getParentGroup(); - - /** - * {@return the id of the group of this feature.} - * @see #getParentGroup() - */ - int getParentGroupID(); - - int getFeatureCardinalityLowerBound(); - - int getFeatureCardinalityUpperBound(); - - default boolean isMandatory() { - return getFeatureCardinalityLowerBound() > 0; - } - - default boolean isOptional() { - return getFeatureCardinalityLowerBound() <= 0; - } - - default IMutableFeatureTree mutate() { - return (IMutableFeatureTree) this; - } - - static interface IMutableFeatureTree extends IFeatureTree, IMutatableAttributable { - - default IFeatureTree addFeatureBelow(IFeature newFeature) { - return addFeatureBelow(newFeature, getChildrenCount(), 0); - } - - default IFeatureTree addFeatureBelow(IFeature newFeature, int index) { - return addFeatureBelow(newFeature, index, 0); - } - - default IFeatureTree addFeatureBelow(IFeature newFeature, int index, int groupID) { - FeatureTree newTree = new FeatureTree(newFeature); - addChild(index, newTree); - newTree.setParentGroupID(groupID); - return newTree; - } - - default IFeatureTree addFeatureAbove(IFeature newFeature) { - FeatureTree newTree = new FeatureTree(newFeature); - Result parent = getParent(); - if (parent.isPresent()) { - parent.get().replaceChild(this, newTree); - } - newTree.addChild(this); - setParentGroupID(0); - return newTree; - } - - default void removeFromTree() { // TODO what about the containing constraints? - Result parent = getParent(); - if (parent.isPresent()) { - int childIndex = parent.get().getChildIndex(this).orElseThrow(); - parent.get().removeChild(this); - // TODO improve group handling, probably needs slicing - for (Group group : getChildrenGroups()) { - parent.get().mutate().addCardinalityGroup(group.getLowerBound(), group.getUpperBound()); - } - for (IFeatureTree child : getChildren()) { - parent.get().mutate().addChild(childIndex++, child); - } - } - } - - void setParentGroupID(int groupID); - - void setFeatureCardinality(Range featureRange); - - void makeMandatory(); - - void makeOptional(); - - int addCardinalityGroup(int lowerBound, int upperBound); - - default int addCardinalityGroup(Range groupRange) { - return addCardinalityGroup(groupRange.getLowerBound(), groupRange.getUpperBound()); - } - - default int addAndGroup() { - return addCardinalityGroup(0, Range.OPEN); - } - - default int addAlternativeGroup() { - return addCardinalityGroup(1, 1); - } - - default int addOrGroup() { - return addCardinalityGroup(1, Range.OPEN); - } - - /** - * Changes the cardinality of the children group with the given id. - * - * @param groupID the id of the group to change - * @param lowerBound the new lower bound - * @param upperBound the new upper bound - */ - void toCardinalityGroup(int groupID, int lowerBound, int upperBound); - - /** - * Changes the cardinality of the children group with the given id. - * - * @param groupID the id of the group to change - * @param groupCardinality the new cardinality - */ - default void toCardinalityGroup(int groupID, Range groupCardinality) { - toCardinalityGroup(groupID, groupCardinality.getLowerBound(), groupCardinality.getUpperBound()); - } - - /** - * Change children group to and group. - * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(groupID, 0, Range.OPEN)}. - * - * @param groupID the id of the group to change - */ - default void toAndGroup(int groupID) { - toCardinalityGroup(groupID, 0, Range.OPEN); - } - - /** - * Change children group to or group. - * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(groupID, 1, Range.OPEN)}. - * - * @param groupID the id of the group to change - */ - default void toOrGroup(int groupID) { - toCardinalityGroup(groupID, 1, Range.OPEN); - } - - /** - * Change children group to alternative group. - * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(groupID, 1, 1)}. - * - * @param groupID the id of the group to change - */ - default void toAlternativeGroup(int groupID) { - toCardinalityGroup(groupID, 1, 1); - } - - /** - * Changes the cardinality of the first children group. - * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(0, groupCardinality)}. - * - * @param groupCardinality the new cardinality - */ - default void toCardinalityGroup(Range groupCardinality) { - toCardinalityGroup(0, groupCardinality); - } - - /** - * Change first children group to cardinality group. - * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(0, lowerBound, upperBound)}. - * - * @param lowerBound the new lower bound - * @param upperBound the new upper bound - */ - default void toCardinalityGroup(int lowerBound, int upperBound) { - toCardinalityGroup(0, lowerBound, upperBound); - } - - /** - * Change first children group to and group. - * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(0, 0, Range.OPEN)}. - * - */ - default void toAndGroup() { - toAndGroup(0); - } - - /** - * Change first children group to alternative group. - * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(0, 1, Range.OPEN)}. - */ - default void toAlternativeGroup() { - toAlternativeGroup(0); - } - - /** - * Change first children group to or group. - * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(0, 1, 1)}. - */ - default void toOrGroup() { - toOrGroup(0); - } - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.data.IAttributable; +import de.featjar.base.data.Range; +import de.featjar.base.data.Result; +import de.featjar.base.tree.structure.ARootedTree; +import de.featjar.base.tree.structure.IRootedTree; +import de.featjar.feature.model.FeatureTree.Group; +import de.featjar.feature.model.mixins.IHasFeatureTree; +import java.util.List; +import java.util.Optional; + +/** + * An ordered {@link ARootedTree} labeled with {@link Feature features}. + * Implements some concepts from feature-oriented domain analysis, such as + * mandatory/optional features and groups. + * + * @author Elias Kuiter + * @author Sebastian Krieter + */ +public interface IFeatureTree extends IRootedTree, IAttributable, IHasFeatureTree { + + IFeature getFeature(); + + /** + * {@return the groups of this feature's children.} + * This list may contain {@code null}. + */ + List getChildrenGroups(); + + /** + * {@return the group of this feature's children with the given id.} + * @param groupID the groupID + */ + Optional getChildrenGroup(int groupID); + + /** + * {@return all children within the group with the given id.} + * @param groupID the groupID + */ + List getChildren(int groupID); + + /** + * {@return the group of this feature. (The group of this feature's parent, in which this feature is contained.)} + * @see #getParentGroupID() + */ + Optional getParentGroup(); + + /** + * {@return the id of the group of this feature.} + * @see #getParentGroup() + */ + int getParentGroupID(); + + int getFeatureCardinalityLowerBound(); + + int getFeatureCardinalityUpperBound(); + + default boolean isMandatory() { + return getFeatureCardinalityLowerBound() > 0; + } + + default boolean isOptional() { + return getFeatureCardinalityLowerBound() <= 0; + } + + default IMutableFeatureTree mutate() { + return (IMutableFeatureTree) this; + } + + static interface IMutableFeatureTree extends IFeatureTree, IMutatableAttributable { + + default IFeatureTree addFeatureBelow(IFeature newFeature) { + return addFeatureBelow(newFeature, getChildrenCount(), 0); + } + + default IFeatureTree addFeatureBelow(IFeature newFeature, int index) { + return addFeatureBelow(newFeature, index, 0); + } + + default IFeatureTree addFeatureBelow(IFeature newFeature, int index, int groupID) { + FeatureTree newTree = new FeatureTree(newFeature); + addChild(index, newTree); + newTree.setParentGroupID(groupID); + return newTree; + } + + default IFeatureTree addFeatureAbove(IFeature newFeature) { + FeatureTree newTree = new FeatureTree(newFeature); + Result parent = getParent(); + if (parent.isPresent()) { + parent.get().replaceChild(this, newTree); + } + newTree.addChild(this); + setParentGroupID(0); + return newTree; + } + + default void removeFromTree() { // TODO what about the containing constraints? + Result parent = getParent(); + if (parent.isPresent()) { + int childIndex = parent.get().getChildIndex(this).orElseThrow(); + parent.get().removeChild(this); + // TODO improve group handling, probably needs slicing + for (Group group : getChildrenGroups()) { + parent.get().mutate().addCardinalityGroup(group.getLowerBound(), group.getUpperBound()); + } + for (IFeatureTree child : getChildren()) { + parent.get().mutate().addChild(childIndex++, child); + } + } + } + + void setParentGroupID(int groupID); + + void setFeatureCardinality(Range featureRange); + + void makeMandatory(); + + void makeOptional(); + + int addCardinalityGroup(int lowerBound, int upperBound); + + default int addCardinalityGroup(Range groupRange) { + return addCardinalityGroup(groupRange.getLowerBound(), groupRange.getUpperBound()); + } + + default int addAndGroup() { + return addCardinalityGroup(0, Range.OPEN); + } + + default int addAlternativeGroup() { + return addCardinalityGroup(1, 1); + } + + default int addOrGroup() { + return addCardinalityGroup(1, Range.OPEN); + } + + /** + * Changes the cardinality of the children group with the given id. + * + * @param groupID the id of the group to change + * @param lowerBound the new lower bound + * @param upperBound the new upper bound + */ + void toCardinalityGroup(int groupID, int lowerBound, int upperBound); + + /** + * Changes the cardinality of the children group with the given id. + * + * @param groupID the id of the group to change + * @param groupCardinality the new cardinality + */ + default void toCardinalityGroup(int groupID, Range groupCardinality) { + toCardinalityGroup(groupID, groupCardinality.getLowerBound(), groupCardinality.getUpperBound()); + } + + /** + * Change children group to and group. + * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(groupID, 0, Range.OPEN)}. + * + * @param groupID the id of the group to change + */ + default void toAndGroup(int groupID) { + toCardinalityGroup(groupID, 0, Range.OPEN); + } + + /** + * Change children group to or group. + * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(groupID, 1, Range.OPEN)}. + * + * @param groupID the id of the group to change + */ + default void toOrGroup(int groupID) { + toCardinalityGroup(groupID, 1, Range.OPEN); + } + + /** + * Change children group to alternative group. + * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(groupID, 1, 1)}. + * + * @param groupID the id of the group to change + */ + default void toAlternativeGroup(int groupID) { + toCardinalityGroup(groupID, 1, 1); + } + + /** + * Changes the cardinality of the first children group. + * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(0, groupCardinality)}. + * + * @param groupCardinality the new cardinality + */ + default void toCardinalityGroup(Range groupCardinality) { + toCardinalityGroup(0, groupCardinality); + } + + /** + * Change first children group to cardinality group. + * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(0, lowerBound, upperBound)}. + * + * @param lowerBound the new lower bound + * @param upperBound the new upper bound + */ + default void toCardinalityGroup(int lowerBound, int upperBound) { + toCardinalityGroup(0, lowerBound, upperBound); + } + + /** + * Change first children group to and group. + * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(0, 0, Range.OPEN)}. + * + */ + default void toAndGroup() { + toAndGroup(0); + } + + /** + * Change first children group to alternative group. + * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(0, 1, Range.OPEN)}. + */ + default void toAlternativeGroup() { + toAlternativeGroup(0); + } + + /** + * Change first children group to or group. + * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(0, 1, 1)}. + */ + default void toOrGroup() { + toOrGroup(0); + } + } +} diff --git a/src/main/java/de/featjar/feature/model/io/AttributeIO.java b/src/main/java/de/featjar/feature/model/io/AttributeIO.java index 17305854..3a78c046 100644 --- a/src/main/java/de/featjar/feature/model/io/AttributeIO.java +++ b/src/main/java/de/featjar/feature/model/io/AttributeIO.java @@ -1,118 +1,118 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.io; - -import de.featjar.base.data.*; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -/** - * Helpers for parsing and writing attributes and attribute values. - * - * @author Elias Kuiter - */ -public class AttributeIO { - public static Result> getType(String typeString) { - switch (typeString.toLowerCase(Locale.ENGLISH)) { - case "string": - return Result.of(String.class); - case "bool": - case "boolean": - return Result.of(Boolean.class); - case "int": - case "integer": - return Result.of(Integer.class); - case "long": - return Result.of(Long.class); - case "float": - return Result.of(Float.class); - case "double": - return Result.of(Double.class); - default: - return Result.empty(); - } - } - - public static Result getTypeString(Class type) { - if (String.class.equals(type)) { - return Result.of("string"); - } else if (Boolean.class.equals(type)) { - return Result.of("boolean"); - } else if (Integer.class.equals(type)) { - return Result.of("integer"); - } else if (Long.class.equals(type)) { - return Result.of("long"); - } else if (Float.class.equals(type)) { - return Result.of("float"); - } else if (Double.class.equals(type)) { - return Result.of("double"); - } - return Result.empty(); - } - - public static Result> parseAttribute(String name, String typeString) { - return getType(typeString).map(type -> new Attribute<>(name, type)); - } - - public static Result> parseAttribute(String namespace, String name, String typeString) { - return getType(typeString).map(type -> new Attribute<>(namespace, name, type)); - } - - public static Result parseAttributeValue(Class type, String valueString) { - if (String.class.equals(type)) { - return Result.of(valueString); - } else if (Boolean.class.equals(type)) { - return Result.of(Boolean.valueOf(valueString)); - } else if (Integer.class.equals(type)) { - return Result.of(Integer.valueOf(valueString)); - } else if (Long.class.equals(type)) { - return Result.of(Long.valueOf(valueString)); - } else if (Float.class.equals(type)) { - return Result.of(Float.valueOf(valueString)); - } else if (Double.class.equals(type)) { - return Result.of(Double.valueOf(valueString)); - } - return Result.empty(); - } - - public static Result parseAttributeValue(String typeString, String valueString) { - return getType(typeString).flatMap(type -> parseAttributeValue(type, valueString)); - } - - @SuppressWarnings("unchecked") - public static List parseAndSetAttributeValue( - IAttributable attributable, String namespace, String name, String typeString, String valueString) { - List problems = new ArrayList<>(); - Result> attribute = AttributeIO.parseAttribute(namespace, name, typeString); - Result value = parseAttributeValue(typeString, valueString); - if (attribute.isEmpty()) { - problems.add(new Problem("invalid type for attribute " + name, Problem.Severity.WARNING)); - } else if (value.isEmpty()) { - problems.add(new Problem("invalid value for attribute " + name, Problem.Severity.WARNING)); - } else if (attributable.hasAttributeValue(attribute.get())) { - problems.add(new Problem("already has value for attribute " + name, Problem.Severity.WARNING)); - } else { - attributable.mutate().setAttributeValue((Attribute) attribute.get(), value.get()); - } - return problems; - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.io; + +import de.featjar.base.data.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Helpers for parsing and writing attributes and attribute values. + * + * @author Elias Kuiter + */ +public class AttributeIO { + public static Result> getType(String typeString) { + switch (typeString.toLowerCase(Locale.ENGLISH)) { + case "string": + return Result.of(String.class); + case "bool": + case "boolean": + return Result.of(Boolean.class); + case "int": + case "integer": + return Result.of(Integer.class); + case "long": + return Result.of(Long.class); + case "float": + return Result.of(Float.class); + case "double": + return Result.of(Double.class); + default: + return Result.empty(); + } + } + + public static Result getTypeString(Class type) { + if (String.class.equals(type)) { + return Result.of("string"); + } else if (Boolean.class.equals(type)) { + return Result.of("boolean"); + } else if (Integer.class.equals(type)) { + return Result.of("integer"); + } else if (Long.class.equals(type)) { + return Result.of("long"); + } else if (Float.class.equals(type)) { + return Result.of("float"); + } else if (Double.class.equals(type)) { + return Result.of("double"); + } + return Result.empty(); + } + + public static Result> parseAttribute(String name, String typeString) { + return getType(typeString).map(type -> new Attribute<>(name, type)); + } + + public static Result> parseAttribute(String namespace, String name, String typeString) { + return getType(typeString).map(type -> new Attribute<>(namespace, name, type)); + } + + public static Result parseAttributeValue(Class type, String valueString) { + if (String.class.equals(type)) { + return Result.of(valueString); + } else if (Boolean.class.equals(type)) { + return Result.of(Boolean.valueOf(valueString)); + } else if (Integer.class.equals(type)) { + return Result.of(Integer.valueOf(valueString)); + } else if (Long.class.equals(type)) { + return Result.of(Long.valueOf(valueString)); + } else if (Float.class.equals(type)) { + return Result.of(Float.valueOf(valueString)); + } else if (Double.class.equals(type)) { + return Result.of(Double.valueOf(valueString)); + } + return Result.empty(); + } + + public static Result parseAttributeValue(String typeString, String valueString) { + return getType(typeString).flatMap(type -> parseAttributeValue(type, valueString)); + } + + @SuppressWarnings("unchecked") + public static List parseAndSetAttributeValue( + IAttributable attributable, String namespace, String name, String typeString, String valueString) { + List problems = new ArrayList<>(); + Result> attribute = AttributeIO.parseAttribute(namespace, name, typeString); + Result value = parseAttributeValue(typeString, valueString); + if (attribute.isEmpty()) { + problems.add(new Problem("invalid type for attribute " + name, Problem.Severity.WARNING)); + } else if (value.isEmpty()) { + problems.add(new Problem("invalid value for attribute " + name, Problem.Severity.WARNING)); + } else if (attributable.hasAttributeValue(attribute.get())) { + problems.add(new Problem("already has value for attribute " + name, Problem.Severity.WARNING)); + } else { + attributable.mutate().setAttributeValue((Attribute) attribute.get(), value.get()); + } + return problems; + } +} diff --git a/src/main/java/de/featjar/feature/model/io/FeatureModelFormats.java b/src/main/java/de/featjar/feature/model/io/FeatureModelFormats.java index 90f651d5..57088528 100644 --- a/src/main/java/de/featjar/feature/model/io/FeatureModelFormats.java +++ b/src/main/java/de/featjar/feature/model/io/FeatureModelFormats.java @@ -1,37 +1,37 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.io; - -import de.featjar.base.FeatJAR; -import de.featjar.base.io.format.AFormats; -import de.featjar.feature.model.IFeatureModel; - -/** - * Manages all formats for {@link IFeatureModel feature models}. - * - * @author Sebastian Krieter - */ -public class FeatureModelFormats extends AFormats { - - public static FeatureModelFormats getInstance() { - return FeatJAR.extensionPoint(FeatureModelFormats.class); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.io; + +import de.featjar.base.FeatJAR; +import de.featjar.base.io.format.AFormats; +import de.featjar.feature.model.IFeatureModel; + +/** + * Manages all formats for {@link IFeatureModel feature models}. + * + * @author Sebastian Krieter + */ +public class FeatureModelFormats extends AFormats { + + public static FeatureModelFormats getInstance() { + return FeatJAR.extensionPoint(FeatureModelFormats.class); + } +} diff --git a/src/main/java/de/featjar/feature/model/io/xml/GraphVizFeatureModelFormat.java b/src/main/java/de/featjar/feature/model/io/xml/GraphVizFeatureModelFormat.java index 8675aa1c..8314748a 100644 --- a/src/main/java/de/featjar/feature/model/io/xml/GraphVizFeatureModelFormat.java +++ b/src/main/java/de/featjar/feature/model/io/xml/GraphVizFeatureModelFormat.java @@ -1,145 +1,145 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.io.xml; - -import de.featjar.base.data.Result; -import de.featjar.base.io.format.IFormat; -import de.featjar.feature.model.IFeatureModel; -import de.featjar.feature.model.IFeatureTree; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -/** - * Writes feature models to GraphViz DOT files. - * - * @author Elias Kuiter - */ -public class GraphVizFeatureModelFormat implements IFormat { - @Override - public String getFileExtension() { - return "dot"; - } - - @Override - public String getName() { - return "GraphViz"; - } - - @Override - public boolean supportsWrite() { - return true; - } - - @Override - public Result serialize(IFeatureModel featureModel) { - // TODO take multiple roots into account - List features = featureModel.getFeatureTreeStream().collect(Collectors.toList()); - return Result.of(String.format( - "digraph {%n graph%s;%n node%s;%n edge%s;%n%s%n%s%n}", - options(option("splines", "false"), option("ranksep", "0.2")), - options( - option("fontname", "Arial"), - option("style", "filled"), - option("fillcolor", "#ccccff"), - option("shape", "box")), - options(option("arrowhead", "none")), - features.stream().map(this::getNode).collect(Collectors.joining("\n")), - features.stream().map(this::getEdge).filter(s -> !s.isEmpty()).collect(Collectors.joining("\n")))); - } - - public String getNode(IFeatureTree feature) { - String nodeString = ""; - nodeString += String.format( - " %s%s;", - quote(feature.getFeature().getIdentifier().toString()), - options( - option("label", feature.getFeature().getName().orElse("")), - option("fillcolor", feature.getFeature().isAbstract() ? "#f2f2ff" : null))); - if (feature.hasParent()) { - nodeString += String.format( - "%n %s%s;", - quote(feature.getFeature().getIdentifier().toString() + "_group"), - options( - option("shape", "diamond"), - option( - "style", - !feature.getParentGroup().get().isAnd() - ? "invis" - : feature.getParentGroup().get().isAlternative() ? "" : null), - option("fillcolor", feature.getParentGroup().get().isOr() ? "#000000" : null), - option("label", ""), - option("width", ".15"), - option("height", ".15"))); - } - return nodeString; - } - - public String getEdge(IFeatureTree feature) { - String edgeString = ""; - if (feature.hasParent()) { - String parentNode = - feature.getParent().get().getFeature().getIdentifier().toString(); - edgeString += getEdge( - parentNode + "_group", - feature, - option("style", feature.getParentGroup().get().isAnd() ? null : "invis")); - if (!feature.getParentGroup().get().isAnd()) - edgeString += - getEdge(parentNode + (feature.getParentGroup().get().isAnd() ? "_group" : ""), feature, ""); - edgeString += String.format( - " %s:s -> %s:n%s;", - quote(feature.getFeature().getIdentifier().toString()), - quote(feature.getFeature().getIdentifier().toString() + "_group"), - options(option("style", feature.getParentGroup().get().isAnd() ? null : "invis"))); - } - return edgeString; - } - - public String getEdge(String parentNode, IFeatureTree childFeature, String option) { - return String.format( - " %s:s -> %s:n%s;%n", - quote(parentNode), - quote(childFeature.getFeature().getIdentifier().toString()), - options( - option( - "arrowhead", - childFeature.getParentGroup().get().isAnd() - ? null - : childFeature.isMandatory() ? "dot" : "odot"), - option)); - } - - protected String quote(String str) { - return String.format("\"%s\"", str.replace("\"", "\\\"")); - } - - protected String options(String... options) { - List optionsList = - Arrays.stream(options).filter(o -> !o.isEmpty()).collect(Collectors.toList()); - if (String.join("", optionsList).trim().isEmpty()) return ""; - return String.format(" [%s]", String.join(" ", optionsList)); - } - - protected String option(String name, String value) { - return value != null ? String.format("%s=%s", name, quote(value)) : ""; - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.io.xml; + +import de.featjar.base.data.Result; +import de.featjar.base.io.format.IFormat; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.IFeatureTree; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Writes feature models to GraphViz DOT files. + * + * @author Elias Kuiter + */ +public class GraphVizFeatureModelFormat implements IFormat { + @Override + public String getFileExtension() { + return "dot"; + } + + @Override + public String getName() { + return "GraphViz"; + } + + @Override + public boolean supportsWrite() { + return true; + } + + @Override + public Result serialize(IFeatureModel featureModel) { + // TODO take multiple roots into account + List features = featureModel.getFeatureTreeStream().collect(Collectors.toList()); + return Result.of(String.format( + "digraph {%n graph%s;%n node%s;%n edge%s;%n%s%n%s%n}", + options(option("splines", "false"), option("ranksep", "0.2")), + options( + option("fontname", "Arial"), + option("style", "filled"), + option("fillcolor", "#ccccff"), + option("shape", "box")), + options(option("arrowhead", "none")), + features.stream().map(this::getNode).collect(Collectors.joining("\n")), + features.stream().map(this::getEdge).filter(s -> !s.isEmpty()).collect(Collectors.joining("\n")))); + } + + public String getNode(IFeatureTree feature) { + String nodeString = ""; + nodeString += String.format( + " %s%s;", + quote(feature.getFeature().getIdentifier().toString()), + options( + option("label", feature.getFeature().getName().orElse("")), + option("fillcolor", feature.getFeature().isAbstract() ? "#f2f2ff" : null))); + if (feature.hasParent()) { + nodeString += String.format( + "%n %s%s;", + quote(feature.getFeature().getIdentifier().toString() + "_group"), + options( + option("shape", "diamond"), + option( + "style", + !feature.getParentGroup().get().isAnd() + ? "invis" + : feature.getParentGroup().get().isAlternative() ? "" : null), + option("fillcolor", feature.getParentGroup().get().isOr() ? "#000000" : null), + option("label", ""), + option("width", ".15"), + option("height", ".15"))); + } + return nodeString; + } + + public String getEdge(IFeatureTree feature) { + String edgeString = ""; + if (feature.hasParent()) { + String parentNode = + feature.getParent().get().getFeature().getIdentifier().toString(); + edgeString += getEdge( + parentNode + "_group", + feature, + option("style", feature.getParentGroup().get().isAnd() ? null : "invis")); + if (!feature.getParentGroup().get().isAnd()) + edgeString += + getEdge(parentNode + (feature.getParentGroup().get().isAnd() ? "_group" : ""), feature, ""); + edgeString += String.format( + " %s:s -> %s:n%s;", + quote(feature.getFeature().getIdentifier().toString()), + quote(feature.getFeature().getIdentifier().toString() + "_group"), + options(option("style", feature.getParentGroup().get().isAnd() ? null : "invis"))); + } + return edgeString; + } + + public String getEdge(String parentNode, IFeatureTree childFeature, String option) { + return String.format( + " %s:s -> %s:n%s;%n", + quote(parentNode), + quote(childFeature.getFeature().getIdentifier().toString()), + options( + option( + "arrowhead", + childFeature.getParentGroup().get().isAnd() + ? null + : childFeature.isMandatory() ? "dot" : "odot"), + option)); + } + + protected String quote(String str) { + return String.format("\"%s\"", str.replace("\"", "\\\"")); + } + + protected String options(String... options) { + List optionsList = + Arrays.stream(options).filter(o -> !o.isEmpty()).collect(Collectors.toList()); + if (String.join("", optionsList).trim().isEmpty()) return ""; + return String.format(" [%s]", String.join(" ", optionsList)); + } + + protected String option(String name, String value) { + return value != null ? String.format("%s=%s", name, quote(value)) : ""; + } +} diff --git a/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelFormat.java b/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelFormat.java index acf220b9..e858cdf1 100644 --- a/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelFormat.java +++ b/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelFormat.java @@ -1,75 +1,75 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.io.xml; - -import de.featjar.base.data.Result; -import de.featjar.base.io.format.IFormat; -import de.featjar.base.io.input.AInputMapper; -import de.featjar.base.io.input.InputHeader; -import de.featjar.feature.model.IFeatureModel; -import de.featjar.formula.io.xml.AXMLFeatureModelParser; - -/** - * Parses and writes feature models from and to FeatureIDE XML files. - * - * @author Sebastian Krieter - * @author Elias Kuiter - */ -public class XMLFeatureModelFormat implements IFormat { - - @Override - public String getName() { - return "FeatureIDE"; - } - - @Override - public String getFileExtension() { - return "xml"; - } - - @Override - public boolean supportsParse() { - return true; - } - - @Override - public boolean supportsWrite() { - return true; - } - - @Override - public boolean supportsContent(InputHeader inputHeader) { - return supportsParse() - && AXMLFeatureModelParser.inputHeaderPattern - .matcher(inputHeader.get()) - .find(); - } - - @Override - public Result parse(AInputMapper inputMapper) { - return new XMLFeatureModelParser().parse(inputMapper); - } - - @Override - public Result serialize(IFeatureModel object) { - return new XMLFeatureModelWriter().serialize(object); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.io.xml; + +import de.featjar.base.data.Result; +import de.featjar.base.io.format.IFormat; +import de.featjar.base.io.input.AInputMapper; +import de.featjar.base.io.input.InputHeader; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.formula.io.xml.AXMLFeatureModelParser; + +/** + * Parses and writes feature models from and to FeatureIDE XML files. + * + * @author Sebastian Krieter + * @author Elias Kuiter + */ +public class XMLFeatureModelFormat implements IFormat { + + @Override + public String getName() { + return "FeatureIDE"; + } + + @Override + public String getFileExtension() { + return "xml"; + } + + @Override + public boolean supportsParse() { + return true; + } + + @Override + public boolean supportsWrite() { + return true; + } + + @Override + public boolean supportsContent(InputHeader inputHeader) { + return supportsParse() + && AXMLFeatureModelParser.inputHeaderPattern + .matcher(inputHeader.get()) + .find(); + } + + @Override + public Result parse(AInputMapper inputMapper) { + return new XMLFeatureModelParser().parse(inputMapper); + } + + @Override + public Result serialize(IFeatureModel object) { + return new XMLFeatureModelWriter().serialize(object); + } +} diff --git a/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelParser.java b/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelParser.java index 363732cd..621271bc 100644 --- a/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelParser.java +++ b/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelParser.java @@ -1,229 +1,229 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.io.xml; - -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.COMMENT; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.COMMENTS; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.CONSTRAINTS; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.DATA_TYPE; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.DESCRIPTION; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.EXT_FEATURE_MODEL; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.FEATURE_MODEL; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.KEY; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.NAMESPACE_TAG; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.PROPERTIES; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.PROPERTY; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.STRUCT; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.TAGS; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.VALUE; - -import de.featjar.base.FeatJAR; -import de.featjar.base.data.Attribute; -import de.featjar.base.data.Problem; -import de.featjar.base.data.Result; -import de.featjar.base.data.Sets; -import de.featjar.base.data.identifier.Identifiers; -import de.featjar.base.io.format.ParseException; -import de.featjar.feature.model.FeatureModel; -import de.featjar.feature.model.IConstraint; -import de.featjar.feature.model.IFeature; -import de.featjar.feature.model.IFeatureModel; -import de.featjar.feature.model.IFeatureModelElement; -import de.featjar.feature.model.IFeatureTree; -import de.featjar.feature.model.io.AttributeIO; -import de.featjar.formula.io.xml.AXMLFeatureModelParser; -import de.featjar.formula.structure.IFormula; -import java.util.Arrays; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -/** - * Parses and writes feature models from and to FeatureIDE XML files. - * - * @author Sebastian Krieter - * @author Elias Kuiter - */ -public class XMLFeatureModelParser extends AXMLFeatureModelParser { - - private IFeatureModel featureModel; - private HashSet featureNames; - - @Override - public IFeatureModel parseDocument(Document document) throws ParseException { - featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); - featureNames = Sets.empty(); - final Element featureModelElement = getDocumentElement(document, FEATURE_MODEL, EXT_FEATURE_MODEL); - - parseFeatureTree(getElement(featureModelElement, STRUCT)); - - Result element = getElementResult(featureModelElement, CONSTRAINTS); - if (element.isPresent()) { - parseConstraints(element.get()); - } - - element = getElementResult(featureModelElement, COMMENTS); - if (element.isPresent()) { - parseComments(element.get()); - } - - element = getElementResult(featureModelElement, PROPERTIES); - if (element.isPresent()) { - parseFeatureModelProperties(element.get()); - } - - return featureModel; - } - - @Override - protected IFeatureTree newFeature( - String name, IFeatureTree parentFeature, boolean mandatory, boolean _abstract, boolean hidden) - throws ParseException { - if (!featureNames.add(name)) { - throw new ParseException("Duplicate feature name!"); - } - IFeature feature = featureModel.mutate().addFeature(name); - IFeatureTree featureTree; - if (parentFeature == null) { - featureTree = featureModel.mutate().addFeatureTreeRoot(feature); - } else { - featureTree = parentFeature.mutate().addFeatureBelow(feature); - } - feature.mutate().setAbstract(_abstract); - feature.mutate().setHidden(hidden); - if (mandatory || parentFeature == null) { - featureTree.mutate().makeMandatory(); - } else { - featureTree.mutate().makeOptional(); - } - return featureTree; - } - - @Override - protected void addAndGroup(IFeatureTree featureTree, List children) { - featureTree.mutate().toAndGroup(); - } - - @Override - protected void addOrGroup(IFeatureTree featureTree, List children) { - featureTree.mutate().toOrGroup(); - } - - @Override - protected void addAlternativeGroup(IFeatureTree featureTree, List children) { - featureTree.mutate().toAlternativeGroup(); - } - - @Override - protected void addFeatureMetadata(IFeatureTree feature, Element e) throws ParseException { - String nodeName = e.getNodeName(); - switch (nodeName) { - case DESCRIPTION: - feature.getFeature().mutate().setDescription(getDescription(e)); - break; - case PROPERTY: - parseProperty(feature.getFeature(), e); - break; - default: - FeatJAR.log().warning("Unkown node name %s", nodeName); - } - } - - @Override - protected IConstraint newConstraint(IFormula formula) { - return featureModel.mutate().addConstraint(formula); - } - - @Override - protected void addConstraintMetadata(IConstraint constraint, Element e) throws ParseException { - String nodeName = e.getNodeName(); - switch (nodeName) { - case DESCRIPTION: - constraint.mutate().setDescription(getDescription(e)); - break; - case PROPERTY: - parseProperty(constraint, e); - break; - case TAGS: - constraint.mutate().setTags(getTags(e)); - break; - default: - FeatJAR.log().warning("Unkown node name %s", nodeName); - } - } - - protected void parseComments(Element element) throws ParseException { - for (final Element e1 : getElements(element.getChildNodes())) { - if (e1.getNodeName().equals(COMMENT)) { - featureModel - .mutate() - .setDescription(featureModel.getDescription().orElse("") + "\n" + e1.getTextContent()); - } else { - addParseProblem("Unknown comment attribute: " + e1.getNodeName(), e1, Problem.Severity.WARNING); - } - } - } - - protected static String getDescription(Node e) { - String description = e.getTextContent(); - // NOTE: The following code is used for backwards compatibility. It replaces - // spaces and tabs that were added to the XML for indentation, but don't - // belong to the actual description. - return description == null - ? null - : description.replaceAll("(\r\n|\r|\n)\\s*", "\n").replaceAll("\\A\n|\n\\Z", ""); - } - - protected static LinkedHashSet getTags(final Node e) { - return new LinkedHashSet<>(Arrays.asList(e.getTextContent().split(","))); - } - - protected void parseProperty(IFeatureModelElement featureModelElement, Element e) throws ParseException { - if (!e.hasAttribute(KEY) || !e.hasAttribute(VALUE)) { - addParseProblem( - "Missing one of the required attributes: " + KEY + " or " + VALUE, e, Problem.Severity.WARNING); - } else { - String typeString = e.hasAttribute(DATA_TYPE) ? e.getAttribute(DATA_TYPE) : "string"; - final String namespace = - e.hasAttribute(NAMESPACE_TAG) ? e.getAttribute(NAMESPACE_TAG) : Attribute.DEFAULT_NAMESPACE; - final String name = e.getAttribute(KEY); - final String valueString = e.getAttribute(VALUE); - parseProblems.addAll(AttributeIO.parseAndSetAttributeValue( - featureModelElement, namespace, name, typeString, valueString)); - } - } - - protected void parseFeatureModelProperties(Element e) throws ParseException { - for (final Element propertyElement : getElements(e.getChildNodes())) { - final String nodeName = propertyElement.getNodeName(); - switch (nodeName) { - case PROPERTY: - parseProperty(featureModel, propertyElement); - break; - default: - FeatJAR.log().warning("Unkown node name %s", nodeName); - } - } - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.io.xml; + +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.COMMENT; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.COMMENTS; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.CONSTRAINTS; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.DATA_TYPE; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.DESCRIPTION; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.EXT_FEATURE_MODEL; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.FEATURE_MODEL; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.KEY; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.NAMESPACE_TAG; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.PROPERTIES; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.PROPERTY; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.STRUCT; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.TAGS; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.VALUE; + +import de.featjar.base.FeatJAR; +import de.featjar.base.data.Attribute; +import de.featjar.base.data.Problem; +import de.featjar.base.data.Result; +import de.featjar.base.data.Sets; +import de.featjar.base.data.identifier.Identifiers; +import de.featjar.base.io.format.ParseException; +import de.featjar.feature.model.FeatureModel; +import de.featjar.feature.model.IConstraint; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.IFeatureModelElement; +import de.featjar.feature.model.IFeatureTree; +import de.featjar.feature.model.io.AttributeIO; +import de.featjar.formula.io.xml.AXMLFeatureModelParser; +import de.featjar.formula.structure.IFormula; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * Parses and writes feature models from and to FeatureIDE XML files. + * + * @author Sebastian Krieter + * @author Elias Kuiter + */ +public class XMLFeatureModelParser extends AXMLFeatureModelParser { + + private IFeatureModel featureModel; + private HashSet featureNames; + + @Override + public IFeatureModel parseDocument(Document document) throws ParseException { + featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + featureNames = Sets.empty(); + final Element featureModelElement = getDocumentElement(document, FEATURE_MODEL, EXT_FEATURE_MODEL); + + parseFeatureTree(getElement(featureModelElement, STRUCT)); + + Result element = getElementResult(featureModelElement, CONSTRAINTS); + if (element.isPresent()) { + parseConstraints(element.get()); + } + + element = getElementResult(featureModelElement, COMMENTS); + if (element.isPresent()) { + parseComments(element.get()); + } + + element = getElementResult(featureModelElement, PROPERTIES); + if (element.isPresent()) { + parseFeatureModelProperties(element.get()); + } + + return featureModel; + } + + @Override + protected IFeatureTree newFeature( + String name, IFeatureTree parentFeature, boolean mandatory, boolean _abstract, boolean hidden) + throws ParseException { + if (!featureNames.add(name)) { + throw new ParseException("Duplicate feature name!"); + } + IFeature feature = featureModel.mutate().addFeature(name); + IFeatureTree featureTree; + if (parentFeature == null) { + featureTree = featureModel.mutate().addFeatureTreeRoot(feature); + } else { + featureTree = parentFeature.mutate().addFeatureBelow(feature); + } + feature.mutate().setAbstract(_abstract); + feature.mutate().setHidden(hidden); + if (mandatory || parentFeature == null) { + featureTree.mutate().makeMandatory(); + } else { + featureTree.mutate().makeOptional(); + } + return featureTree; + } + + @Override + protected void addAndGroup(IFeatureTree featureTree, List children) { + featureTree.mutate().toAndGroup(); + } + + @Override + protected void addOrGroup(IFeatureTree featureTree, List children) { + featureTree.mutate().toOrGroup(); + } + + @Override + protected void addAlternativeGroup(IFeatureTree featureTree, List children) { + featureTree.mutate().toAlternativeGroup(); + } + + @Override + protected void addFeatureMetadata(IFeatureTree feature, Element e) throws ParseException { + String nodeName = e.getNodeName(); + switch (nodeName) { + case DESCRIPTION: + feature.getFeature().mutate().setDescription(getDescription(e)); + break; + case PROPERTY: + parseProperty(feature.getFeature(), e); + break; + default: + FeatJAR.log().warning("Unkown node name %s", nodeName); + } + } + + @Override + protected IConstraint newConstraint(IFormula formula) { + return featureModel.mutate().addConstraint(formula); + } + + @Override + protected void addConstraintMetadata(IConstraint constraint, Element e) throws ParseException { + String nodeName = e.getNodeName(); + switch (nodeName) { + case DESCRIPTION: + constraint.mutate().setDescription(getDescription(e)); + break; + case PROPERTY: + parseProperty(constraint, e); + break; + case TAGS: + constraint.mutate().setTags(getTags(e)); + break; + default: + FeatJAR.log().warning("Unkown node name %s", nodeName); + } + } + + protected void parseComments(Element element) throws ParseException { + for (final Element e1 : getElements(element.getChildNodes())) { + if (e1.getNodeName().equals(COMMENT)) { + featureModel + .mutate() + .setDescription(featureModel.getDescription().orElse("") + "\n" + e1.getTextContent()); + } else { + addParseProblem("Unknown comment attribute: " + e1.getNodeName(), e1, Problem.Severity.WARNING); + } + } + } + + protected static String getDescription(Node e) { + String description = e.getTextContent(); + // NOTE: The following code is used for backwards compatibility. It replaces + // spaces and tabs that were added to the XML for indentation, but don't + // belong to the actual description. + return description == null + ? null + : description.replaceAll("(\r\n|\r|\n)\\s*", "\n").replaceAll("\\A\n|\n\\Z", ""); + } + + protected static LinkedHashSet getTags(final Node e) { + return new LinkedHashSet<>(Arrays.asList(e.getTextContent().split(","))); + } + + protected void parseProperty(IFeatureModelElement featureModelElement, Element e) throws ParseException { + if (!e.hasAttribute(KEY) || !e.hasAttribute(VALUE)) { + addParseProblem( + "Missing one of the required attributes: " + KEY + " or " + VALUE, e, Problem.Severity.WARNING); + } else { + String typeString = e.hasAttribute(DATA_TYPE) ? e.getAttribute(DATA_TYPE) : "string"; + final String namespace = + e.hasAttribute(NAMESPACE_TAG) ? e.getAttribute(NAMESPACE_TAG) : Attribute.DEFAULT_NAMESPACE; + final String name = e.getAttribute(KEY); + final String valueString = e.getAttribute(VALUE); + parseProblems.addAll(AttributeIO.parseAndSetAttributeValue( + featureModelElement, namespace, name, typeString, valueString)); + } + } + + protected void parseFeatureModelProperties(Element e) throws ParseException { + for (final Element propertyElement : getElements(e.getChildNodes())) { + final String nodeName = propertyElement.getNodeName(); + switch (nodeName) { + case PROPERTY: + parseProperty(featureModel, propertyElement); + break; + default: + FeatJAR.log().warning("Unkown node name %s", nodeName); + } + } + } +} diff --git a/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelWriter.java b/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelWriter.java index 0ac1e9d7..62c5d2fb 100644 --- a/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelWriter.java +++ b/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelWriter.java @@ -1,267 +1,267 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.io.xml; - -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.ABSTRACT; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.ALT; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.AND; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.ATMOST1; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.CONJ; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.CONSTRAINTS; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.DATA_TYPE; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.DESCRIPTION; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.DISJ; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.EQ; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.FEATURE; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.FEATURE_MODEL; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.HIDDEN; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.IMP; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.KEY; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.MANDATORY; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.NAME; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.NAMESPACE_TAG; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.NOT; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.OR; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.PROPERTY; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.RULE; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.STRUCT; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.TAGS; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.TRUE; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.VALUE; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.VAR; - -import de.featjar.base.FeatJAR; -import de.featjar.base.data.IAttribute; -import de.featjar.base.io.xml.AXMLWriter; -import de.featjar.feature.model.IConstraint; -import de.featjar.feature.model.IFeatureModel; -import de.featjar.feature.model.IFeatureTree; -import de.featjar.feature.model.io.AttributeIO; -import de.featjar.formula.structure.IExpression; -import de.featjar.formula.structure.IFormula; -import de.featjar.formula.structure.connective.And; -import de.featjar.formula.structure.connective.AtMost; -import de.featjar.formula.structure.connective.BiImplies; -import de.featjar.formula.structure.connective.Implies; -import de.featjar.formula.structure.connective.Not; -import de.featjar.formula.structure.connective.Or; -import de.featjar.formula.structure.predicate.Literal; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -/** - * Parses and writes feature models from and to FeatureIDE XML files. - * - * @author Sebastian Krieter - * @author Elias Kuiter - */ -public class XMLFeatureModelWriter extends AXMLWriter { - - private IFeatureModel featureModel; - - @Override - public void writeDocument(IFeatureModel featureModel, Document doc) { - this.featureModel = featureModel; - final Element root = doc.createElement(FEATURE_MODEL); - doc.appendChild(root); - - writeFeatures(doc, root); - writeConstraints(doc, root); - } - - protected void writeFeatures(Document doc, final Element root) { - final Element struct = doc.createElement(STRUCT); - root.appendChild(struct); - writeFeatureTreeRec(doc, struct, featureModel.getRoots().get(0)); - } - - protected void writeConstraints(Document doc, final Element root) { - if (!featureModel.getConstraints().isEmpty()) { - final Element constraints = doc.createElement(CONSTRAINTS); - root.appendChild(constraints); - for (final IConstraint constraint : featureModel.getConstraints()) { - Element rule; - rule = doc.createElement(RULE); - - constraints.appendChild(rule); - addDescription(doc, constraint.getDescription().orElse(null), rule); - addProperties(doc, constraint.getAttributes().get(), rule); - addTags(doc, constraint.getTags(), rule); - createPropositionalConstraints(doc, rule, constraint.getFormula()); - } - } - } - - /** - * Inserts the tags concerning propositional constraints into the DOM document representation - * - * @param doc - * @param node Parent node for the propositional nodes - */ - protected void createPropositionalConstraints(Document doc, Element xmlNode, IFormula node) { - if (node == null) { - return; - } - - final Element op; - if (node instanceof Literal) { - final Literal literal = (Literal) node; - if (!literal.isPositive()) { - final Element opNot = doc.createElement(NOT); - xmlNode.appendChild(opNot); - xmlNode = opNot; - } - op = doc.createElement(VAR); - op.appendChild(doc.createTextNode(literal.getFirstChild().get().getName())); - xmlNode.appendChild(op); - return; - } else if (node instanceof Or) { - op = doc.createElement(DISJ); - } else if (node instanceof BiImplies) { - op = doc.createElement(EQ); - } else if (node instanceof Implies) { - op = doc.createElement(IMP); - } else if (node instanceof And) { - op = doc.createElement(CONJ); - } else if (node instanceof Not) { - op = doc.createElement(NOT); - } else if (node instanceof AtMost) { - op = doc.createElement(ATMOST1); - } else { - FeatJAR.log().error("Unsupported element %s", node); - return; - } - xmlNode.appendChild(op); - - for (final IExpression child : node.getChildren()) { - createPropositionalConstraints(doc, op, (IFormula) child); - } - } - - /** - * Creates document based on feature model step by step - * - * @param doc document to write - * @param node parent node - * @param feat current feature - */ - protected void writeFeatureTreeRec(Document doc, Element node, IFeatureTree feat) { - if (feat == null) { - return; - } - - final List children = feat.getChildren(); - - final Element fnod; - if (children.isEmpty()) { - fnod = doc.createElement(FEATURE); - writeFeatureProperties(doc, node, feat, fnod); - } else { - if (feat.getChildrenGroups().get(0).isAnd()) { - fnod = doc.createElement(AND); - } else if (feat.getChildrenGroups().get(0).isOr()) { - fnod = doc.createElement(OR); - } else if (feat.getChildrenGroups().get(0).isAlternative()) { - fnod = doc.createElement(ALT); - } else { - FeatJAR.log().error("Unkown group %s", feat.getParentGroup()); - return; - } - - writeFeatureProperties(doc, node, feat, fnod); - - for (final IFeatureTree feature : children) { - writeFeatureTreeRec(doc, fnod, feature); - } - } - } - - protected void writeFeatureProperties(Document doc, Element node, IFeatureTree feat, final Element fnod) { - addDescription(doc, feat.getFeature().getDescription().orElse(null), fnod); - if (feat.getAttributes().isPresent()) { - addProperties(doc, feat.getAttributes().get(), fnod); - } - writeAttributes(node, fnod, feat); - } - - protected void addDescription(Document doc, String description, Element fnod) { - if ((description != null) && !description.trim().isEmpty()) { - final Element descr = doc.createElement(DESCRIPTION); - descr.setTextContent(description); - fnod.appendChild(descr); - } - } - - protected void addProperties(Document doc, Map, Object> attributes, Element fnod) { - for (final Entry, Object> property : attributes.entrySet()) { - final Element propNode; - propNode = doc.createElement(PROPERTY); - propNode.setAttribute(NAMESPACE_TAG, property.getKey().getNamespace()); - propNode.setAttribute( - DATA_TYPE, - AttributeIO.getTypeString(property.getKey().getType()) - .orElseThrow(p -> new IllegalArgumentException())); - propNode.setAttribute(KEY, property.getKey().getName()); - propNode.setAttribute(VALUE, property.getValue().toString()); // TODO - fnod.appendChild(propNode); - } - } - - private void addTags(Document doc, Set tags, Element fnod) { - if ((tags != null) && !tags.isEmpty()) { - StringBuilder tagStrings = new StringBuilder(); - for (final String tagString : tags) { - tagStrings.append(tagString); - tagStrings.append(','); - } - if (tagStrings.length() > 0) { - tagStrings.deleteCharAt(tagStrings.length() - 1); - } - final Element tag = doc.createElement(TAGS); - tag.setTextContent(tagStrings.toString()); - fnod.appendChild(tag); - } - } - - protected void writeAttributes(Element node, Element fnod, IFeatureTree feat) { - fnod.setAttribute(NAME, feat.getFeature().getName().get()); - if (feat.getFeature().isHidden()) { - fnod.setAttribute(HIDDEN, TRUE); - } - if (feat.isMandatory() || feat.getParent().isEmpty()) { - if ((feat.getParent().isPresent()) - && feat.getParent().get().getChildrenGroups().get(0).isAnd()) { - fnod.setAttribute(MANDATORY, TRUE); - } else if (feat.getParent().isEmpty()) { - fnod.setAttribute(MANDATORY, TRUE); - } - } - if (feat.getFeature().isAbstract()) { - fnod.setAttribute(ABSTRACT, TRUE); - } - - node.appendChild(fnod); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.io.xml; + +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.ABSTRACT; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.ALT; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.AND; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.ATMOST1; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.CONJ; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.CONSTRAINTS; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.DATA_TYPE; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.DESCRIPTION; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.DISJ; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.EQ; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.FEATURE; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.FEATURE_MODEL; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.HIDDEN; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.IMP; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.KEY; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.MANDATORY; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.NAME; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.NAMESPACE_TAG; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.NOT; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.OR; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.PROPERTY; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.RULE; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.STRUCT; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.TAGS; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.TRUE; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.VALUE; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.VAR; + +import de.featjar.base.FeatJAR; +import de.featjar.base.data.IAttribute; +import de.featjar.base.io.xml.AXMLWriter; +import de.featjar.feature.model.IConstraint; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.IFeatureTree; +import de.featjar.feature.model.io.AttributeIO; +import de.featjar.formula.structure.IExpression; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.connective.And; +import de.featjar.formula.structure.connective.AtMost; +import de.featjar.formula.structure.connective.BiImplies; +import de.featjar.formula.structure.connective.Implies; +import de.featjar.formula.structure.connective.Not; +import de.featjar.formula.structure.connective.Or; +import de.featjar.formula.structure.predicate.Literal; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Parses and writes feature models from and to FeatureIDE XML files. + * + * @author Sebastian Krieter + * @author Elias Kuiter + */ +public class XMLFeatureModelWriter extends AXMLWriter { + + private IFeatureModel featureModel; + + @Override + public void writeDocument(IFeatureModel featureModel, Document doc) { + this.featureModel = featureModel; + final Element root = doc.createElement(FEATURE_MODEL); + doc.appendChild(root); + + writeFeatures(doc, root); + writeConstraints(doc, root); + } + + protected void writeFeatures(Document doc, final Element root) { + final Element struct = doc.createElement(STRUCT); + root.appendChild(struct); + writeFeatureTreeRec(doc, struct, featureModel.getRoots().get(0)); + } + + protected void writeConstraints(Document doc, final Element root) { + if (!featureModel.getConstraints().isEmpty()) { + final Element constraints = doc.createElement(CONSTRAINTS); + root.appendChild(constraints); + for (final IConstraint constraint : featureModel.getConstraints()) { + Element rule; + rule = doc.createElement(RULE); + + constraints.appendChild(rule); + addDescription(doc, constraint.getDescription().orElse(null), rule); + addProperties(doc, constraint.getAttributes().get(), rule); + addTags(doc, constraint.getTags(), rule); + createPropositionalConstraints(doc, rule, constraint.getFormula()); + } + } + } + + /** + * Inserts the tags concerning propositional constraints into the DOM document representation + * + * @param doc + * @param node Parent node for the propositional nodes + */ + protected void createPropositionalConstraints(Document doc, Element xmlNode, IFormula node) { + if (node == null) { + return; + } + + final Element op; + if (node instanceof Literal) { + final Literal literal = (Literal) node; + if (!literal.isPositive()) { + final Element opNot = doc.createElement(NOT); + xmlNode.appendChild(opNot); + xmlNode = opNot; + } + op = doc.createElement(VAR); + op.appendChild(doc.createTextNode(literal.getFirstChild().get().getName())); + xmlNode.appendChild(op); + return; + } else if (node instanceof Or) { + op = doc.createElement(DISJ); + } else if (node instanceof BiImplies) { + op = doc.createElement(EQ); + } else if (node instanceof Implies) { + op = doc.createElement(IMP); + } else if (node instanceof And) { + op = doc.createElement(CONJ); + } else if (node instanceof Not) { + op = doc.createElement(NOT); + } else if (node instanceof AtMost) { + op = doc.createElement(ATMOST1); + } else { + FeatJAR.log().error("Unsupported element %s", node); + return; + } + xmlNode.appendChild(op); + + for (final IExpression child : node.getChildren()) { + createPropositionalConstraints(doc, op, (IFormula) child); + } + } + + /** + * Creates document based on feature model step by step + * + * @param doc document to write + * @param node parent node + * @param feat current feature + */ + protected void writeFeatureTreeRec(Document doc, Element node, IFeatureTree feat) { + if (feat == null) { + return; + } + + final List children = feat.getChildren(); + + final Element fnod; + if (children.isEmpty()) { + fnod = doc.createElement(FEATURE); + writeFeatureProperties(doc, node, feat, fnod); + } else { + if (feat.getChildrenGroups().get(0).isAnd()) { + fnod = doc.createElement(AND); + } else if (feat.getChildrenGroups().get(0).isOr()) { + fnod = doc.createElement(OR); + } else if (feat.getChildrenGroups().get(0).isAlternative()) { + fnod = doc.createElement(ALT); + } else { + FeatJAR.log().error("Unkown group %s", feat.getParentGroup()); + return; + } + + writeFeatureProperties(doc, node, feat, fnod); + + for (final IFeatureTree feature : children) { + writeFeatureTreeRec(doc, fnod, feature); + } + } + } + + protected void writeFeatureProperties(Document doc, Element node, IFeatureTree feat, final Element fnod) { + addDescription(doc, feat.getFeature().getDescription().orElse(null), fnod); + if (feat.getAttributes().isPresent()) { + addProperties(doc, feat.getAttributes().get(), fnod); + } + writeAttributes(node, fnod, feat); + } + + protected void addDescription(Document doc, String description, Element fnod) { + if ((description != null) && !description.trim().isEmpty()) { + final Element descr = doc.createElement(DESCRIPTION); + descr.setTextContent(description); + fnod.appendChild(descr); + } + } + + protected void addProperties(Document doc, Map, Object> attributes, Element fnod) { + for (final Entry, Object> property : attributes.entrySet()) { + final Element propNode; + propNode = doc.createElement(PROPERTY); + propNode.setAttribute(NAMESPACE_TAG, property.getKey().getNamespace()); + propNode.setAttribute( + DATA_TYPE, + AttributeIO.getTypeString(property.getKey().getType()) + .orElseThrow(p -> new IllegalArgumentException())); + propNode.setAttribute(KEY, property.getKey().getName()); + propNode.setAttribute(VALUE, property.getValue().toString()); // TODO + fnod.appendChild(propNode); + } + } + + private void addTags(Document doc, Set tags, Element fnod) { + if ((tags != null) && !tags.isEmpty()) { + StringBuilder tagStrings = new StringBuilder(); + for (final String tagString : tags) { + tagStrings.append(tagString); + tagStrings.append(','); + } + if (tagStrings.length() > 0) { + tagStrings.deleteCharAt(tagStrings.length() - 1); + } + final Element tag = doc.createElement(TAGS); + tag.setTextContent(tagStrings.toString()); + fnod.appendChild(tag); + } + } + + protected void writeAttributes(Element node, Element fnod, IFeatureTree feat) { + fnod.setAttribute(NAME, feat.getFeature().getName().get()); + if (feat.getFeature().isHidden()) { + fnod.setAttribute(HIDDEN, TRUE); + } + if (feat.isMandatory() || feat.getParent().isEmpty()) { + if ((feat.getParent().isPresent()) + && feat.getParent().get().getChildrenGroups().get(0).isAnd()) { + fnod.setAttribute(MANDATORY, TRUE); + } else if (feat.getParent().isEmpty()) { + fnod.setAttribute(MANDATORY, TRUE); + } + } + if (feat.getFeature().isAbstract()) { + fnod.setAttribute(ABSTRACT, TRUE); + } + + node.appendChild(fnod); + } +} diff --git a/src/main/java/de/featjar/feature/model/mixins/IHasCommonAttributes.java b/src/main/java/de/featjar/feature/model/mixins/IHasCommonAttributes.java index 7e847d92..8d99183f 100644 --- a/src/main/java/de/featjar/feature/model/mixins/IHasCommonAttributes.java +++ b/src/main/java/de/featjar/feature/model/mixins/IHasCommonAttributes.java @@ -1,49 +1,49 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.mixins; - -import de.featjar.base.data.Attribute; -import de.featjar.base.data.IAttributable; -import de.featjar.base.data.Result; -import de.featjar.feature.model.*; - -/** - * Implements accessors for commonly used {@link Attribute attributes}. - * That is, all {@link IFeatureModel feature models}, {@link IFeature features}, - * and {@link IConstraint constraints} can have names and descriptions. - * - * @author Elias Kuiter - */ -public interface IHasCommonAttributes extends IAttributable { - default Result getName() { - return getAttributeValue(Attributes.NAME); - } - - default Result getDescription() { - return getAttributeValue(Attributes.DESCRIPTION); - } - - public static interface IHasMutableCommonAttributes extends IMutatableAttributable { - void setName(String name); - - void setDescription(String description); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.mixins; + +import de.featjar.base.data.Attribute; +import de.featjar.base.data.IAttributable; +import de.featjar.base.data.Result; +import de.featjar.feature.model.*; + +/** + * Implements accessors for commonly used {@link Attribute attributes}. + * That is, all {@link IFeatureModel feature models}, {@link IFeature features}, + * and {@link IConstraint constraints} can have names and descriptions. + * + * @author Elias Kuiter + */ +public interface IHasCommonAttributes extends IAttributable { + default Result getName() { + return getAttributeValue(Attributes.NAME); + } + + default Result getDescription() { + return getAttributeValue(Attributes.DESCRIPTION); + } + + public static interface IHasMutableCommonAttributes extends IMutatableAttributable { + void setName(String name); + + void setDescription(String description); + } +} diff --git a/src/main/java/de/featjar/feature/model/mixins/IHasConstraints.java b/src/main/java/de/featjar/feature/model/mixins/IHasConstraints.java index 8703769d..d7365190 100644 --- a/src/main/java/de/featjar/feature/model/mixins/IHasConstraints.java +++ b/src/main/java/de/featjar/feature/model/mixins/IHasConstraints.java @@ -1,56 +1,56 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.mixins; - -import de.featjar.base.data.Result; -import de.featjar.base.data.identifier.IIdentifier; -import de.featjar.feature.model.IConstraint; -import de.featjar.feature.model.IFeatureModel; -import java.util.Collection; -import java.util.Objects; - -/** - * Implements a {@link IFeatureModel} mixin for common operations on {@link IConstraint constraints}. - * - * @author Elias Kuiter - */ -public interface IHasConstraints { - Collection getConstraints(); - - default Result getConstraint(IIdentifier identifier) { - Objects.requireNonNull(identifier); - return Result.ofOptional(getConstraints().stream() - .filter(constraint -> constraint.getIdentifier().equals(identifier)) - .findFirst()); - } - - default boolean hasConstraint(IIdentifier identifier) { - return getConstraint(identifier).isPresent(); - } - - default boolean hasConstraint(IConstraint constraint) { - return hasConstraint(constraint.getIdentifier()); - } - - default int getNumberOfConstraints() { - return getConstraints().size(); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.mixins; + +import de.featjar.base.data.Result; +import de.featjar.base.data.identifier.IIdentifier; +import de.featjar.feature.model.IConstraint; +import de.featjar.feature.model.IFeatureModel; +import java.util.Collection; +import java.util.Objects; + +/** + * Implements a {@link IFeatureModel} mixin for common operations on {@link IConstraint constraints}. + * + * @author Elias Kuiter + */ +public interface IHasConstraints { + Collection getConstraints(); + + default Result getConstraint(IIdentifier identifier) { + Objects.requireNonNull(identifier); + return Result.ofOptional(getConstraints().stream() + .filter(constraint -> constraint.getIdentifier().equals(identifier)) + .findFirst()); + } + + default boolean hasConstraint(IIdentifier identifier) { + return getConstraint(identifier).isPresent(); + } + + default boolean hasConstraint(IConstraint constraint) { + return hasConstraint(constraint.getIdentifier()); + } + + default int getNumberOfConstraints() { + return getConstraints().size(); + } +} diff --git a/src/main/java/de/featjar/feature/model/mixins/IHasFeatureTree.java b/src/main/java/de/featjar/feature/model/mixins/IHasFeatureTree.java index 268687b8..105abfb9 100644 --- a/src/main/java/de/featjar/feature/model/mixins/IHasFeatureTree.java +++ b/src/main/java/de/featjar/feature/model/mixins/IHasFeatureTree.java @@ -1,96 +1,96 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.mixins; - -import de.featjar.base.data.*; -import de.featjar.base.data.identifier.IIdentifier; -import de.featjar.base.tree.Trees; -import de.featjar.feature.model.*; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Implements a {@link IFeatureModel} mixin for common operations on the {@link IFeatureTree}. - * - * @author Elias Kuiter - */ -public interface IHasFeatureTree { - List getRoots(); - - default Stream getFeatureTreeStream() { - return getRoots().stream().flatMap(Trees::preOrderStream); - } - - default LinkedHashSet getTreeFeatures() { - LinkedHashSet featureSet = new LinkedHashSet<>(); - getFeatureTreeStream().map(IFeatureTree::getFeature).forEach(featureSet::add); - return featureSet; - } - - default int getNumberOfTreeFeatures() { - return getTreeFeatures().size(); - } - - default List getRootFeatures() { - return getRoots().stream().map(IFeatureTree::getFeature).collect(Collectors.toList()); - } - - default Result getTreeFeature(IIdentifier identifier) { - Objects.requireNonNull(identifier); - return Result.ofOptional(getFeatureTreeStream() - .map(IFeatureTree::getFeature) - .filter(feature -> feature.getIdentifier().equals(identifier)) - .findFirst()); - } - - default Result getTreeFeature(String name) { - Objects.requireNonNull(name); - return Result.ofOptional(getFeatureTreeStream() - .map(IFeatureTree::getFeature) - .filter(feature -> feature.getName().valueEquals(name)) - .findFirst()); - } - - default Result getFeatureTree(String name) { - Objects.requireNonNull(name); - return Result.ofOptional(getFeatureTreeStream() - .filter(tree -> tree.getFeature().getName().valueEquals(name)) - .findFirst()); - } - - default Result getFeatureTree(IFeature feature) { - Objects.requireNonNull(feature); - return Result.ofOptional(getFeatureTreeStream() - .filter(tree -> tree.getFeature().equals(feature)) - .findFirst()); - } - - default boolean hasTreeFeature(IIdentifier identifier) { - return getTreeFeature(identifier).isPresent(); - } - - default boolean hasTreeFeature(IFeature feature) { - return hasTreeFeature(feature.getIdentifier()); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.mixins; + +import de.featjar.base.data.*; +import de.featjar.base.data.identifier.IIdentifier; +import de.featjar.base.tree.Trees; +import de.featjar.feature.model.*; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Implements a {@link IFeatureModel} mixin for common operations on the {@link IFeatureTree}. + * + * @author Elias Kuiter + */ +public interface IHasFeatureTree { + List getRoots(); + + default Stream getFeatureTreeStream() { + return getRoots().stream().flatMap(Trees::preOrderStream); + } + + default LinkedHashSet getTreeFeatures() { + LinkedHashSet featureSet = new LinkedHashSet<>(); + getFeatureTreeStream().map(IFeatureTree::getFeature).forEach(featureSet::add); + return featureSet; + } + + default int getNumberOfTreeFeatures() { + return getTreeFeatures().size(); + } + + default List getRootFeatures() { + return getRoots().stream().map(IFeatureTree::getFeature).collect(Collectors.toList()); + } + + default Result getTreeFeature(IIdentifier identifier) { + Objects.requireNonNull(identifier); + return Result.ofOptional(getFeatureTreeStream() + .map(IFeatureTree::getFeature) + .filter(feature -> feature.getIdentifier().equals(identifier)) + .findFirst()); + } + + default Result getTreeFeature(String name) { + Objects.requireNonNull(name); + return Result.ofOptional(getFeatureTreeStream() + .map(IFeatureTree::getFeature) + .filter(feature -> feature.getName().valueEquals(name)) + .findFirst()); + } + + default Result getFeatureTree(String name) { + Objects.requireNonNull(name); + return Result.ofOptional(getFeatureTreeStream() + .filter(tree -> tree.getFeature().getName().valueEquals(name)) + .findFirst()); + } + + default Result getFeatureTree(IFeature feature) { + Objects.requireNonNull(feature); + return Result.ofOptional(getFeatureTreeStream() + .filter(tree -> tree.getFeature().equals(feature)) + .findFirst()); + } + + default boolean hasTreeFeature(IIdentifier identifier) { + return getTreeFeature(identifier).isPresent(); + } + + default boolean hasTreeFeature(IFeature feature) { + return hasTreeFeature(feature.getIdentifier()); + } +} diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java index dcacf5bb..6f05920d 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java @@ -1,154 +1,154 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.transformer; - -import de.featjar.base.computation.AComputation; -import de.featjar.base.computation.Dependency; -import de.featjar.base.computation.IComputation; -import de.featjar.base.computation.Progress; -import de.featjar.base.data.Range; -import de.featjar.base.data.Result; -import de.featjar.feature.model.FeatureTree.Group; -import de.featjar.feature.model.IFeature; -import de.featjar.feature.model.IFeatureModel; -import de.featjar.feature.model.IFeatureTree; -import de.featjar.formula.structure.Expressions; -import de.featjar.formula.structure.IFormula; -import de.featjar.formula.structure.connective.And; -import de.featjar.formula.structure.connective.AtLeast; -import de.featjar.formula.structure.connective.AtMost; -import de.featjar.formula.structure.connective.Between; -import de.featjar.formula.structure.connective.Choose; -import de.featjar.formula.structure.connective.Implies; -import de.featjar.formula.structure.connective.Or; -import de.featjar.formula.structure.connective.Reference; -import de.featjar.formula.structure.predicate.Literal; -import de.featjar.formula.structure.term.value.Variable; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; - -/** - * Transforms a feature model into a boolean formula. - * - * @author Sebastian Krieter - */ -public class ComputeFormula extends AComputation { - protected static final Dependency FEATURE_MODEL = Dependency.newDependency(IFeatureModel.class); - - public ComputeFormula(IComputation formula) { - super(formula); - } - - protected ComputeFormula(ComputeFormula other) { - super(other); - } - - @Override - public Result compute(List dependencyList, Progress progress) { - IFeatureModel featureModel = FEATURE_MODEL.get(dependencyList); - ArrayList constraints = new ArrayList<>(); - HashSet variables = new HashSet<>(); - featureModel.getFeatureTreeStream().forEach(node -> { - // TODO use better error value - IFeature feature = node.getFeature(); - String featureName = feature.getName().orElse(""); - Variable variable = new Variable(featureName, feature.getType()); - variables.add(variable); - - // TODO take featureRanges into Account - Result potentialParentTree = node.getParent(); - Literal featureLiteral = Expressions.literal(featureName); - if (potentialParentTree.isEmpty()) { - handleRoot(constraints, featureLiteral, node); - } else { - handleParent(constraints, featureLiteral, node); - } - handleGroups(constraints, featureLiteral, node); - }); - Reference reference = new Reference(new And(constraints)); - reference.setFreeVariables(variables); - return Result.of(reference); - } - - private void handleParent(ArrayList constraints, Literal featureLiteral, IFeatureTree node) { - constraints.add(new Implies( - featureLiteral, - Expressions.literal( - node.getParent().get().getFeature().getName().orElse("")))); - } - - private void handleRoot(ArrayList constraints, Literal featureLiteral, IFeatureTree node) { - if (node.isMandatory()) { - constraints.add(featureLiteral); - } - } - - private void handleGroups(ArrayList constraints, Literal featureLiteral, IFeatureTree node) { - List childrenGroups = node.getChildrenGroups(); - int groupCount = childrenGroups.size(); - ArrayList> groupLiterals = new ArrayList<>(groupCount); - for (int i = 0; i < groupCount; i++) { - groupLiterals.add(null); - } - List children = node.getChildren(); - for (IFeatureTree childNode : children) { - Literal childLiteral = - Expressions.literal(childNode.getFeature().getName().orElse("")); - - if (childNode.isMandatory()) { - constraints.add(new Implies(featureLiteral, childLiteral)); - } - - int groupID = childNode.getParentGroupID(); - List list = groupLiterals.get(groupID); - if (list == null) { - groupLiterals.set(groupID, list = new ArrayList<>()); - } - list.add(childLiteral); - } - for (int i = 0; i < groupCount; i++) { - Group group = childrenGroups.get(i); - if (group != null) { - if (group.isOr()) { - constraints.add(new Implies(featureLiteral, new Or(groupLiterals.get(i)))); - } else if (group.isAlternative()) { - constraints.add(new Implies(featureLiteral, new Choose(1, groupLiterals.get(i)))); - } else { - int lowerBound = group.getLowerBound(); - int upperBound = group.getUpperBound(); - if (lowerBound > 0) { - if (upperBound != Range.OPEN) { - constraints.add(new Implies( - featureLiteral, new Between(lowerBound, upperBound, groupLiterals.get(i)))); - } else { - constraints.add(new Implies(featureLiteral, new AtMost(upperBound, groupLiterals.get(i)))); - } - } else { - if (upperBound != Range.OPEN) { - constraints.add(new Implies(featureLiteral, new AtLeast(lowerBound, groupLiterals.get(i)))); - } - } - } - } - } - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.transformer; + +import de.featjar.base.computation.AComputation; +import de.featjar.base.computation.Dependency; +import de.featjar.base.computation.IComputation; +import de.featjar.base.computation.Progress; +import de.featjar.base.data.Range; +import de.featjar.base.data.Result; +import de.featjar.feature.model.FeatureTree.Group; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.IFeatureTree; +import de.featjar.formula.structure.Expressions; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.connective.And; +import de.featjar.formula.structure.connective.AtLeast; +import de.featjar.formula.structure.connective.AtMost; +import de.featjar.formula.structure.connective.Between; +import de.featjar.formula.structure.connective.Choose; +import de.featjar.formula.structure.connective.Implies; +import de.featjar.formula.structure.connective.Or; +import de.featjar.formula.structure.connective.Reference; +import de.featjar.formula.structure.predicate.Literal; +import de.featjar.formula.structure.term.value.Variable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +/** + * Transforms a feature model into a boolean formula. + * + * @author Sebastian Krieter + */ +public class ComputeFormula extends AComputation { + protected static final Dependency FEATURE_MODEL = Dependency.newDependency(IFeatureModel.class); + + public ComputeFormula(IComputation formula) { + super(formula); + } + + protected ComputeFormula(ComputeFormula other) { + super(other); + } + + @Override + public Result compute(List dependencyList, Progress progress) { + IFeatureModel featureModel = FEATURE_MODEL.get(dependencyList); + ArrayList constraints = new ArrayList<>(); + HashSet variables = new HashSet<>(); + featureModel.getFeatureTreeStream().forEach(node -> { + // TODO use better error value + IFeature feature = node.getFeature(); + String featureName = feature.getName().orElse(""); + Variable variable = new Variable(featureName, feature.getType()); + variables.add(variable); + + // TODO take featureRanges into Account + Result potentialParentTree = node.getParent(); + Literal featureLiteral = Expressions.literal(featureName); + if (potentialParentTree.isEmpty()) { + handleRoot(constraints, featureLiteral, node); + } else { + handleParent(constraints, featureLiteral, node); + } + handleGroups(constraints, featureLiteral, node); + }); + Reference reference = new Reference(new And(constraints)); + reference.setFreeVariables(variables); + return Result.of(reference); + } + + private void handleParent(ArrayList constraints, Literal featureLiteral, IFeatureTree node) { + constraints.add(new Implies( + featureLiteral, + Expressions.literal( + node.getParent().get().getFeature().getName().orElse("")))); + } + + private void handleRoot(ArrayList constraints, Literal featureLiteral, IFeatureTree node) { + if (node.isMandatory()) { + constraints.add(featureLiteral); + } + } + + private void handleGroups(ArrayList constraints, Literal featureLiteral, IFeatureTree node) { + List childrenGroups = node.getChildrenGroups(); + int groupCount = childrenGroups.size(); + ArrayList> groupLiterals = new ArrayList<>(groupCount); + for (int i = 0; i < groupCount; i++) { + groupLiterals.add(null); + } + List children = node.getChildren(); + for (IFeatureTree childNode : children) { + Literal childLiteral = + Expressions.literal(childNode.getFeature().getName().orElse("")); + + if (childNode.isMandatory()) { + constraints.add(new Implies(featureLiteral, childLiteral)); + } + + int groupID = childNode.getParentGroupID(); + List list = groupLiterals.get(groupID); + if (list == null) { + groupLiterals.set(groupID, list = new ArrayList<>()); + } + list.add(childLiteral); + } + for (int i = 0; i < groupCount; i++) { + Group group = childrenGroups.get(i); + if (group != null) { + if (group.isOr()) { + constraints.add(new Implies(featureLiteral, new Or(groupLiterals.get(i)))); + } else if (group.isAlternative()) { + constraints.add(new Implies(featureLiteral, new Choose(1, groupLiterals.get(i)))); + } else { + int lowerBound = group.getLowerBound(); + int upperBound = group.getUpperBound(); + if (lowerBound > 0) { + if (upperBound != Range.OPEN) { + constraints.add(new Implies( + featureLiteral, new Between(lowerBound, upperBound, groupLiterals.get(i)))); + } else { + constraints.add(new Implies(featureLiteral, new AtMost(upperBound, groupLiterals.get(i)))); + } + } else { + if (upperBound != Range.OPEN) { + constraints.add(new Implies(featureLiteral, new AtLeast(lowerBound, groupLiterals.get(i)))); + } + } + } + } + } + } +} diff --git a/src/test/java/de/featjar/feature/model/AttributeTest.java b/src/test/java/de/featjar/feature/model/AttributeTest.java index 26d50c10..95d08858 100644 --- a/src/test/java/de/featjar/feature/model/AttributeTest.java +++ b/src/test/java/de/featjar/feature/model/AttributeTest.java @@ -1,117 +1,117 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import de.featjar.base.data.Attribute; -import de.featjar.base.data.IAttributable; -import de.featjar.base.data.IAttribute; -import de.featjar.base.data.Result; -import de.featjar.base.data.identifier.Identifiers; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Optional; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** - * Tests for {@link Attribute}, {@link Attributes}, and {@link IAttributable}. - * - * @author Elias Kuiter - */ -public class AttributeTest { - FeatureModel featureModel; - Attribute attribute = new Attribute<>("any", "test", String.class); - - @BeforeEach - public void createFeatureModel() { - featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); - } - - @Test - public void attribute() { - assertEquals("any", attribute.getNamespace()); - assertEquals("test", attribute.getName()); - assertEquals(String.class, attribute.getType()); - } - - @Test - public void attributableGetSet() { - LinkedHashMap, Object> attributeToValueMap = new LinkedHashMap<>(); - Attribute attributeWithDefaultValue = new Attribute<>("test", String.class).setDefaultValue("default"); - Assertions.assertTrue(featureModel.getAttributeValue(attribute).isEmpty()); - Assertions.assertEquals(Result.of("default"), featureModel.getAttributeValue(attributeWithDefaultValue)); - assertEquals(attributeToValueMap, featureModel.getAttributes().get()); - featureModel.mutate().setAttributeValue(attribute, "value"); - attributeToValueMap.put(attribute, "value"); - IAttributable attributable = new IAttributable() { - @Override - public Optional, Object>> getAttributes() { - return Optional.of(attributeToValueMap); - } - }; - Assertions.assertEquals(Result.of("value"), featureModel.getAttributeValue(attribute)); - assertEquals(Result.of("value"), attribute.apply(attributable)); - assertTrue(featureModel.getAttributes().isPresent()); - assertEquals(attributeToValueMap, featureModel.getAttributes().get()); - featureModel.mutate().removeAttributeValue(attribute); - attributeToValueMap.clear(); - Assertions.assertEquals(Result.empty(), featureModel.getAttributeValue(attribute)); - assertEquals(attributeToValueMap, featureModel.getAttributes().get()); - } - - @Test - public void attributableToggle() { - Attribute booleanAttribute = new Attribute<>("test", Boolean.class).setDefaultValue(false); - Assertions.assertEquals(Result.of(false), featureModel.getAttributeValue(booleanAttribute)); - featureModel.mutate().toggleAttributeValue(booleanAttribute); - Assertions.assertEquals(Result.of(true), featureModel.getAttributeValue(booleanAttribute)); - } - - @Test - public void attributesName() { - Assertions.assertEquals(featureModel.getName(), featureModel.getAttributeValue(Attributes.NAME)); - Assertions.assertEquals( - "@" + featureModel.getIdentifier(), featureModel.getName().get()); - Assertions.assertEquals(Result.empty(), featureModel.getDescription()); - } - - @Test - public void attributesDescription() { - featureModel.mutate().setDescription("desc"); - Assertions.assertEquals(Result.of("desc"), featureModel.getDescription()); - featureModel.mutate().setDescription(null); - Assertions.assertEquals(Result.empty(), featureModel.getDescription()); - } - - @Test - public void attributesHidden() { - Assertions.assertTrue(featureModel.getRootFeatures().isEmpty()); - IFeature addFeature = featureModel.addFeature("hiddenFeature"); - Assertions.assertFalse(addFeature.isHidden()); - addFeature.mutate().setHidden(true); - Assertions.assertTrue(addFeature.isHidden()); - Assertions.assertFalse(addFeature.mutate().toggleHidden()); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import de.featjar.base.data.Attribute; +import de.featjar.base.data.IAttributable; +import de.featjar.base.data.IAttribute; +import de.featjar.base.data.Result; +import de.featjar.base.data.identifier.Identifiers; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link Attribute}, {@link Attributes}, and {@link IAttributable}. + * + * @author Elias Kuiter + */ +public class AttributeTest { + FeatureModel featureModel; + Attribute attribute = new Attribute<>("any", "test", String.class); + + @BeforeEach + public void createFeatureModel() { + featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + } + + @Test + public void attribute() { + assertEquals("any", attribute.getNamespace()); + assertEquals("test", attribute.getName()); + assertEquals(String.class, attribute.getType()); + } + + @Test + public void attributableGetSet() { + LinkedHashMap, Object> attributeToValueMap = new LinkedHashMap<>(); + Attribute attributeWithDefaultValue = new Attribute<>("test", String.class).setDefaultValue("default"); + Assertions.assertTrue(featureModel.getAttributeValue(attribute).isEmpty()); + Assertions.assertEquals(Result.of("default"), featureModel.getAttributeValue(attributeWithDefaultValue)); + assertEquals(attributeToValueMap, featureModel.getAttributes().get()); + featureModel.mutate().setAttributeValue(attribute, "value"); + attributeToValueMap.put(attribute, "value"); + IAttributable attributable = new IAttributable() { + @Override + public Optional, Object>> getAttributes() { + return Optional.of(attributeToValueMap); + } + }; + Assertions.assertEquals(Result.of("value"), featureModel.getAttributeValue(attribute)); + assertEquals(Result.of("value"), attribute.apply(attributable)); + assertTrue(featureModel.getAttributes().isPresent()); + assertEquals(attributeToValueMap, featureModel.getAttributes().get()); + featureModel.mutate().removeAttributeValue(attribute); + attributeToValueMap.clear(); + Assertions.assertEquals(Result.empty(), featureModel.getAttributeValue(attribute)); + assertEquals(attributeToValueMap, featureModel.getAttributes().get()); + } + + @Test + public void attributableToggle() { + Attribute booleanAttribute = new Attribute<>("test", Boolean.class).setDefaultValue(false); + Assertions.assertEquals(Result.of(false), featureModel.getAttributeValue(booleanAttribute)); + featureModel.mutate().toggleAttributeValue(booleanAttribute); + Assertions.assertEquals(Result.of(true), featureModel.getAttributeValue(booleanAttribute)); + } + + @Test + public void attributesName() { + Assertions.assertEquals(featureModel.getName(), featureModel.getAttributeValue(Attributes.NAME)); + Assertions.assertEquals( + "@" + featureModel.getIdentifier(), featureModel.getName().get()); + Assertions.assertEquals(Result.empty(), featureModel.getDescription()); + } + + @Test + public void attributesDescription() { + featureModel.mutate().setDescription("desc"); + Assertions.assertEquals(Result.of("desc"), featureModel.getDescription()); + featureModel.mutate().setDescription(null); + Assertions.assertEquals(Result.empty(), featureModel.getDescription()); + } + + @Test + public void attributesHidden() { + Assertions.assertTrue(featureModel.getRootFeatures().isEmpty()); + IFeature addFeature = featureModel.addFeature("hiddenFeature"); + Assertions.assertFalse(addFeature.isHidden()); + addFeature.mutate().setHidden(true); + Assertions.assertTrue(addFeature.isHidden()); + Assertions.assertFalse(addFeature.mutate().toggleHidden()); + } +} diff --git a/src/test/java/de/featjar/feature/model/FeatureModelTest.java b/src/test/java/de/featjar/feature/model/FeatureModelTest.java index 73cd4739..e1566fff 100644 --- a/src/test/java/de/featjar/feature/model/FeatureModelTest.java +++ b/src/test/java/de/featjar/feature/model/FeatureModelTest.java @@ -1,102 +1,102 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import static org.junit.jupiter.api.Assertions.*; - -import de.featjar.base.data.Result; -import de.featjar.base.data.identifier.Identifiers; -import de.featjar.formula.structure.Expressions; -import java.util.*; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** - * Tests for {@link FeatureModel}, its elements, and its mixins. - * - * @author Elias Kuiter - */ -public class FeatureModelTest { - IFeatureModel featureModel; - - @BeforeEach - public void createFeatureModel() { - featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); - } - - @Test - public void featureModel() { - Assertions.assertEquals("1", featureModel.getIdentifier().toString()); - assertTrue(featureModel.getRoots().isEmpty()); - assertTrue(featureModel.getFeatures().isEmpty()); - assertTrue(featureModel.getConstraints().isEmpty()); - } - - @Test - public void commonAttributesMixin() { - Assertions.assertEquals("@1", featureModel.getName().get()); - Assertions.assertEquals(Result.empty(), featureModel.getDescription()); - featureModel.mutate().setName("My Model"); - featureModel.mutate().setDescription("awesome description"); - Assertions.assertEquals(Result.of("My Model"), featureModel.getName()); - Assertions.assertEquals(Result.of("awesome description"), featureModel.getDescription()); - } - - @Test - public void featureModelConstraintMixin() { - Assertions.assertEquals(0, featureModel.getNumberOfConstraints()); - IConstraint constraint1 = featureModel.mutate().addConstraint(Expressions.True); - IConstraint constraint2 = featureModel.mutate().addConstraint(Expressions.True); - IConstraint constraint3 = featureModel.mutate().addConstraint(Expressions.False); - Assertions.assertEquals(3, featureModel.getNumberOfConstraints()); - Assertions.assertEquals(Result.of(constraint1), featureModel.getConstraint(constraint1.getIdentifier())); - Assertions.assertTrue(featureModel.hasConstraint(constraint2.getIdentifier())); - constraint2.mutate().remove(); - Assertions.assertFalse(featureModel.hasConstraint(constraint2.getIdentifier())); - Assertions.assertTrue(featureModel.hasConstraint(constraint3)); - } - - @Test - public void featureModelFeatureTreeMixin() { - IFeature rootFeature = featureModel.mutate().addFeature("root"); - Assertions.assertEquals(1, featureModel.getNumberOfFeatures()); - IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(rootFeature); - IFeature childFeature = featureModel.mutate().addFeature("child1"); - final IFeatureTree childTree = rootTree.mutate().addFeatureBelow(childFeature); - assertSame(childFeature, childTree.getFeature()); - assertSame(childTree, childFeature.getFeatureTree().get()); - assertSame(childFeature, childTree.getFeature()); - assertSame(rootFeature, childTree.getParent().get().getFeature()); - assertSame(childTree.getParent().get(), rootFeature.getFeatureTree().get()); - assertSame(featureModel.getFeature(childFeature.getIdentifier()).get(), childFeature); - Assertions.assertEquals(2, featureModel.getNumberOfFeatures()); - Assertions.assertEquals(Result.of(childFeature), featureModel.getFeature(childFeature.getIdentifier())); - Assertions.assertTrue(featureModel.hasFeature(childFeature.getIdentifier())); - Assertions.assertTrue(featureModel.getFeature("root2").isEmpty()); - rootFeature.mutate().setName("root2"); - Assertions.assertEquals(Result.of(rootFeature), featureModel.getFeature("root2")); - assertEquals(List.of(childTree), rootFeature.getFeatureTree().get().getChildren()); - assertEquals(rootFeature.getFeatureTree(), childTree.getParent()); - childTree.mutate().removeFromTree(); - assertEquals(List.of(), rootFeature.getFeatureTree().get().getChildren()); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import static org.junit.jupiter.api.Assertions.*; + +import de.featjar.base.data.Result; +import de.featjar.base.data.identifier.Identifiers; +import de.featjar.formula.structure.Expressions; +import java.util.*; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link FeatureModel}, its elements, and its mixins. + * + * @author Elias Kuiter + */ +public class FeatureModelTest { + IFeatureModel featureModel; + + @BeforeEach + public void createFeatureModel() { + featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + } + + @Test + public void featureModel() { + Assertions.assertEquals("1", featureModel.getIdentifier().toString()); + assertTrue(featureModel.getRoots().isEmpty()); + assertTrue(featureModel.getFeatures().isEmpty()); + assertTrue(featureModel.getConstraints().isEmpty()); + } + + @Test + public void commonAttributesMixin() { + Assertions.assertEquals("@1", featureModel.getName().get()); + Assertions.assertEquals(Result.empty(), featureModel.getDescription()); + featureModel.mutate().setName("My Model"); + featureModel.mutate().setDescription("awesome description"); + Assertions.assertEquals(Result.of("My Model"), featureModel.getName()); + Assertions.assertEquals(Result.of("awesome description"), featureModel.getDescription()); + } + + @Test + public void featureModelConstraintMixin() { + Assertions.assertEquals(0, featureModel.getNumberOfConstraints()); + IConstraint constraint1 = featureModel.mutate().addConstraint(Expressions.True); + IConstraint constraint2 = featureModel.mutate().addConstraint(Expressions.True); + IConstraint constraint3 = featureModel.mutate().addConstraint(Expressions.False); + Assertions.assertEquals(3, featureModel.getNumberOfConstraints()); + Assertions.assertEquals(Result.of(constraint1), featureModel.getConstraint(constraint1.getIdentifier())); + Assertions.assertTrue(featureModel.hasConstraint(constraint2.getIdentifier())); + constraint2.mutate().remove(); + Assertions.assertFalse(featureModel.hasConstraint(constraint2.getIdentifier())); + Assertions.assertTrue(featureModel.hasConstraint(constraint3)); + } + + @Test + public void featureModelFeatureTreeMixin() { + IFeature rootFeature = featureModel.mutate().addFeature("root"); + Assertions.assertEquals(1, featureModel.getNumberOfFeatures()); + IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(rootFeature); + IFeature childFeature = featureModel.mutate().addFeature("child1"); + final IFeatureTree childTree = rootTree.mutate().addFeatureBelow(childFeature); + assertSame(childFeature, childTree.getFeature()); + assertSame(childTree, childFeature.getFeatureTree().get()); + assertSame(childFeature, childTree.getFeature()); + assertSame(rootFeature, childTree.getParent().get().getFeature()); + assertSame(childTree.getParent().get(), rootFeature.getFeatureTree().get()); + assertSame(featureModel.getFeature(childFeature.getIdentifier()).get(), childFeature); + Assertions.assertEquals(2, featureModel.getNumberOfFeatures()); + Assertions.assertEquals(Result.of(childFeature), featureModel.getFeature(childFeature.getIdentifier())); + Assertions.assertTrue(featureModel.hasFeature(childFeature.getIdentifier())); + Assertions.assertTrue(featureModel.getFeature("root2").isEmpty()); + rootFeature.mutate().setName("root2"); + Assertions.assertEquals(Result.of(rootFeature), featureModel.getFeature("root2")); + assertEquals(List.of(childTree), rootFeature.getFeatureTree().get().getChildren()); + assertEquals(rootFeature.getFeatureTree(), childTree.getParent()); + childTree.mutate().removeFromTree(); + assertEquals(List.of(), rootFeature.getFeatureTree().get().getChildren()); + } +} diff --git a/src/test/java/de/featjar/feature/model/FeatureTest.java b/src/test/java/de/featjar/feature/model/FeatureTest.java index 58b9bb0c..8758ad35 100644 --- a/src/test/java/de/featjar/feature/model/FeatureTest.java +++ b/src/test/java/de/featjar/feature/model/FeatureTest.java @@ -1,117 +1,117 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import de.featjar.base.data.identifier.Identifiers; -import de.featjar.formula.structure.IFormula; -import de.featjar.formula.structure.connective.And; -import de.featjar.formula.structure.connective.BiImplies; -import de.featjar.formula.structure.connective.Implies; -import de.featjar.formula.structure.connective.Not; -import de.featjar.formula.structure.connective.Or; -import de.featjar.formula.structure.predicate.Literal; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -public class FeatureTest { - private static FeatureModel featureModel; - - @BeforeAll - public static void setup() { - FeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); - - // features - IFeatureTree rootTree = - featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - rootTree.mutate().toAndGroup(); - - IFeature childFeature1 = featureModel.mutate().addFeature("Test1"); - IFeatureTree childTree1 = rootTree.mutate().addFeatureBelow(childFeature1); - - IFeature childFeature2 = featureModel.mutate().addFeature("Test2"); - IFeatureTree childTree2 = rootTree.mutate().addFeatureBelow(childFeature2); - - IFeature childFeature3 = featureModel.mutate().addFeature("Test3"); - IFeatureTree childTree3 = childTree1.mutate().addFeatureBelow(childFeature3); - childTree1.mutate().toAlternativeGroup(); - - IFeature childFeature4 = featureModel.mutate().addFeature("Test4"); - childTree1.mutate().addFeatureBelow(childFeature4); - - IFeature childFeature5 = featureModel.mutate().addFeature("Test5"); - IFeatureTree childTree5 = childTree2.mutate().addFeatureBelow(childFeature5); - childTree2.mutate().toOrGroup(); - - IFeature childFeature6 = featureModel.mutate().addFeature("Test6"); - childTree2.mutate().addFeatureBelow(childFeature6); - - IFeature childFeature7 = featureModel.mutate().addFeature("Test7"); - IFeatureTree childTree7 = rootTree.mutate().addFeatureBelow(childFeature7); - childTree7.mutate().makeMandatory(); - - IFeature childFeature8 = featureModel.mutate().addFeature("Test8"); - childTree3.mutate().addFeatureBelow(childFeature8); - - IFormula formula1 = new Or( - new And(new Literal("Test1"), new Literal("Test2")), - new BiImplies(new Literal("Test3"), new Literal("Test4")), - new Implies(new Literal("Test5"), new Literal("Test6")), - new Not(new Literal("Test7"))); - - // constraints - featureModel.mutate().addConstraint(formula1); - - featureModel.getFeature("Test1").get().mutate().setHidden(true); - - FeatureTest.featureModel = featureModel; - } - - @Test - public void hasHiddenParentMethodCorrectlyTraversesTreeToCheckIfThereIsAParentThatIsHidden() { - IFeature root = featureModel.getFeature("root").get(); - IFeature test1 = featureModel.getFeature("Test1").get(); - IFeature test2 = featureModel.getFeature("Test2").get(); - IFeature test3 = featureModel.getFeature("Test3").get(); - IFeature test5 = featureModel.getFeature("Test5").get(); - IFeature test8 = featureModel.getFeature("Test8").get(); - - // Tests the visible root. - assertFalse(root.hasHiddenParent()); - - // Tests the hidden feature. - assertFalse(test1.hasHiddenParent()); - - // Tests a visible feature at the same depth of the hidden feature (test1). - assertFalse(test2.hasHiddenParent()); - - // Tests child of hidden feature (test1). - assertTrue(test3.hasHiddenParent()); - - // Tests child of visible feature (test2). - assertFalse(test5.hasHiddenParent()); - - // Tests grandchild for hidden feature (test1). - assertTrue(test8.hasHiddenParent()); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import de.featjar.base.data.identifier.Identifiers; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.connective.And; +import de.featjar.formula.structure.connective.BiImplies; +import de.featjar.formula.structure.connective.Implies; +import de.featjar.formula.structure.connective.Not; +import de.featjar.formula.structure.connective.Or; +import de.featjar.formula.structure.predicate.Literal; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class FeatureTest { + private static FeatureModel featureModel; + + @BeforeAll + public static void setup() { + FeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + + // features + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAndGroup(); + + IFeature childFeature1 = featureModel.mutate().addFeature("Test1"); + IFeatureTree childTree1 = rootTree.mutate().addFeatureBelow(childFeature1); + + IFeature childFeature2 = featureModel.mutate().addFeature("Test2"); + IFeatureTree childTree2 = rootTree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("Test3"); + IFeatureTree childTree3 = childTree1.mutate().addFeatureBelow(childFeature3); + childTree1.mutate().toAlternativeGroup(); + + IFeature childFeature4 = featureModel.mutate().addFeature("Test4"); + childTree1.mutate().addFeatureBelow(childFeature4); + + IFeature childFeature5 = featureModel.mutate().addFeature("Test5"); + IFeatureTree childTree5 = childTree2.mutate().addFeatureBelow(childFeature5); + childTree2.mutate().toOrGroup(); + + IFeature childFeature6 = featureModel.mutate().addFeature("Test6"); + childTree2.mutate().addFeatureBelow(childFeature6); + + IFeature childFeature7 = featureModel.mutate().addFeature("Test7"); + IFeatureTree childTree7 = rootTree.mutate().addFeatureBelow(childFeature7); + childTree7.mutate().makeMandatory(); + + IFeature childFeature8 = featureModel.mutate().addFeature("Test8"); + childTree3.mutate().addFeatureBelow(childFeature8); + + IFormula formula1 = new Or( + new And(new Literal("Test1"), new Literal("Test2")), + new BiImplies(new Literal("Test3"), new Literal("Test4")), + new Implies(new Literal("Test5"), new Literal("Test6")), + new Not(new Literal("Test7"))); + + // constraints + featureModel.mutate().addConstraint(formula1); + + featureModel.getFeature("Test1").get().mutate().setHidden(true); + + FeatureTest.featureModel = featureModel; + } + + @Test + public void hasHiddenParentMethodCorrectlyTraversesTreeToCheckIfThereIsAParentThatIsHidden() { + IFeature root = featureModel.getFeature("root").get(); + IFeature test1 = featureModel.getFeature("Test1").get(); + IFeature test2 = featureModel.getFeature("Test2").get(); + IFeature test3 = featureModel.getFeature("Test3").get(); + IFeature test5 = featureModel.getFeature("Test5").get(); + IFeature test8 = featureModel.getFeature("Test8").get(); + + // Tests the visible root. + assertFalse(root.hasHiddenParent()); + + // Tests the hidden feature. + assertFalse(test1.hasHiddenParent()); + + // Tests a visible feature at the same depth of the hidden feature (test1). + assertFalse(test2.hasHiddenParent()); + + // Tests child of hidden feature (test1). + assertTrue(test3.hasHiddenParent()); + + // Tests child of visible feature (test2). + assertFalse(test5.hasHiddenParent()); + + // Tests grandchild for hidden feature (test1). + assertTrue(test8.hasHiddenParent()); + } +} diff --git a/src/test/java/de/featjar/feature/model/IdentifierTest.java b/src/test/java/de/featjar/feature/model/IdentifierTest.java index d72eabe1..1a221d82 100644 --- a/src/test/java/de/featjar/feature/model/IdentifierTest.java +++ b/src/test/java/de/featjar/feature/model/IdentifierTest.java @@ -1,87 +1,87 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; - -import de.featjar.base.data.identifier.AIdentifier; -import de.featjar.base.data.identifier.IIdentifiable; -import de.featjar.base.data.identifier.IIdentifier; -import de.featjar.base.data.identifier.Identifiers; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** - * Tests for {@link AIdentifier} and {@link IIdentifiable}. - * - * @author Elias Kuiter - */ -public class IdentifierTest { - IFeatureModel featureModel; - - @BeforeEach - public void createFeatureModel() { - featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); - } - - @Test - void identifierCounter() { - IIdentifier identifier = Identifiers.newCounterIdentifier(); - assertEquals("1", identifier.toString()); - assertEquals("2", identifier.getNewIdentifier().toString()); - assertEquals("3", identifier.getNewIdentifier().toString()); - assertNotEquals(identifier.toString(), identifier.getNewIdentifier().toString()); - assertEquals(identifier, identifier.getFactory().parse(identifier.toString())); - } - - @Test - void identifierUUID() { - IIdentifier identifier = Identifiers.newUUIDIdentifier(); - assertNotEquals(identifier.toString(), identifier.getNewIdentifier().toString()); - assertEquals(identifier, identifier.getFactory().parse(identifier.toString())); - } - - @Test - void identifiable() { - IIdentifier identifier = Identifiers.newCounterIdentifier(); - featureModel = new FeatureModel(identifier); - assertEquals("1", featureModel.getIdentifier().toString()); - featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - Assertions.assertEquals( - "2", featureModel.getRootFeatures().get(0).getIdentifier().toString()); - Assertions.assertEquals("3", identifier.getFactory().get().toString()); - Assertions.assertEquals( - "4", featureModel.getRootFeatures().get(0).getNewIdentifier().toString()); - featureModel = new FeatureModel(identifier.getNewIdentifier()); - featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - assertEquals("5", featureModel.getIdentifier().toString()); - Assertions.assertEquals( - "6", featureModel.getRootFeatures().get(0).getIdentifier().toString()); - assertEquals("7", featureModel.getNewIdentifier().toString()); - assertEquals( - "2", - new FeatureModel(Identifiers.newCounterIdentifier()) - .getNewIdentifier() - .toString()); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import de.featjar.base.data.identifier.AIdentifier; +import de.featjar.base.data.identifier.IIdentifiable; +import de.featjar.base.data.identifier.IIdentifier; +import de.featjar.base.data.identifier.Identifiers; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link AIdentifier} and {@link IIdentifiable}. + * + * @author Elias Kuiter + */ +public class IdentifierTest { + IFeatureModel featureModel; + + @BeforeEach + public void createFeatureModel() { + featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + } + + @Test + void identifierCounter() { + IIdentifier identifier = Identifiers.newCounterIdentifier(); + assertEquals("1", identifier.toString()); + assertEquals("2", identifier.getNewIdentifier().toString()); + assertEquals("3", identifier.getNewIdentifier().toString()); + assertNotEquals(identifier.toString(), identifier.getNewIdentifier().toString()); + assertEquals(identifier, identifier.getFactory().parse(identifier.toString())); + } + + @Test + void identifierUUID() { + IIdentifier identifier = Identifiers.newUUIDIdentifier(); + assertNotEquals(identifier.toString(), identifier.getNewIdentifier().toString()); + assertEquals(identifier, identifier.getFactory().parse(identifier.toString())); + } + + @Test + void identifiable() { + IIdentifier identifier = Identifiers.newCounterIdentifier(); + featureModel = new FeatureModel(identifier); + assertEquals("1", featureModel.getIdentifier().toString()); + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + Assertions.assertEquals( + "2", featureModel.getRootFeatures().get(0).getIdentifier().toString()); + Assertions.assertEquals("3", identifier.getFactory().get().toString()); + Assertions.assertEquals( + "4", featureModel.getRootFeatures().get(0).getNewIdentifier().toString()); + featureModel = new FeatureModel(identifier.getNewIdentifier()); + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + assertEquals("5", featureModel.getIdentifier().toString()); + Assertions.assertEquals( + "6", featureModel.getRootFeatures().get(0).getIdentifier().toString()); + assertEquals("7", featureModel.getNewIdentifier().toString()); + assertEquals( + "2", + new FeatureModel(Identifiers.newCounterIdentifier()) + .getNewIdentifier() + .toString()); + } +} diff --git a/src/test/java/de/featjar/feature/model/configuration/ConfigurationTest.java b/src/test/java/de/featjar/feature/model/configuration/ConfigurationTest.java index 5e40def2..b9a4c911 100644 --- a/src/test/java/de/featjar/feature/model/configuration/ConfigurationTest.java +++ b/src/test/java/de/featjar/feature/model/configuration/ConfigurationTest.java @@ -1,356 +1,356 @@ -/* -< * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.configuration; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import de.featjar.base.data.identifier.Identifiers; -import de.featjar.feature.configuration.Configuration; -import de.featjar.feature.configuration.Configuration.Selection; -import de.featjar.feature.configuration.Configuration.SelectionNotPossibleException; -import de.featjar.feature.model.FeatureModel; -import de.featjar.feature.model.IFeature; -import de.featjar.feature.model.IFeatureTree; -import de.featjar.formula.structure.IFormula; -import de.featjar.formula.structure.connective.And; -import de.featjar.formula.structure.connective.BiImplies; -import de.featjar.formula.structure.connective.Implies; -import de.featjar.formula.structure.connective.Not; -import de.featjar.formula.structure.connective.Or; -import de.featjar.formula.structure.predicate.Literal; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -public class ConfigurationTest { - - private static FeatureModel featureModel; - private static Configuration configuration; - - @BeforeAll - public static void setupTestConfiguration() { - - FeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); - - // features - IFeatureTree rootTree = - featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - rootTree.mutate().toAlternativeGroup(); - - IFeature childFeature1 = featureModel.mutate().addFeature("Test1"); - IFeatureTree childTree1 = rootTree.mutate().addFeatureBelow(childFeature1); - childTree1.mutate().toAlternativeGroup(); - - IFeature childFeature2 = featureModel.mutate().addFeature("Test2"); - IFeatureTree childTree2 = rootTree.mutate().addFeatureBelow(childFeature2); - childTree2.mutate().toOrGroup(); - - IFeature childFeature3 = featureModel.mutate().addFeature("Test3"); - IFeatureTree childTree3 = childTree1.mutate().addFeatureBelow(childFeature3); - - IFeature childFeature4 = featureModel.mutate().addFeature("Test4"); - childTree1.mutate().addFeatureBelow(childFeature4); - - IFeature childFeature5 = featureModel.mutate().addFeature("Test5"); - IFeatureTree childTree5 = childTree2.mutate().addFeatureBelow(childFeature5); - - IFeature childFeature6 = featureModel.mutate().addFeature("Test6"); - childTree2.mutate().addFeatureBelow(childFeature6); - - IFeature childFeature7 = featureModel.mutate().addFeature("Test7"); - IFeatureTree childTree7 = rootTree.mutate().addFeatureBelow(childFeature7); - childTree7.mutate().makeMandatory(); - - IFormula formula1 = new Or( - new And(new Literal("Test1"), new Literal("Test2")), - new BiImplies(new Literal("Test3"), new Literal("Test4")), - new Implies(new Literal("Test5"), new Literal("Test6")), - new Not(new Literal("Test7"))); - - // constraints - featureModel.mutate().addConstraint(formula1); - - ConfigurationTest.featureModel = featureModel; - ConfigurationTest.configuration = new Configuration(featureModel); - } - - @Test - public void testfeatureModelToConfigurationAsSet() { - for (IFeature feature : featureModel.getFeatures()) { - assertFalse( - configuration.getSelection(feature.getName().orElseThrow()).isEmpty()); - } - } - - @Test - public void testConfigurationCloning() { - - // configuration setup - FeatureModel featureModelForCloning = new FeatureModel(Identifiers.newCounterIdentifier()); - - // features - IFeatureTree rootTree = featureModelForCloning - .mutate() - .addFeatureTreeRoot(featureModelForCloning.mutate().addFeature("root")); - - IFeature childFeature1 = featureModelForCloning.mutate().addFeature("Test1"); - IFeatureTree childTree1 = rootTree.mutate().addFeatureBelow(childFeature1); - childTree1.mutate().toAlternativeGroup(); - - IFeature childFeature2 = featureModelForCloning.mutate().addFeature("Test2"); - IFeatureTree childTree2 = rootTree.mutate().addFeatureBelow(childFeature2); - childTree2.mutate().toOrGroup(); - - IFeature childFeature3 = featureModelForCloning.mutate().addFeature("Test3"); - IFeatureTree childTree3 = childTree1.mutate().addFeatureBelow(childFeature3); - - IFeature childFeature4 = featureModelForCloning.mutate().addFeature("Test4"); - childTree1.mutate().addFeatureBelow(childFeature4); - - IFeature childFeature5 = featureModelForCloning.mutate().addFeature("Test5"); - IFeatureTree childTree5 = childTree2.mutate().addFeatureBelow(childFeature5); - - IFeature childFeature6 = featureModelForCloning.mutate().addFeature("Test6"); - childTree2.mutate().addFeatureBelow(childFeature6); - - IFeature childFeature7 = featureModelForCloning.mutate().addFeature("Test7"); - IFeatureTree childTree7 = rootTree.mutate().addFeatureBelow(childFeature7); - childTree7.mutate().makeMandatory(); - - IFormula formula1 = new Or( - new And(new Literal("Test1"), new Literal("Test2")), - new BiImplies(new Literal("Test3"), new Literal("Test4")), - new Implies(new Literal("Test5"), new Literal("Test6")), - new Not(new Literal("Test7"))); - - // constraints - featureModelForCloning.mutate().addConstraint(formula1); - - Configuration configurationForCloning = new Configuration(featureModelForCloning); - - // selection setup - configurationForCloning.getSelection("root").orElseThrow().setManual(Boolean.TRUE); - configurationForCloning.getSelection("Test1").orElseThrow().setAutomatic(Boolean.TRUE); - configurationForCloning.getSelection("Test2").orElseThrow().setManual(Boolean.TRUE); - configurationForCloning.getSelection("Test3").orElseThrow().setAutomatic(Boolean.FALSE); - configurationForCloning.getSelection("Test4").orElseThrow().setManual(Boolean.FALSE); - - // execute clone() - Configuration clonedConfiguration = configurationForCloning.clone(); - - // check whether the selections of the clonedConfiguration are the same as for configurationForCloning - assertEquals( - Boolean.TRUE, - clonedConfiguration.getSelection("root").orElseThrow().getManual()); - assertEquals( - Boolean.TRUE, - clonedConfiguration.getSelection("Test1").orElseThrow().getAutomatic()); - assertEquals( - Boolean.TRUE, - clonedConfiguration.getSelection("Test2").orElseThrow().getManual()); - - assertEquals( - Boolean.FALSE, - clonedConfiguration.getSelection("Test3").orElseThrow().getAutomatic()); - assertEquals( - Boolean.FALSE, - clonedConfiguration.getSelection("Test4").orElseThrow().getManual()); - - assertEquals( - null, clonedConfiguration.getSelection("Test5").orElseThrow().getSelection()); - assertEquals( - null, clonedConfiguration.getSelection("Test6").orElseThrow().getSelection()); - assertEquals( - null, clonedConfiguration.getSelection("Test7").orElseThrow().getSelection()); - } - - @Test - public void testSelectionAttributesSetterAndGetterOfAutomaticAndManual() { - Selection testSelection = new Selection<>(Boolean.class); - - // Initial state - assertEquals(null, testSelection.getAutomatic()); - assertEquals(null, testSelection.getManual()); - - // Test manual selection to SELECTED - testSelection.setManual(Boolean.TRUE); - assertEquals(null, testSelection.getAutomatic()); - assertEquals(Boolean.TRUE, testSelection.getManual()); - assertThrows(SelectionNotPossibleException.class, () -> { - testSelection.setAutomatic(Boolean.FALSE); - }); - testSelection.setManual(null); - - // Test automatic selection to SELECTED - testSelection.setAutomatic(Boolean.TRUE); - assertEquals(Boolean.TRUE, testSelection.getAutomatic()); - assertEquals(null, testSelection.getManual()); - assertThrows(SelectionNotPossibleException.class, () -> { - testSelection.setManual(Boolean.FALSE); - }); - testSelection.setAutomatic(null); - - // Test manual selection to UNSELECTED - testSelection.setManual(Boolean.FALSE); - assertEquals(null, testSelection.getAutomatic()); - assertEquals(Boolean.FALSE, testSelection.getManual()); - assertThrows(SelectionNotPossibleException.class, () -> { - testSelection.setAutomatic(Boolean.TRUE); - }); - testSelection.setManual(null); - - // Test automatic selection to UNSELECTED - testSelection.setAutomatic(Boolean.FALSE); - assertEquals(Boolean.FALSE, testSelection.getAutomatic()); - assertEquals(null, testSelection.getManual()); - assertThrows(SelectionNotPossibleException.class, () -> { - testSelection.setManual(Boolean.TRUE); - }); - testSelection.setAutomatic(null); - - // Test setting both manual and automatic to SELECTED - testSelection.setManual(Boolean.TRUE); - testSelection.setAutomatic(Boolean.TRUE); - assertEquals(Boolean.TRUE, testSelection.getManual()); - assertEquals(Boolean.TRUE, testSelection.getAutomatic()); - testSelection.setManual(null); - testSelection.setAutomatic(null); - - // Test setting both manual and automatic to UNSELECTED - testSelection.setManual(Boolean.FALSE); - testSelection.setAutomatic(Boolean.FALSE); - assertEquals(Boolean.FALSE, testSelection.getManual()); - assertEquals(Boolean.FALSE, testSelection.getAutomatic()); - testSelection.setManual(null); - testSelection.setAutomatic(null); - } - - @Test - public void testSelectionAttributesSetterAndGetterOfAutomaticAndManualFromATestConfiguration() { - Configuration testConfiguration = configuration.clone(); - - assertEquals(null, testConfiguration.getSelection("Test1").orElseThrow().getAutomatic()); - assertEquals(null, testConfiguration.getSelection("Test2").orElseThrow().getManual()); - - testConfiguration.get("Test1").setAutomatic(Boolean.TRUE); - testConfiguration.get("Test2").setManual(Boolean.TRUE); - - assertEquals( - Boolean.TRUE, - testConfiguration.getSelection("Test1").orElseThrow().getAutomatic()); - assertEquals( - Boolean.TRUE, - testConfiguration.getSelection("Test2").orElseThrow().getManual()); - assertEquals( - Boolean.TRUE, - testConfiguration.getSelection("Test1").orElseThrow().getSelection()); - assertEquals( - Boolean.TRUE, - testConfiguration.getSelection("Test2").orElseThrow().getSelection()); - - testConfiguration.get("Test1").setAutomatic(Boolean.FALSE); - testConfiguration.get("Test2").setManual(Boolean.FALSE); - - assertEquals( - Boolean.FALSE, - testConfiguration.getSelection("Test1").orElseThrow().getAutomatic()); - assertEquals( - Boolean.FALSE, - testConfiguration.getSelection("Test2").orElseThrow().getManual()); - assertEquals( - Boolean.FALSE, - testConfiguration.getSelection("Test1").orElseThrow().getSelection()); - assertEquals( - Boolean.FALSE, - testConfiguration.getSelection("Test2").orElseThrow().getSelection()); - } - - @Test - public void testResetValuesShouldClearSelectionAttributes() { - - Configuration testConfiguration = configuration.clone(); - - // selection setup - testConfiguration.get("root").setManual(Boolean.TRUE); - testConfiguration.get("Test1").setAutomatic(Boolean.TRUE); - testConfiguration.get("Test2").setManual(Boolean.TRUE); - testConfiguration.get("Test3").setAutomatic(Boolean.FALSE); - testConfiguration.get("Test4").setManual(Boolean.FALSE); - - // check all SELECTED features in LinkedHashMap selectableFeatures - assertEquals(Boolean.TRUE, testConfiguration.get("root").getSelection()); - assertEquals(Boolean.TRUE, testConfiguration.get("Test1").getSelection()); - assertEquals(Boolean.TRUE, testConfiguration.get("Test2").getSelection()); - - // check all UNSELECTED features in LinkedHashMap selectableFeatures - assertEquals(Boolean.FALSE, testConfiguration.get("Test3").getSelection()); - assertEquals(Boolean.FALSE, testConfiguration.get("Test4").getSelection()); - - // check all UNDEFINED features in LinkedHashMap selectableFeatures - assertEquals(null, testConfiguration.get("Test5").getSelection()); - assertEquals(null, testConfiguration.get("Test6").getSelection()); - assertEquals(null, testConfiguration.get("Test7").getSelection()); - - // execute resetValues - testConfiguration.reset(); - - // selected, unselected and automatic features should be empty after resetValues() - assertTrue(testConfiguration.getManualFeatures().isEmpty()); - assertTrue(testConfiguration.getAutomaticFeatures().isEmpty()); - - assertEquals(null, testConfiguration.get("root").getSelection()); - assertEquals(null, testConfiguration.get("Test1").getSelection()); - assertEquals(null, testConfiguration.get("Test2").getSelection()); - assertEquals(null, testConfiguration.get("Test3").getSelection()); - assertEquals(null, testConfiguration.get("Test4").getSelection()); - assertEquals(null, testConfiguration.get("Test5").getSelection()); - assertEquals(null, testConfiguration.get("Test6").getSelection()); - assertEquals(null, testConfiguration.get("Test7").getSelection()); - } - - @Test - public void testResetAutomaticValuesShouldOnlyClearAutomaticAttributes() { - Configuration testConfiguration = configuration.clone(); - - testConfiguration.get("Test1").setAutomatic(Boolean.TRUE); - testConfiguration.get("Test2").setAutomatic(Boolean.FALSE); - testConfiguration.get("Test3").setManual(Boolean.TRUE); - testConfiguration.get("Test4").setManual(Boolean.FALSE); - - // execute resetAutomaticValues() - testConfiguration.resetAutomatic(); - - // there should be no automatic features anymore - assertTrue(testConfiguration.getAutomaticFeatures().isEmpty()); - - assertTrue(testConfiguration.get("Test1").getAutomatic() == null); - assertTrue(testConfiguration.get("Test2").getAutomatic() == null); - assertTrue(testConfiguration.get("Test3").getAutomatic() == null); - assertTrue(testConfiguration.get("Test4").getAutomatic() == null); - - assertTrue(testConfiguration.get("Test1").getManual() == null); - assertTrue(testConfiguration.get("Test2").getManual() == null); - assertTrue(testConfiguration.get("Test3").getManual() == Boolean.TRUE); - assertTrue(testConfiguration.get("Test4").getManual() == Boolean.FALSE); - } -} +/* +< * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.configuration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import de.featjar.base.data.identifier.Identifiers; +import de.featjar.feature.configuration.Configuration; +import de.featjar.feature.configuration.Configuration.Selection; +import de.featjar.feature.configuration.Configuration.SelectionNotPossibleException; +import de.featjar.feature.model.FeatureModel; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureTree; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.connective.And; +import de.featjar.formula.structure.connective.BiImplies; +import de.featjar.formula.structure.connective.Implies; +import de.featjar.formula.structure.connective.Not; +import de.featjar.formula.structure.connective.Or; +import de.featjar.formula.structure.predicate.Literal; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class ConfigurationTest { + + private static FeatureModel featureModel; + private static Configuration configuration; + + @BeforeAll + public static void setupTestConfiguration() { + + FeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + + // features + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAlternativeGroup(); + + IFeature childFeature1 = featureModel.mutate().addFeature("Test1"); + IFeatureTree childTree1 = rootTree.mutate().addFeatureBelow(childFeature1); + childTree1.mutate().toAlternativeGroup(); + + IFeature childFeature2 = featureModel.mutate().addFeature("Test2"); + IFeatureTree childTree2 = rootTree.mutate().addFeatureBelow(childFeature2); + childTree2.mutate().toOrGroup(); + + IFeature childFeature3 = featureModel.mutate().addFeature("Test3"); + IFeatureTree childTree3 = childTree1.mutate().addFeatureBelow(childFeature3); + + IFeature childFeature4 = featureModel.mutate().addFeature("Test4"); + childTree1.mutate().addFeatureBelow(childFeature4); + + IFeature childFeature5 = featureModel.mutate().addFeature("Test5"); + IFeatureTree childTree5 = childTree2.mutate().addFeatureBelow(childFeature5); + + IFeature childFeature6 = featureModel.mutate().addFeature("Test6"); + childTree2.mutate().addFeatureBelow(childFeature6); + + IFeature childFeature7 = featureModel.mutate().addFeature("Test7"); + IFeatureTree childTree7 = rootTree.mutate().addFeatureBelow(childFeature7); + childTree7.mutate().makeMandatory(); + + IFormula formula1 = new Or( + new And(new Literal("Test1"), new Literal("Test2")), + new BiImplies(new Literal("Test3"), new Literal("Test4")), + new Implies(new Literal("Test5"), new Literal("Test6")), + new Not(new Literal("Test7"))); + + // constraints + featureModel.mutate().addConstraint(formula1); + + ConfigurationTest.featureModel = featureModel; + ConfigurationTest.configuration = new Configuration(featureModel); + } + + @Test + public void testfeatureModelToConfigurationAsSet() { + for (IFeature feature : featureModel.getFeatures()) { + assertFalse( + configuration.getSelection(feature.getName().orElseThrow()).isEmpty()); + } + } + + @Test + public void testConfigurationCloning() { + + // configuration setup + FeatureModel featureModelForCloning = new FeatureModel(Identifiers.newCounterIdentifier()); + + // features + IFeatureTree rootTree = featureModelForCloning + .mutate() + .addFeatureTreeRoot(featureModelForCloning.mutate().addFeature("root")); + + IFeature childFeature1 = featureModelForCloning.mutate().addFeature("Test1"); + IFeatureTree childTree1 = rootTree.mutate().addFeatureBelow(childFeature1); + childTree1.mutate().toAlternativeGroup(); + + IFeature childFeature2 = featureModelForCloning.mutate().addFeature("Test2"); + IFeatureTree childTree2 = rootTree.mutate().addFeatureBelow(childFeature2); + childTree2.mutate().toOrGroup(); + + IFeature childFeature3 = featureModelForCloning.mutate().addFeature("Test3"); + IFeatureTree childTree3 = childTree1.mutate().addFeatureBelow(childFeature3); + + IFeature childFeature4 = featureModelForCloning.mutate().addFeature("Test4"); + childTree1.mutate().addFeatureBelow(childFeature4); + + IFeature childFeature5 = featureModelForCloning.mutate().addFeature("Test5"); + IFeatureTree childTree5 = childTree2.mutate().addFeatureBelow(childFeature5); + + IFeature childFeature6 = featureModelForCloning.mutate().addFeature("Test6"); + childTree2.mutate().addFeatureBelow(childFeature6); + + IFeature childFeature7 = featureModelForCloning.mutate().addFeature("Test7"); + IFeatureTree childTree7 = rootTree.mutate().addFeatureBelow(childFeature7); + childTree7.mutate().makeMandatory(); + + IFormula formula1 = new Or( + new And(new Literal("Test1"), new Literal("Test2")), + new BiImplies(new Literal("Test3"), new Literal("Test4")), + new Implies(new Literal("Test5"), new Literal("Test6")), + new Not(new Literal("Test7"))); + + // constraints + featureModelForCloning.mutate().addConstraint(formula1); + + Configuration configurationForCloning = new Configuration(featureModelForCloning); + + // selection setup + configurationForCloning.getSelection("root").orElseThrow().setManual(Boolean.TRUE); + configurationForCloning.getSelection("Test1").orElseThrow().setAutomatic(Boolean.TRUE); + configurationForCloning.getSelection("Test2").orElseThrow().setManual(Boolean.TRUE); + configurationForCloning.getSelection("Test3").orElseThrow().setAutomatic(Boolean.FALSE); + configurationForCloning.getSelection("Test4").orElseThrow().setManual(Boolean.FALSE); + + // execute clone() + Configuration clonedConfiguration = configurationForCloning.clone(); + + // check whether the selections of the clonedConfiguration are the same as for configurationForCloning + assertEquals( + Boolean.TRUE, + clonedConfiguration.getSelection("root").orElseThrow().getManual()); + assertEquals( + Boolean.TRUE, + clonedConfiguration.getSelection("Test1").orElseThrow().getAutomatic()); + assertEquals( + Boolean.TRUE, + clonedConfiguration.getSelection("Test2").orElseThrow().getManual()); + + assertEquals( + Boolean.FALSE, + clonedConfiguration.getSelection("Test3").orElseThrow().getAutomatic()); + assertEquals( + Boolean.FALSE, + clonedConfiguration.getSelection("Test4").orElseThrow().getManual()); + + assertEquals( + null, clonedConfiguration.getSelection("Test5").orElseThrow().getSelection()); + assertEquals( + null, clonedConfiguration.getSelection("Test6").orElseThrow().getSelection()); + assertEquals( + null, clonedConfiguration.getSelection("Test7").orElseThrow().getSelection()); + } + + @Test + public void testSelectionAttributesSetterAndGetterOfAutomaticAndManual() { + Selection testSelection = new Selection<>(Boolean.class); + + // Initial state + assertEquals(null, testSelection.getAutomatic()); + assertEquals(null, testSelection.getManual()); + + // Test manual selection to SELECTED + testSelection.setManual(Boolean.TRUE); + assertEquals(null, testSelection.getAutomatic()); + assertEquals(Boolean.TRUE, testSelection.getManual()); + assertThrows(SelectionNotPossibleException.class, () -> { + testSelection.setAutomatic(Boolean.FALSE); + }); + testSelection.setManual(null); + + // Test automatic selection to SELECTED + testSelection.setAutomatic(Boolean.TRUE); + assertEquals(Boolean.TRUE, testSelection.getAutomatic()); + assertEquals(null, testSelection.getManual()); + assertThrows(SelectionNotPossibleException.class, () -> { + testSelection.setManual(Boolean.FALSE); + }); + testSelection.setAutomatic(null); + + // Test manual selection to UNSELECTED + testSelection.setManual(Boolean.FALSE); + assertEquals(null, testSelection.getAutomatic()); + assertEquals(Boolean.FALSE, testSelection.getManual()); + assertThrows(SelectionNotPossibleException.class, () -> { + testSelection.setAutomatic(Boolean.TRUE); + }); + testSelection.setManual(null); + + // Test automatic selection to UNSELECTED + testSelection.setAutomatic(Boolean.FALSE); + assertEquals(Boolean.FALSE, testSelection.getAutomatic()); + assertEquals(null, testSelection.getManual()); + assertThrows(SelectionNotPossibleException.class, () -> { + testSelection.setManual(Boolean.TRUE); + }); + testSelection.setAutomatic(null); + + // Test setting both manual and automatic to SELECTED + testSelection.setManual(Boolean.TRUE); + testSelection.setAutomatic(Boolean.TRUE); + assertEquals(Boolean.TRUE, testSelection.getManual()); + assertEquals(Boolean.TRUE, testSelection.getAutomatic()); + testSelection.setManual(null); + testSelection.setAutomatic(null); + + // Test setting both manual and automatic to UNSELECTED + testSelection.setManual(Boolean.FALSE); + testSelection.setAutomatic(Boolean.FALSE); + assertEquals(Boolean.FALSE, testSelection.getManual()); + assertEquals(Boolean.FALSE, testSelection.getAutomatic()); + testSelection.setManual(null); + testSelection.setAutomatic(null); + } + + @Test + public void testSelectionAttributesSetterAndGetterOfAutomaticAndManualFromATestConfiguration() { + Configuration testConfiguration = configuration.clone(); + + assertEquals(null, testConfiguration.getSelection("Test1").orElseThrow().getAutomatic()); + assertEquals(null, testConfiguration.getSelection("Test2").orElseThrow().getManual()); + + testConfiguration.get("Test1").setAutomatic(Boolean.TRUE); + testConfiguration.get("Test2").setManual(Boolean.TRUE); + + assertEquals( + Boolean.TRUE, + testConfiguration.getSelection("Test1").orElseThrow().getAutomatic()); + assertEquals( + Boolean.TRUE, + testConfiguration.getSelection("Test2").orElseThrow().getManual()); + assertEquals( + Boolean.TRUE, + testConfiguration.getSelection("Test1").orElseThrow().getSelection()); + assertEquals( + Boolean.TRUE, + testConfiguration.getSelection("Test2").orElseThrow().getSelection()); + + testConfiguration.get("Test1").setAutomatic(Boolean.FALSE); + testConfiguration.get("Test2").setManual(Boolean.FALSE); + + assertEquals( + Boolean.FALSE, + testConfiguration.getSelection("Test1").orElseThrow().getAutomatic()); + assertEquals( + Boolean.FALSE, + testConfiguration.getSelection("Test2").orElseThrow().getManual()); + assertEquals( + Boolean.FALSE, + testConfiguration.getSelection("Test1").orElseThrow().getSelection()); + assertEquals( + Boolean.FALSE, + testConfiguration.getSelection("Test2").orElseThrow().getSelection()); + } + + @Test + public void testResetValuesShouldClearSelectionAttributes() { + + Configuration testConfiguration = configuration.clone(); + + // selection setup + testConfiguration.get("root").setManual(Boolean.TRUE); + testConfiguration.get("Test1").setAutomatic(Boolean.TRUE); + testConfiguration.get("Test2").setManual(Boolean.TRUE); + testConfiguration.get("Test3").setAutomatic(Boolean.FALSE); + testConfiguration.get("Test4").setManual(Boolean.FALSE); + + // check all SELECTED features in LinkedHashMap selectableFeatures + assertEquals(Boolean.TRUE, testConfiguration.get("root").getSelection()); + assertEquals(Boolean.TRUE, testConfiguration.get("Test1").getSelection()); + assertEquals(Boolean.TRUE, testConfiguration.get("Test2").getSelection()); + + // check all UNSELECTED features in LinkedHashMap selectableFeatures + assertEquals(Boolean.FALSE, testConfiguration.get("Test3").getSelection()); + assertEquals(Boolean.FALSE, testConfiguration.get("Test4").getSelection()); + + // check all UNDEFINED features in LinkedHashMap selectableFeatures + assertEquals(null, testConfiguration.get("Test5").getSelection()); + assertEquals(null, testConfiguration.get("Test6").getSelection()); + assertEquals(null, testConfiguration.get("Test7").getSelection()); + + // execute resetValues + testConfiguration.reset(); + + // selected, unselected and automatic features should be empty after resetValues() + assertTrue(testConfiguration.getManualFeatures().isEmpty()); + assertTrue(testConfiguration.getAutomaticFeatures().isEmpty()); + + assertEquals(null, testConfiguration.get("root").getSelection()); + assertEquals(null, testConfiguration.get("Test1").getSelection()); + assertEquals(null, testConfiguration.get("Test2").getSelection()); + assertEquals(null, testConfiguration.get("Test3").getSelection()); + assertEquals(null, testConfiguration.get("Test4").getSelection()); + assertEquals(null, testConfiguration.get("Test5").getSelection()); + assertEquals(null, testConfiguration.get("Test6").getSelection()); + assertEquals(null, testConfiguration.get("Test7").getSelection()); + } + + @Test + public void testResetAutomaticValuesShouldOnlyClearAutomaticAttributes() { + Configuration testConfiguration = configuration.clone(); + + testConfiguration.get("Test1").setAutomatic(Boolean.TRUE); + testConfiguration.get("Test2").setAutomatic(Boolean.FALSE); + testConfiguration.get("Test3").setManual(Boolean.TRUE); + testConfiguration.get("Test4").setManual(Boolean.FALSE); + + // execute resetAutomaticValues() + testConfiguration.resetAutomatic(); + + // there should be no automatic features anymore + assertTrue(testConfiguration.getAutomaticFeatures().isEmpty()); + + assertTrue(testConfiguration.get("Test1").getAutomatic() == null); + assertTrue(testConfiguration.get("Test2").getAutomatic() == null); + assertTrue(testConfiguration.get("Test3").getAutomatic() == null); + assertTrue(testConfiguration.get("Test4").getAutomatic() == null); + + assertTrue(testConfiguration.get("Test1").getManual() == null); + assertTrue(testConfiguration.get("Test2").getManual() == null); + assertTrue(testConfiguration.get("Test3").getManual() == Boolean.TRUE); + assertTrue(testConfiguration.get("Test4").getManual() == Boolean.FALSE); + } +} diff --git a/src/test/java/de/featjar/feature/model/io/GraphVizFeatureModelFormatTest.java b/src/test/java/de/featjar/feature/model/io/GraphVizFeatureModelFormatTest.java index 2e48d6a6..93094a4d 100644 --- a/src/test/java/de/featjar/feature/model/io/GraphVizFeatureModelFormatTest.java +++ b/src/test/java/de/featjar/feature/model/io/GraphVizFeatureModelFormatTest.java @@ -1,54 +1,54 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.io; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import de.featjar.Common; -import de.featjar.base.FeatJAR; -import de.featjar.base.io.IO; -import de.featjar.feature.model.IFeatureModel; -import de.featjar.feature.model.io.xml.GraphVizFeatureModelFormat; -import de.featjar.feature.model.io.xml.XMLFeatureModelFormat; -import java.io.IOException; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -public class GraphVizFeatureModelFormatTest extends Common { - - @BeforeAll - public static void begin() { - FeatJAR.testConfiguration().initialize(); - } - - @AfterAll - public static void end() { - FeatJAR.deinitialize(); - } - - @Test - public void graphVizFeatureModelFormat() throws IOException { - IFeatureModel featureModel = load("testFeatureModels/car.xml", new XMLFeatureModelFormat()); - String print = IO.print(featureModel, new GraphVizFeatureModelFormat()); - assertTrue(print.startsWith("digraph {")); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.io; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import de.featjar.Common; +import de.featjar.base.FeatJAR; +import de.featjar.base.io.IO; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.io.xml.GraphVizFeatureModelFormat; +import de.featjar.feature.model.io.xml.XMLFeatureModelFormat; +import java.io.IOException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class GraphVizFeatureModelFormatTest extends Common { + + @BeforeAll + public static void begin() { + FeatJAR.testConfiguration().initialize(); + } + + @AfterAll + public static void end() { + FeatJAR.deinitialize(); + } + + @Test + public void graphVizFeatureModelFormat() throws IOException { + IFeatureModel featureModel = load("testFeatureModels/car.xml", new XMLFeatureModelFormat()); + String print = IO.print(featureModel, new GraphVizFeatureModelFormat()); + assertTrue(print.startsWith("digraph {")); + } +} diff --git a/src/test/java/de/featjar/feature/model/io/XMLFeatureModelFormulaFormatTest.java b/src/test/java/de/featjar/feature/model/io/XMLFeatureModelFormulaFormatTest.java index 44b1437f..5a837088 100644 --- a/src/test/java/de/featjar/feature/model/io/XMLFeatureModelFormulaFormatTest.java +++ b/src/test/java/de/featjar/feature/model/io/XMLFeatureModelFormulaFormatTest.java @@ -1,159 +1,159 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.io; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import de.featjar.Common; -import de.featjar.base.data.Result; -import de.featjar.base.data.Sets; -import de.featjar.base.data.identifier.Identifiers; -import de.featjar.base.io.format.IFormat; -import de.featjar.base.io.input.StringInputMapper; -import de.featjar.feature.model.FeatureModel; -import de.featjar.feature.model.IFeature; -import de.featjar.feature.model.IFeatureModel; -import de.featjar.feature.model.IFeatureTree; -import de.featjar.feature.model.io.xml.XMLFeatureModelFormat; -import de.featjar.formula.structure.IFormula; -import de.featjar.formula.structure.connective.And; -import de.featjar.formula.structure.connective.BiImplies; -import de.featjar.formula.structure.connective.Implies; -import de.featjar.formula.structure.connective.Not; -import de.featjar.formula.structure.connective.Or; -import de.featjar.formula.structure.predicate.Literal; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Objects; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -public class XMLFeatureModelFormulaFormatTest extends Common { - - private static FeatureModel featureModel; - - @BeforeAll - public static void setup() { - FeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); - - // features - IFeatureTree rootTree = - featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - rootTree.mutate().toAlternativeGroup(); - - IFeature childFeature1 = featureModel.mutate().addFeature("Test1"); - IFeatureTree childTree1 = rootTree.mutate().addFeatureBelow(childFeature1); - - IFeature childFeature2 = featureModel.mutate().addFeature("Test2"); - IFeatureTree childTree2 = rootTree.mutate().addFeatureBelow(childFeature2); - childTree2.mutate().toOrGroup(); - - IFeature childFeature3 = featureModel.mutate().addFeature("Test3"); - childTree1.mutate().addFeatureBelow(childFeature3); - - IFeature childFeature4 = featureModel.mutate().addFeature("Test4"); - childTree1.mutate().addFeatureBelow(childFeature4); - - IFeature childFeature5 = featureModel.mutate().addFeature("Test5"); - childTree2.mutate().addFeatureBelow(childFeature5); - - IFeature childFeature6 = featureModel.mutate().addFeature("Test6"); - childTree2.mutate().addFeatureBelow(childFeature6); - - IFeature childFeature7 = featureModel.mutate().addFeature("Test7"); - IFeatureTree childTree7 = rootTree.mutate().addFeatureBelow(childFeature7); - childTree7.mutate().makeMandatory(); - - IFormula formula1 = new Or( - new And(new Literal("Test1"), new Literal("Test2")), - new BiImplies(new Literal("Test3"), new Literal("Test4")), - new Implies(new Literal("Test5"), new Literal("Test6")), - new Not(new Literal("Test7"))); - - // constraints - featureModel.mutate().addConstraint(formula1); - - XMLFeatureModelFormulaFormatTest.featureModel = featureModel; - } - - @Test - public void xmlFeatureModelFormat() { - IFeatureModel featureModelResult = load("testFeatureModels/car.xml", new XMLFeatureModelFormat()); - IFeatureModel featureModel = featureModelResult; - String[] featureNames = new String[] { - "Car", - "Carbody", - "Radio", - "Ports", - "USB", - "CD", - "Navigation", - "DigitalCards", - "Europe", - "USA", - "GPSAntenna", - "Bluetooth", - "Gearbox", - "Manual", - "Automatic", - "GearboxTest" - }; - assertEquals( - Sets.of(featureNames), - featureModel.getFeatures().stream() - .map(IFeature::getName) - .map(Result::get) - .collect(Sets.toSet())); - } - - @Test - void testXMLFileToFeatureModelToXMLFile() throws IOException { - parseAndSerialize(new XMLFeatureModelFormat(), "testFeatureModels/car.xml"); - } - - private void parseAndSerialize(IFormat format, String path) { - IFeatureModel fm = Common.load(path, format); - - Result serializeResult = format.serialize(fm); - assertTrue(serializeResult.isPresent(), "Serialization failed"); - } - - // TODO: Need to assert objects.equals for each featuremodel.element instead of for the featuremodel itself. - - // @Test - void testFeatureModelToXMLStringToFeatureModel() throws IOException { - IFormat format = new XMLFeatureModelFormat(); - - Result serializedResult = format.serialize(featureModel); - Assertions.assertTrue(serializedResult.isPresent(), "Serialization of IFeatureModel failed"); - - String serializedFeatureModel = serializedResult.get(); - Result parseResult = - format.parse(new StringInputMapper(serializedFeatureModel, StandardCharsets.UTF_8, "xml")); - - Assertions.assertTrue(parseResult.isPresent(), parseResult.printProblems()); - IFeatureModel parsedFeatureModel = parseResult.get(); - - Assertions.assertTrue(Objects.equals(parsedFeatureModel, featureModel)); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.io; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import de.featjar.Common; +import de.featjar.base.data.Result; +import de.featjar.base.data.Sets; +import de.featjar.base.data.identifier.Identifiers; +import de.featjar.base.io.format.IFormat; +import de.featjar.base.io.input.StringInputMapper; +import de.featjar.feature.model.FeatureModel; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.IFeatureTree; +import de.featjar.feature.model.io.xml.XMLFeatureModelFormat; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.connective.And; +import de.featjar.formula.structure.connective.BiImplies; +import de.featjar.formula.structure.connective.Implies; +import de.featjar.formula.structure.connective.Not; +import de.featjar.formula.structure.connective.Or; +import de.featjar.formula.structure.predicate.Literal; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class XMLFeatureModelFormulaFormatTest extends Common { + + private static FeatureModel featureModel; + + @BeforeAll + public static void setup() { + FeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + + // features + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAlternativeGroup(); + + IFeature childFeature1 = featureModel.mutate().addFeature("Test1"); + IFeatureTree childTree1 = rootTree.mutate().addFeatureBelow(childFeature1); + + IFeature childFeature2 = featureModel.mutate().addFeature("Test2"); + IFeatureTree childTree2 = rootTree.mutate().addFeatureBelow(childFeature2); + childTree2.mutate().toOrGroup(); + + IFeature childFeature3 = featureModel.mutate().addFeature("Test3"); + childTree1.mutate().addFeatureBelow(childFeature3); + + IFeature childFeature4 = featureModel.mutate().addFeature("Test4"); + childTree1.mutate().addFeatureBelow(childFeature4); + + IFeature childFeature5 = featureModel.mutate().addFeature("Test5"); + childTree2.mutate().addFeatureBelow(childFeature5); + + IFeature childFeature6 = featureModel.mutate().addFeature("Test6"); + childTree2.mutate().addFeatureBelow(childFeature6); + + IFeature childFeature7 = featureModel.mutate().addFeature("Test7"); + IFeatureTree childTree7 = rootTree.mutate().addFeatureBelow(childFeature7); + childTree7.mutate().makeMandatory(); + + IFormula formula1 = new Or( + new And(new Literal("Test1"), new Literal("Test2")), + new BiImplies(new Literal("Test3"), new Literal("Test4")), + new Implies(new Literal("Test5"), new Literal("Test6")), + new Not(new Literal("Test7"))); + + // constraints + featureModel.mutate().addConstraint(formula1); + + XMLFeatureModelFormulaFormatTest.featureModel = featureModel; + } + + @Test + public void xmlFeatureModelFormat() { + IFeatureModel featureModelResult = load("testFeatureModels/car.xml", new XMLFeatureModelFormat()); + IFeatureModel featureModel = featureModelResult; + String[] featureNames = new String[] { + "Car", + "Carbody", + "Radio", + "Ports", + "USB", + "CD", + "Navigation", + "DigitalCards", + "Europe", + "USA", + "GPSAntenna", + "Bluetooth", + "Gearbox", + "Manual", + "Automatic", + "GearboxTest" + }; + assertEquals( + Sets.of(featureNames), + featureModel.getFeatures().stream() + .map(IFeature::getName) + .map(Result::get) + .collect(Sets.toSet())); + } + + @Test + void testXMLFileToFeatureModelToXMLFile() throws IOException { + parseAndSerialize(new XMLFeatureModelFormat(), "testFeatureModels/car.xml"); + } + + private void parseAndSerialize(IFormat format, String path) { + IFeatureModel fm = Common.load(path, format); + + Result serializeResult = format.serialize(fm); + assertTrue(serializeResult.isPresent(), "Serialization failed"); + } + + // TODO: Need to assert objects.equals for each featuremodel.element instead of for the featuremodel itself. + + // @Test + void testFeatureModelToXMLStringToFeatureModel() throws IOException { + IFormat format = new XMLFeatureModelFormat(); + + Result serializedResult = format.serialize(featureModel); + Assertions.assertTrue(serializedResult.isPresent(), "Serialization of IFeatureModel failed"); + + String serializedFeatureModel = serializedResult.get(); + Result parseResult = + format.parse(new StringInputMapper(serializedFeatureModel, StandardCharsets.UTF_8, "xml")); + + Assertions.assertTrue(parseResult.isPresent(), parseResult.printProblems()); + IFeatureModel parsedFeatureModel = parseResult.get(); + + Assertions.assertTrue(Objects.equals(parsedFeatureModel, featureModel)); + } +} diff --git a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java index 5dd1b8d8..341155ba 100644 --- a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java +++ b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java @@ -1,10 +1,27 @@ +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ package de.featjar.feature.model.transformer; import static org.junit.jupiter.api.Assertions.assertEquals; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - import de.featjar.base.computation.ComputeConstant; import de.featjar.base.data.identifier.Identifiers; import de.featjar.feature.model.FeatureModel; @@ -16,62 +33,58 @@ import de.featjar.formula.structure.connective.Implies; import de.featjar.formula.structure.connective.Reference; import de.featjar.formula.structure.predicate.Literal; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; class ComputeFormulaTest { - private IFeatureModel featureModel; - private IFormula expected; - + private IFeatureModel featureModel; + private IFormula expected; + @BeforeEach public void createFeatureModel() { featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); - } - - @Test - void onlyRoot() { - - // root and nothing else - featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - - // root must be selected - expected = new Reference(new And( - new Literal("root") - )); - - executeTest(); - } - - @Test - void oneFeature () { - - // root - IFeatureTree rootTree = - featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - rootTree.mutate().toAndGroup(); - - // TODO: check if setting root feature is missing here or if compute misses adding root feature literal - - // create and add our only child - IFeature childFeature = featureModel.mutate().addFeature("Test1"); - rootTree.mutate().addFeatureBelow(childFeature); - - // TODO: check order if bug is fixed - expected = new Reference( new And ( - new Literal ("root"), - new Implies( new Literal("Test1") , new Literal ("root") ) - )); - + + @Test + void onlyRoot() { + + // root and nothing else + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + + // root must be selected + expected = new Reference(new And(new Literal("root"))); + + executeTest(); + } + + @Test + void oneFeature() { + + // root + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAndGroup(); + + // TODO: check if setting root feature is missing here or if compute misses adding root feature literal + + // create and add our only child + IFeature childFeature = featureModel.mutate().addFeature("Test1"); + rootTree.mutate().addFeatureBelow(childFeature); + + // TODO: check order if bug is fixed + expected = new Reference(new And(new Literal("root"), new Implies(new Literal("Test1"), new Literal("root")))); + executeTest(); - } - - private void executeTest() { - - ComputeConstant computeConstant = new ComputeConstant(featureModel); - ComputeFormula computeFormula = new ComputeFormula(computeConstant); - - IFormula resultFormula = computeFormula.computeResult().get(); - - // assert - assertEquals(expected, resultFormula); - } + } + + private void executeTest() { + + ComputeConstant computeConstant = new ComputeConstant(featureModel); + ComputeFormula computeFormula = new ComputeFormula(computeConstant); + + IFormula resultFormula = computeFormula.computeResult().get(); + + // assert + assertEquals(expected, resultFormula); + } } From fe41b3da999eba3852c18dbcb3e979038b41b959 Mon Sep 17 00:00:00 2001 From: Klara Surmeier Date: Wed, 1 Oct 2025 16:38:57 +0200 Subject: [PATCH 06/67] ComputeFormula:Draft: feature cardinality to cardinality group --- .../feature/model/transformer/ComputeFormula.java | 13 ++++++++++++- .../model/transformer/ComputeFormulaTest.java | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java index 6f05920d..6be6386a 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java @@ -79,9 +79,14 @@ public Result compute(List dependencyList, Progress progress) Literal featureLiteral = Expressions.literal(featureName); if (potentialParentTree.isEmpty()) { handleRoot(constraints, featureLiteral, node); - } else { + } + else { handleParent(constraints, featureLiteral, node); } + + if(node.getFeatureCardinalityUpperBound() > 1) { + handleCardinalityFeature(constraints, featureLiteral, node); + } handleGroups(constraints, featureLiteral, node); }); Reference reference = new Reference(new And(constraints)); @@ -101,6 +106,12 @@ private void handleRoot(ArrayList constraints, Literal featureLiteral, constraints.add(featureLiteral); } } + + private void handleCardinalityFeature(ArrayList constraints, Literal featureLiteral, IFeatureTree node) { + // create literals for every case in range (if range goes further than one) + + + } private void handleGroups(ArrayList constraints, Literal featureLiteral, IFeatureTree node) { List childrenGroups = node.getChildrenGroups(); diff --git a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java index 341155ba..0e9a1a51 100644 --- a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java +++ b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java @@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import de.featjar.base.computation.ComputeConstant; +import de.featjar.base.data.Range; import de.featjar.base.data.identifier.Identifiers; import de.featjar.feature.model.FeatureModel; import de.featjar.feature.model.IFeature; @@ -76,6 +77,20 @@ void oneFeature() { executeTest(); } + + @Test + void withCardinality() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAndGroup(); + + IFeature childFeature = featureModel.mutate().addFeature("Test1"); + + // set cardinality for the child feature + rootTree.mutate().addFeatureBelow(childFeature).mutate().setFeatureCardinality(Range.of(0, 2)); + + executeTest(); + } private void executeTest() { From 5db8f4bc40d7875137218a222f8a2f4bf809cd9c Mon Sep 17 00:00:00 2001 From: Malena Horstmann Date: Thu, 2 Oct 2025 13:58:06 +0200 Subject: [PATCH 07/67] Unit Test for cardinality group --- .../model/transformer/ComputeFormulaTest.java | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java index 0e9a1a51..859bac02 100644 --- a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java +++ b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java @@ -79,14 +79,31 @@ void oneFeature() { } @Test - void withCardinality() { + void withCardinalityGroup() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toCardinalityGroup(Range.of(2, 3)); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("Test1"); + rootTree.mutate().addFeatureBelow(childFeature1); + + IFeature childFeature2 = featureModel.mutate().addFeature("Test2"); + rootTree.mutate().addFeatureBelow(childFeature2); + +// childTree1.mutate().toCardinalityGroup(Range.of(2, 3)); + + executeTest(); + } + + @Test + void withCardinalityFeature() { IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); rootTree.mutate().toAndGroup(); + // create and set cardinality for the child feature IFeature childFeature = featureModel.mutate().addFeature("Test1"); - - // set cardinality for the child feature rootTree.mutate().addFeatureBelow(childFeature).mutate().setFeatureCardinality(Range.of(0, 2)); executeTest(); From 218d260fb3faf8118f84e32a7b451d56351e2d1b Mon Sep 17 00:00:00 2001 From: Lara Date: Thu, 2 Oct 2025 15:52:22 +0200 Subject: [PATCH 08/67] feat:translate int and floatt --- .../model/transformer/ComputeFormula.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java index dcacf5bb..835b015a 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java @@ -74,6 +74,25 @@ public Result compute(List dependencyList, Progress progress) Variable variable = new Variable(featureName, feature.getType()); variables.add(variable); + if (feature.getType().equals(Boolean.class)) { + IFormula featureFormula = Expressions.literal(featureName); + + Result potentialParentTree = node.getParent; + if(potentialParentTree.isEmpty()) { + handleRoot(constraints, featureFormula, node); + } else{ + handleParent(constraints, featureFormula, node); + } + handleGroups(constraints, featureFormula, node); + } else if (feature.getType.equals(Integer.class)) { + IFormula featureLiteral = new NotEquals(variable, new Constant(0)); + constraint.add(featureLiteral); + } else if(feature.getType.equals(float.class) { + IFormula featureLiteral = new NotEquals(variable, new Constant(0.0)); + constraint.add(featureLiteral); + } + + // TODO take featureRanges into Account Result potentialParentTree = node.getParent(); Literal featureLiteral = Expressions.literal(featureName); From a406dc66d44dc768b16c2aab397a33ea0ca4353a Mon Sep 17 00:00:00 2001 From: Lara Date: Thu, 2 Oct 2025 16:31:59 +0200 Subject: [PATCH 09/67] fix:translate int and float --- .../model/transformer/ComputeFormula.java | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java index 835b015a..9d2d76e0 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java @@ -73,26 +73,17 @@ public Result compute(List dependencyList, Progress progress) String featureName = feature.getName().orElse(""); Variable variable = new Variable(featureName, feature.getType()); variables.add(variable); - - if (feature.getType().equals(Boolean.class)) { - IFormula featureFormula = Expressions.literal(featureName); - - Result potentialParentTree = node.getParent; - if(potentialParentTree.isEmpty()) { - handleRoot(constraints, featureFormula, node); - } else{ - handleParent(constraints, featureFormula, node); - } - handleGroups(constraints, featureFormula, node); + IFormula featureLiteral; + if (feature.getType().equals(Boolean.class)) { + featureLiteral = Expressions.literal(featureName); } else if (feature.getType.equals(Integer.class)) { - IFormula featureLiteral = new NotEquals(variable, new Constant(0)); - constraint.add(featureLiteral); - } else if(feature.getType.equals(float.class) { - IFormula featureLiteral = new NotEquals(variable, new Constant(0.0)); - constraint.add(featureLiteral); + featureLiteral = new NotEquals(variable, new Constant(0)); + } else if(feature.getType.equals(Float.class) { + featureLiteral = new NotEquals(variable, new Constant(0.0)); + } else { + FeatJAR.log().warning("Could not handle type "+ feature.getType()); + return; } - - // TODO take featureRanges into Account Result potentialParentTree = node.getParent(); Literal featureLiteral = Expressions.literal(featureName); @@ -105,7 +96,7 @@ public Result compute(List dependencyList, Progress progress) }); Reference reference = new Reference(new And(constraints)); reference.setFreeVariables(variables); - return Result.of(reference); + return Result.of(reference, problemList); } private void handleParent(ArrayList constraints, Literal featureLiteral, IFeatureTree node) { From cec5e10dee3f18e35940ee86662cf33e3bdc6b71 Mon Sep 17 00:00:00 2001 From: Malena Horstmann Date: Thu, 2 Oct 2025 16:44:09 +0200 Subject: [PATCH 10/67] ComputeFormula: Added Visitor for implication chain and cardinality group constraints --- .../model/transformer/CardinalityObject.java | 21 +++ .../model/transformer/ComputeFormula.java | 139 ++++---------- .../transformer/ComputeFormulaVisitor.java | 176 ++++++++++++++++++ .../model/transformer/ComputeFormulaTest.java | 5 +- 4 files changed, 235 insertions(+), 106 deletions(-) create mode 100644 src/main/java/de/featjar/feature/model/transformer/CardinalityObject.java create mode 100644 src/main/java/de/featjar/feature/model/transformer/ComputeFormulaVisitor.java diff --git a/src/main/java/de/featjar/feature/model/transformer/CardinalityObject.java b/src/main/java/de/featjar/feature/model/transformer/CardinalityObject.java new file mode 100644 index 00000000..5b918a92 --- /dev/null +++ b/src/main/java/de/featjar/feature/model/transformer/CardinalityObject.java @@ -0,0 +1,21 @@ +package de.featjar.feature.model.transformer; + +public class CardinalityObject { + + String name; + int number; + + public CardinalityObject(String name, int number) { + + this.name = name; + this.number = number; + } + + public String getName() { + return name; + } + + public int getNumber() { + return number; + } +} diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java index 6be6386a..0f844496 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java @@ -20,31 +20,22 @@ */ package de.featjar.feature.model.transformer; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + import de.featjar.base.computation.AComputation; import de.featjar.base.computation.Dependency; import de.featjar.base.computation.IComputation; import de.featjar.base.computation.Progress; -import de.featjar.base.data.Range; import de.featjar.base.data.Result; -import de.featjar.feature.model.FeatureTree.Group; -import de.featjar.feature.model.IFeature; +import de.featjar.base.tree.Trees; import de.featjar.feature.model.IFeatureModel; import de.featjar.feature.model.IFeatureTree; -import de.featjar.formula.structure.Expressions; import de.featjar.formula.structure.IFormula; import de.featjar.formula.structure.connective.And; -import de.featjar.formula.structure.connective.AtLeast; -import de.featjar.formula.structure.connective.AtMost; -import de.featjar.formula.structure.connective.Between; -import de.featjar.formula.structure.connective.Choose; -import de.featjar.formula.structure.connective.Implies; -import de.featjar.formula.structure.connective.Or; import de.featjar.formula.structure.connective.Reference; -import de.featjar.formula.structure.predicate.Literal; import de.featjar.formula.structure.term.value.Variable; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; /** * Transforms a feature model into a boolean formula. @@ -52,7 +43,7 @@ * @author Sebastian Krieter */ public class ComputeFormula extends AComputation { - protected static final Dependency FEATURE_MODEL = Dependency.newDependency(IFeatureModel.class); + protected static final Dependency FEATURE_MODEL = Dependency.newDependency(IFeatureModel.class); public ComputeFormula(IComputation formula) { super(formula); @@ -61,105 +52,43 @@ public ComputeFormula(IComputation formula) { protected ComputeFormula(ComputeFormula other) { super(other); } + + @Override public Result compute(List dependencyList, Progress progress) { IFeatureModel featureModel = FEATURE_MODEL.get(dependencyList); ArrayList constraints = new ArrayList<>(); HashSet variables = new HashSet<>(); - featureModel.getFeatureTreeStream().forEach(node -> { - // TODO use better error value - IFeature feature = node.getFeature(); - String featureName = feature.getName().orElse(""); - Variable variable = new Variable(featureName, feature.getType()); - variables.add(variable); - - // TODO take featureRanges into Account - Result potentialParentTree = node.getParent(); - Literal featureLiteral = Expressions.literal(featureName); - if (potentialParentTree.isEmpty()) { - handleRoot(constraints, featureLiteral, node); - } - else { - handleParent(constraints, featureLiteral, node); - } - - if(node.getFeatureCardinalityUpperBound() > 1) { - handleCardinalityFeature(constraints, featureLiteral, node); - } - handleGroups(constraints, featureLiteral, node); - }); + + IFeatureTree iFeatureTree = featureModel.getRoots().get(0); + Trees.traverse(iFeatureTree, new ComputeFormulaVisitor(constraints, variables)); + +// featureModel.getFeatureTreeStream().forEach(node -> { +// // TODO use better error value +// IFeature feature = node.getFeature(); +// String featureName = feature.getName().orElse(""); +// Variable variable = new Variable(featureName, feature.getType()); +// variables.add(variable); +// +// // TODO take featureRanges into Account +// Result potentialParentTree = node.getParent(); +// Literal featureLiteral = Expressions.literal(featureName); +// if (potentialParentTree.isEmpty()) { +// handleRoot(constraints, featureLiteral, node); +// } +// else { +// handleParent(constraints, featureLiteral, node); +// } +// +// if(node.getFeatureCardinalityUpperBound() > 1) { +// handleCardinalityFeature(constraints, featureLiteral, node); +// } +// handleGroups(constraints, featureLiteral, node); +// }); Reference reference = new Reference(new And(constraints)); reference.setFreeVariables(variables); return Result.of(reference); } - private void handleParent(ArrayList constraints, Literal featureLiteral, IFeatureTree node) { - constraints.add(new Implies( - featureLiteral, - Expressions.literal( - node.getParent().get().getFeature().getName().orElse("")))); - } - - private void handleRoot(ArrayList constraints, Literal featureLiteral, IFeatureTree node) { - if (node.isMandatory()) { - constraints.add(featureLiteral); - } - } - - private void handleCardinalityFeature(ArrayList constraints, Literal featureLiteral, IFeatureTree node) { - // create literals for every case in range (if range goes further than one) - - - } - - private void handleGroups(ArrayList constraints, Literal featureLiteral, IFeatureTree node) { - List childrenGroups = node.getChildrenGroups(); - int groupCount = childrenGroups.size(); - ArrayList> groupLiterals = new ArrayList<>(groupCount); - for (int i = 0; i < groupCount; i++) { - groupLiterals.add(null); - } - List children = node.getChildren(); - for (IFeatureTree childNode : children) { - Literal childLiteral = - Expressions.literal(childNode.getFeature().getName().orElse("")); - - if (childNode.isMandatory()) { - constraints.add(new Implies(featureLiteral, childLiteral)); - } - - int groupID = childNode.getParentGroupID(); - List list = groupLiterals.get(groupID); - if (list == null) { - groupLiterals.set(groupID, list = new ArrayList<>()); - } - list.add(childLiteral); - } - for (int i = 0; i < groupCount; i++) { - Group group = childrenGroups.get(i); - if (group != null) { - if (group.isOr()) { - constraints.add(new Implies(featureLiteral, new Or(groupLiterals.get(i)))); - } else if (group.isAlternative()) { - constraints.add(new Implies(featureLiteral, new Choose(1, groupLiterals.get(i)))); - } else { - int lowerBound = group.getLowerBound(); - int upperBound = group.getUpperBound(); - if (lowerBound > 0) { - if (upperBound != Range.OPEN) { - constraints.add(new Implies( - featureLiteral, new Between(lowerBound, upperBound, groupLiterals.get(i)))); - } else { - constraints.add(new Implies(featureLiteral, new AtMost(upperBound, groupLiterals.get(i)))); - } - } else { - if (upperBound != Range.OPEN) { - constraints.add(new Implies(featureLiteral, new AtLeast(lowerBound, groupLiterals.get(i)))); - } - } - } - } - } - } } diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormulaVisitor.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormulaVisitor.java new file mode 100644 index 00000000..1e25ad4a --- /dev/null +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormulaVisitor.java @@ -0,0 +1,176 @@ +package de.featjar.feature.model.transformer; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Stack; + +import de.featjar.base.data.Range; +import de.featjar.base.data.Result; +import de.featjar.base.tree.visitor.ITreeVisitor; +import de.featjar.feature.model.FeatureTree.Group; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureTree; +import de.featjar.formula.structure.Expressions; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.connective.AtLeast; +import de.featjar.formula.structure.connective.AtMost; +import de.featjar.formula.structure.connective.Between; +import de.featjar.formula.structure.connective.Choose; +import de.featjar.formula.structure.connective.Implies; +import de.featjar.formula.structure.connective.Or; +import de.featjar.formula.structure.predicate.Literal; +import de.featjar.formula.structure.term.value.Variable; + +public class ComputeFormulaVisitor implements ITreeVisitor { + + protected ArrayList constraints = new ArrayList<>(); + protected HashSet variables = new HashSet<>(); + protected Stack cardinalityStack = new Stack<>(); + + public ComputeFormulaVisitor(ArrayList constraints, HashSet variables) { + + this.constraints = constraints; + this.variables = variables; + } + + @Override + public TraversalAction firstVisit(List path) { + IFeatureTree node = ITreeVisitor.getCurrentNode(path); + + // TODO use better error value + IFeature feature = node.getFeature(); + String featureName = feature.getName().orElse(""); + Variable variable = new Variable(featureName, feature.getType()); + variables.add(variable); + + // TODO take featureRanges into Account + Result potentialParentTree = node.getParent(); + Literal featureLiteral = Expressions.literal(featureName); + if (potentialParentTree.isEmpty()) { + handleRoot(featureLiteral, node); + } + else if(node.getFeatureCardinalityUpperBound() > 1) { + handleCardinalityFeature(featureLiteral, node); + } + else{ + handleParent(featureLiteral, node); + } + + handleGroups(featureLiteral, node); + + + return ITreeVisitor.super.firstVisit(path); + } + + @Override + public TraversalAction lastVisit(List path) { + // TODO Auto-generated method stub + return ITreeVisitor.super.lastVisit(path); + } + + private void handleParent(Literal featureLiteral, IFeatureTree node) { + constraints.add(new Implies( + featureLiteral, + Expressions.literal( + node.getParent().get().getFeature().getName().orElse("")))); + } + + private void handleRoot(Literal featureLiteral, IFeatureTree node) { + if (node.isMandatory()) { + constraints.add(featureLiteral); + } + } + + private void handleCardinalityFeature(Literal featureLiteral, IFeatureTree node) { + + // step 1: add cardinality feature to stack: + cardinalityStack.add(new CardinalityObject(node.getFeature().getName().get(), node.getFeatureCardinalityUpperBound())); + + int lowerBound = node.getFeatureCardinalityLowerBound(); + int upperBound = node.getFeatureCardinalityUpperBound(); + + ArrayList featureList = new ArrayList(); + + // step 2: add literals and implication to parent + String literalName = ""; + for (int i = 1; i<= upperBound; i++) { + + literalName = node.getFeature().getName().get() + "_" + i; + featureLiteral = new Literal(literalName); + handleParent(featureLiteral, node); + + if (i > 1) { + // step 3: add to implication chain + IFormula previousLiteral = featureList.get(featureList.size()-1); + constraints.add(new Implies(featureLiteral, previousLiteral)); + } + + featureList.add(featureLiteral); + } + + // step 4: add cardinality constraint + if (lowerBound > 0) { + if (upperBound != Range.OPEN) { + constraints.add(new Implies( + featureLiteral, new Between(lowerBound, upperBound, featureList))); + } else { + constraints.add(new Implies(featureLiteral, new AtMost(upperBound, featureList))); + } + } else { + if (upperBound != Range.OPEN) { + constraints.add(new Implies(featureLiteral, new AtLeast(lowerBound, featureList))); + } + } + } + + private void handleGroups(Literal featureLiteral, IFeatureTree node) { + List childrenGroups = node.getChildrenGroups(); + int groupCount = childrenGroups.size(); + ArrayList> groupLiterals = new ArrayList<>(groupCount); + for (int i = 0; i < groupCount; i++) { + groupLiterals.add(null); + } + List children = node.getChildren(); + for (IFeatureTree childNode : children) { + Literal childLiteral = + Expressions.literal(childNode.getFeature().getName().orElse("")); + + if (childNode.isMandatory()) { + constraints.add(new Implies(featureLiteral, childLiteral)); + } + + int groupID = childNode.getParentGroupID(); + List list = groupLiterals.get(groupID); + if (list == null) { + groupLiterals.set(groupID, list = new ArrayList<>()); + } + list.add(childLiteral); + } + for (int i = 0; i < groupCount; i++) { + Group group = childrenGroups.get(i); + if (group != null) { + if (group.isOr()) { + constraints.add(new Implies(featureLiteral, new Or(groupLiterals.get(i)))); + } else if (group.isAlternative()) { + constraints.add(new Implies(featureLiteral, new Choose(1, groupLiterals.get(i)))); + } else { + int lowerBound = group.getLowerBound(); + int upperBound = group.getUpperBound(); + if (lowerBound > 0) { + if (upperBound != Range.OPEN) { + constraints.add(new Implies( + featureLiteral, new Between(lowerBound, upperBound, groupLiterals.get(i)))); + } else { + constraints.add(new Implies(featureLiteral, new AtMost(upperBound, groupLiterals.get(i)))); + } + } else { + if (upperBound != Range.OPEN) { + constraints.add(new Implies(featureLiteral, new AtLeast(lowerBound, groupLiterals.get(i)))); + } + } + } + } + } + } +} diff --git a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java index 859bac02..1e603ff6 100644 --- a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java +++ b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java @@ -91,7 +91,10 @@ void withCardinalityGroup() { IFeature childFeature2 = featureModel.mutate().addFeature("Test2"); rootTree.mutate().addFeatureBelow(childFeature2); -// childTree1.mutate().toCardinalityGroup(Range.of(2, 3)); + IFeature childFeature3 = featureModel.mutate().addFeature("Test3"); + rootTree.mutate().addFeatureBelow(childFeature3); + + executeTest(); } From 6665d8f4387d9a05da5284a10b9c95d8f6ea0d9d Mon Sep 17 00:00:00 2001 From: Klara Surmeier Date: Mon, 6 Oct 2025 10:01:22 +0200 Subject: [PATCH 11/67] ComputeFromula:Draft: extension to child feature of cardinality feature --- .../transformer/ComputeFormulaVisitor.java | 120 +++++++++++++----- .../model/transformer/ComputeFormulaTest.java | 21 ++- 2 files changed, 105 insertions(+), 36 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormulaVisitor.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormulaVisitor.java index 1e25ad4a..81c7b58b 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormulaVisitor.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormulaVisitor.java @@ -41,6 +41,7 @@ public TraversalAction firstVisit(List path) { // TODO use better error value IFeature feature = node.getFeature(); String featureName = feature.getName().orElse(""); + // TODO: do not add variable if its a cardinality var. Add duplicates instead Variable variable = new Variable(featureName, feature.getType()); variables.add(variable); @@ -84,44 +85,93 @@ private void handleRoot(Literal featureLiteral, IFeatureTree node) { private void handleCardinalityFeature(Literal featureLiteral, IFeatureTree node) { - // step 1: add cardinality feature to stack: - cardinalityStack.add(new CardinalityObject(node.getFeature().getName().get(), node.getFeatureCardinalityUpperBound())); - - int lowerBound = node.getFeatureCardinalityLowerBound(); - int upperBound = node.getFeatureCardinalityUpperBound(); - - ArrayList featureList = new ArrayList(); - - // step 2: add literals and implication to parent - String literalName = ""; - for (int i = 1; i<= upperBound; i++) { - - literalName = node.getFeature().getName().get() + "_" + i; - featureLiteral = new Literal(literalName); - handleParent(featureLiteral, node); - - if (i > 1) { - // step 3: add to implication chain - IFormula previousLiteral = featureList.get(featureList.size()-1); - constraints.add(new Implies(featureLiteral, previousLiteral)); - } + // step 0: check stack + if (cardinalityStack.empty()) { + int lowerBound = node.getFeatureCardinalityLowerBound(); + int upperBound = node.getFeatureCardinalityUpperBound(); + + ArrayList featureList = new ArrayList(); + + // step 2: add literals and implication to parent + String literalName = ""; + for (int i = 1; i<= upperBound; i++) { + + literalName = node.getFeature().getName().get() + "_" + i; + featureLiteral = new Literal(literalName); + handleParent(featureLiteral, node); + + if (i > 1) { + // step 3: add to implication chain + IFormula previousLiteral = featureList.get(featureList.size()-1); + constraints.add(new Implies(featureLiteral, previousLiteral)); + } + + featureList.add(featureLiteral); + } + + // step 4: add cardinality constraint + // TODO: feature literal must be parent! + if (lowerBound > 0) { + if (upperBound != Range.OPEN) { + constraints.add(new Implies( + featureLiteral, new Between(lowerBound, upperBound, featureList))); + } else { + constraints.add(new Implies(featureLiteral, new AtMost(upperBound, featureList))); + } + } else { + if (upperBound != Range.OPEN) { + constraints.add(new Implies(featureLiteral, new AtLeast(lowerBound, featureList))); + } + } + } else { + int lowerBound = node.getFeatureCardinalityLowerBound(); + int upperBound = node.getFeatureCardinalityUpperBound(); - featureList.add(featureLiteral); + CardinalityObject parentCardObject = cardinalityStack.pop(); + int parentUpperBound = parentCardObject.getNumber(); + + String literalName = ""; + for (int i = 1; i <= parentUpperBound; i++) { + + ArrayList featureList = new ArrayList(); + for (int j = 1; j <= upperBound; j++) { + + literalName = node.getFeature().getName().get() + "_" + i + "_" + j; + featureLiteral = new Literal(literalName); + handleParent(featureLiteral, node); + + if (j > 1) { + // step 3: add to implication chain + IFormula previousLiteral = featureList.get(featureList.size()-1); + constraints.add(new Implies(featureLiteral, previousLiteral)); + } + + featureList.add(featureLiteral); + } + + // step 4: add cardinality constraint + if (lowerBound > 0) { + if (upperBound != Range.OPEN) { + constraints.add(new Implies( + featureLiteral, new Between(lowerBound, upperBound, featureList))); + } else { + constraints.add(new Implies(featureLiteral, new AtMost(upperBound, featureList))); + } + } else { + if (upperBound != Range.OPEN) { + constraints.add(new Implies(featureLiteral, new AtLeast(lowerBound, featureList))); + } + } + + } + } - // step 4: add cardinality constraint - if (lowerBound > 0) { - if (upperBound != Range.OPEN) { - constraints.add(new Implies( - featureLiteral, new Between(lowerBound, upperBound, featureList))); - } else { - constraints.add(new Implies(featureLiteral, new AtMost(upperBound, featureList))); - } - } else { - if (upperBound != Range.OPEN) { - constraints.add(new Implies(featureLiteral, new AtLeast(lowerBound, featureList))); - } - } + + // step 1: add cardinality feature to stack: + cardinalityStack.add(new CardinalityObject(node.getFeature().getName().get(), node.getFeatureCardinalityUpperBound())); + + } private void handleGroups(Literal featureLiteral, IFeatureTree node) { diff --git a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java index 1e603ff6..4ab8036e 100644 --- a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java +++ b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java @@ -100,7 +100,7 @@ void withCardinalityGroup() { } @Test - void withCardinalityFeature() { + void withOneCardinalityFeature() { IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); rootTree.mutate().toAndGroup(); @@ -111,6 +111,25 @@ void withCardinalityFeature() { executeTest(); } + + @Test + void withTwoCardinalityFeatures() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("Test1"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + IFeature childFeature2 = featureModel.mutate().addFeature("Test2"); + IFeatureTree childFeature2Tree = childFeature1Tree.mutate().addFeatureBelow(childFeature2); + childFeature2Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + + executeTest(); + } private void executeTest() { From aaf3fca84806362a54bfd1017dcfa7f01ff5a591 Mon Sep 17 00:00:00 2001 From: Lara Date: Mon, 6 Oct 2025 10:16:50 +0200 Subject: [PATCH 12/67] fix: add missing parentheses for getType() calls git status git --- .../model/transformer/ComputeFormula.java | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java index 9d2d76e0..757e5edb 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java @@ -73,17 +73,20 @@ public Result compute(List dependencyList, Progress progress) String featureName = feature.getName().orElse(""); Variable variable = new Variable(featureName, feature.getType()); variables.add(variable); - IFormula featureLiteral; - if (feature.getType().equals(Boolean.class)) { - featureLiteral = Expressions.literal(featureName); - } else if (feature.getType.equals(Integer.class)) { - featureLiteral = new NotEquals(variable, new Constant(0)); - } else if(feature.getType.equals(Float.class) { - featureLiteral = new NotEquals(variable, new Constant(0.0)); - } else { - FeatJAR.log().warning("Could not handle type "+ feature.getType()); - return; - } + + IFormula featureLiteral; + if (feature.getType().equals(Boolean.class)) { + featureLiteral = Expressions.literal(featureName); + } else if (feature.getType().equals(Integer.class)) { + featureLiteral = new NotEquals(variable, new Constant(0)); + } else if(feature.getType().equals(Float.class)) { + featureLiteral = new NotEquals(variable, new Constant(0.0)); + } else { + FeatJAR.log().warning("Could not handle type "+ feature.getType()); + return; + } + + // TODO take featureRanges into Account Result potentialParentTree = node.getParent(); Literal featureLiteral = Expressions.literal(featureName); From f4b5ceb60f889a231383abfeec9d56cfb83d6b00 Mon Sep 17 00:00:00 2001 From: Lara Date: Mon, 6 Oct 2025 14:02:49 +0200 Subject: [PATCH 13/67] fix: changed featureLiteral to featureFormula --- .../model/transformer/ComputeFormula.java | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java index 757e5edb..42adc106 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java @@ -20,6 +20,7 @@ */ package de.featjar.feature.model.transformer; +import de.featjar.base.FeatJAR; import de.featjar.base.computation.AComputation; import de.featjar.base.computation.Dependency; import de.featjar.base.computation.IComputation; @@ -41,6 +42,8 @@ import de.featjar.formula.structure.connective.Or; import de.featjar.formula.structure.connective.Reference; import de.featjar.formula.structure.predicate.Literal; +import de.featjar.formula.structure.predicate.NotEquals; +import de.featjar.formula.structure.term.value.Constant; import de.featjar.formula.structure.term.value.Variable; import java.util.ArrayList; import java.util.HashSet; @@ -73,23 +76,23 @@ public Result compute(List dependencyList, Progress progress) String featureName = feature.getName().orElse(""); Variable variable = new Variable(featureName, feature.getType()); variables.add(variable); - - IFormula featureLiteral; - if (feature.getType().equals(Boolean.class)) { - featureLiteral = Expressions.literal(featureName); - } else if (feature.getType().equals(Integer.class)) { - featureLiteral = new NotEquals(variable, new Constant(0)); - } else if(feature.getType().equals(Float.class)) { - featureLiteral = new NotEquals(variable, new Constant(0.0)); - } else { - FeatJAR.log().warning("Could not handle type "+ feature.getType()); - return; - } + IFormula featureFormula; + if (feature.getType().equals(Boolean.class)) { + featureFormula = Expressions.literal(featureName); + } else if (feature.getType().equals(Integer.class)) { + featureFormula = new NotEquals(variable, new Constant(0)); + } else if(feature.getType().equals(Float.class)) { + featureFormula = new NotEquals(variable, new Constant(0.0)); + } else { + FeatJAR.log().warning("Could not handle type "+ feature.getType()); + return; + } // TODO take featureRanges into Account Result potentialParentTree = node.getParent(); Literal featureLiteral = Expressions.literal(featureName); + if (potentialParentTree.isEmpty()) { handleRoot(constraints, featureLiteral, node); } else { @@ -99,7 +102,7 @@ public Result compute(List dependencyList, Progress progress) }); Reference reference = new Reference(new And(constraints)); reference.setFreeVariables(variables); - return Result.of(reference, problemList); + return Result.of(reference); } private void handleParent(ArrayList constraints, Literal featureLiteral, IFeatureTree node) { From 6803b3c304daea58a7ac7e4a48c599bf1f1f8339 Mon Sep 17 00:00:00 2001 From: Malena Horstmann Date: Tue, 7 Oct 2025 11:07:20 +0200 Subject: [PATCH 14/67] added simple translation for cardinality features and unit tests accordingly --- .../model/transformer/ComputeFormula.java | 12 +- .../ComputeSimpleFormulaVisitor.java | 186 ++++++++++++++++++ .../model/transformer/ComputeFormulaTest.java | 98 ++++++++- 3 files changed, 291 insertions(+), 5 deletions(-) create mode 100644 src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java index 0f844496..76d4ac37 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java @@ -44,6 +44,7 @@ */ public class ComputeFormula extends AComputation { protected static final Dependency FEATURE_MODEL = Dependency.newDependency(IFeatureModel.class); + protected boolean doSimpleCardinalityTranslation = false; public ComputeFormula(IComputation formula) { super(formula); @@ -53,6 +54,9 @@ protected ComputeFormula(ComputeFormula other) { super(other); } + public void SetSimple() { + doSimpleCardinalityTranslation = true; + } @Override @@ -62,7 +66,13 @@ public Result compute(List dependencyList, Progress progress) HashSet variables = new HashSet<>(); IFeatureTree iFeatureTree = featureModel.getRoots().get(0); - Trees.traverse(iFeatureTree, new ComputeFormulaVisitor(constraints, variables)); + + if(doSimpleCardinalityTranslation) { + Trees.traverse(iFeatureTree, new ComputeSimpleFormulaVisitor(constraints, variables)); + } + else { + Trees.traverse(iFeatureTree, new ComputeFormulaVisitor(constraints, variables)); + } // featureModel.getFeatureTreeStream().forEach(node -> { // // TODO use better error value diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java b/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java new file mode 100644 index 00000000..d351ccac --- /dev/null +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java @@ -0,0 +1,186 @@ +package de.featjar.feature.model.transformer; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +import de.featjar.base.data.Range; +import de.featjar.base.data.Result; +import de.featjar.base.tree.visitor.ITreeVisitor; +import de.featjar.feature.model.FeatureTree.Group; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureTree; +import de.featjar.formula.structure.Expressions; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.connective.AtLeast; +import de.featjar.formula.structure.connective.AtMost; +import de.featjar.formula.structure.connective.Between; +import de.featjar.formula.structure.connective.Choose; +import de.featjar.formula.structure.connective.Implies; +import de.featjar.formula.structure.connective.Or; +import de.featjar.formula.structure.predicate.Literal; +import de.featjar.formula.structure.term.value.Variable; + +/** + * This visitor implements a simple translation of IFeatureModel to boolean formula. + * In this implementation, a cardinality feature can not be a parent. The next non-cardinality + * feature will be the parent instead within the boolean representation. + */ +public class ComputeSimpleFormulaVisitor implements ITreeVisitor { + + protected ArrayList constraints = new ArrayList<>(); + protected HashSet variables = new HashSet<>(); + + public ComputeSimpleFormulaVisitor(ArrayList constraints, HashSet variables) { + + this.constraints = constraints; + this.variables = variables; + } + + @Override + public TraversalAction firstVisit(List path) { + IFeatureTree node = ITreeVisitor.getCurrentNode(path); + + // TODO use better error value + IFeature feature = node.getFeature(); + String featureName = feature.getName().orElse(""); + // TODO: do not add variable if its a cardinality var. Add duplicates instead + Variable variable = new Variable(featureName, feature.getType()); + variables.add(variable); + + // TODO take featureRanges into Account + Result potentialParentTree = node.getParent(); + Literal featureLiteral = Expressions.literal(featureName); + if (potentialParentTree.isEmpty()) { + handleRoot(featureLiteral, node); + } else if (node.getFeatureCardinalityUpperBound() > 1) { + handleCardinalityFeature(featureLiteral, node); + } else { + handleParent(featureLiteral, node); + } + + handleGroups(featureLiteral, node); + + return ITreeVisitor.super.firstVisit(path); + } + + private void handleParent(Literal featureLiteral, IFeatureTree node) { + // cardinal features must not be a parent + Literal parentLiteral = getNextNonCardinalityParent(node); + constraints.add(new Implies(featureLiteral, parentLiteral)); + } + + private void handleRoot(Literal featureLiteral, IFeatureTree node) { + if (node.isMandatory()) { + constraints.add(featureLiteral); + } + } + + private void handleCardinalityFeature(Literal featureLiteral, IFeatureTree node) { + + int lowerBound = node.getFeatureCardinalityLowerBound(); + int upperBound = node.getFeatureCardinalityUpperBound(); + + ArrayList featureList = new ArrayList(); + + // step 2: add literals and implication to parent + String literalName = ""; + Literal parentLiteral = getNextNonCardinalityParent(node); + for (int i = 1; i <= upperBound; i++) { + + literalName = node.getFeature().getName().get() + "_" + i; + featureLiteral = new Literal(literalName); + handleParent(featureLiteral, node); + + if (i > 1) { + // step 3: add to implication chain + IFormula previousLiteral = featureList.get(featureList.size() - 1); + constraints.add(new Implies(featureLiteral, previousLiteral)); + } + + featureList.add(featureLiteral); + } + + // step 4: add cardinality constraint +// Literal parentLiteral = Expressions.literal(node.getParent().get().getFeature().getName().orElse("")); + + if (lowerBound > 0) { + if (upperBound != Range.OPEN) { + constraints.add(new Implies(parentLiteral, new Between(lowerBound, upperBound, featureList))); + } else { + constraints.add(new Implies(parentLiteral, new AtMost(upperBound, featureList))); + } + } else { + if (upperBound != Range.OPEN) { + constraints.add(new Implies(parentLiteral, new AtLeast(lowerBound, featureList))); + } + } + } + + private Literal getNextNonCardinalityParent(IFeatureTree node) { + + // TODO: if it is possible that root can be as well a cardinality feature - there must be an alternative + node = node.getParent().get(); + + if (node.getFeatureCardinalityUpperBound() > 1) { + return getNextNonCardinalityParent(node); + } + + return Expressions.literal(node.getFeature().getName().orElse("")); + } + + private void handleGroups(Literal featureLiteral, IFeatureTree node) { + List childrenGroups = node.getChildrenGroups(); + int groupCount = childrenGroups.size(); + ArrayList> groupLiterals = new ArrayList<>(groupCount); + for (int i = 0; i < groupCount; i++) { + groupLiterals.add(null); + } + + // if node is cardinality feature, set feature literal to parent with no cardinality + if(node.getFeatureCardinalityUpperBound() > 1) { + featureLiteral = getNextNonCardinalityParent(node); + } + + List children = node.getChildren(); + for (IFeatureTree childNode : children) { + Literal childLiteral = Expressions.literal(childNode.getFeature().getName().orElse("")); + + if (childNode.isMandatory()) { + constraints.add(new Implies(featureLiteral, childLiteral)); + } + + int groupID = childNode.getParentGroupID(); + List list = groupLiterals.get(groupID); + if (list == null) { + groupLiterals.set(groupID, list = new ArrayList<>()); + } + list.add(childLiteral); + } + for (int i = 0; i < groupCount; i++) { + Group group = childrenGroups.get(i); + if (group != null) { + if (group.isOr()) { + constraints.add(new Implies(featureLiteral, new Or(groupLiterals.get(i)))); + } else if (group.isAlternative()) { + constraints.add(new Implies(featureLiteral, new Choose(1, groupLiterals.get(i)))); + } else { + int lowerBound = group.getLowerBound(); + int upperBound = group.getUpperBound(); + if (lowerBound > 0) { + if (upperBound != Range.OPEN) { + constraints.add(new Implies(featureLiteral, + new Between(lowerBound, upperBound, groupLiterals.get(i)))); + } else { + constraints.add(new Implies(featureLiteral, new AtMost(upperBound, groupLiterals.get(i)))); + } + } else { + if (upperBound != Range.OPEN) { + constraints.add(new Implies(featureLiteral, new AtLeast(lowerBound, groupLiterals.get(i)))); + } + } + } + } + } + } +} diff --git a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java index 4ab8036e..e3ca0346 100644 --- a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java +++ b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java @@ -22,6 +22,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.Arrays; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + import de.featjar.base.computation.ComputeConstant; import de.featjar.base.data.Range; import de.featjar.base.data.identifier.Identifiers; @@ -31,11 +36,11 @@ import de.featjar.feature.model.IFeatureTree; import de.featjar.formula.structure.IFormula; import de.featjar.formula.structure.connective.And; +import de.featjar.formula.structure.connective.AtLeast; +import de.featjar.formula.structure.connective.Choose; import de.featjar.formula.structure.connective.Implies; import de.featjar.formula.structure.connective.Reference; import de.featjar.formula.structure.predicate.Literal; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; class ComputeFormulaTest { private IFeatureModel featureModel; @@ -46,6 +51,74 @@ public void createFeatureModel() { featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); } + @Test + void simpleWithTwoCardinalies() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + IFeatureTree childFeature2Tree = childFeature1Tree.mutate().addFeatureBelow(childFeature2); + childFeature2Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + expected = new Reference(new And( +// new Literal("root"), + new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("root"), new AtLeast(0, Arrays.asList(new Literal("A_1"), new Literal("A_2")))), + + new Implies(new Literal("B_1"), new Literal("root")), + new Implies(new Literal("B_2"), new Literal("root")), + new Implies(new Literal("B_2"), new Literal("B_1")), + new Implies(new Literal("root"), new AtLeast(0, Arrays.asList(new Literal("B_1"), new Literal("B_2")))) + )); + + executeSimpleTest(); + + } + + @Test + void simpleWithCardinalityAndChildGroup() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + childFeature1Tree.mutate().toAlternativeGroup(); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + childFeature1Tree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + childFeature1Tree.mutate().addFeatureBelow(childFeature3); + + + expected = new Reference(new And( +// new Literal("root"), + new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("root"), new AtLeast(0, Arrays.asList(new Literal("A_1"), new Literal("A_2")))), + + new Implies(new Literal("root"), new Choose(1, Arrays.asList(new Literal("B"), new Literal("C")))), + new Implies(new Literal("B"), new Literal("root")), + new Implies(new Literal("C"), new Literal("root")) + )); + + executeSimpleTest(); + + } + @Test void onlyRoot() { @@ -53,7 +126,9 @@ void onlyRoot() { featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); // root must be selected - expected = new Reference(new And(new Literal("root"))); + expected = new Reference(new And( +// new Literal("root") + )); executeTest(); } @@ -73,7 +148,10 @@ void oneFeature() { rootTree.mutate().addFeatureBelow(childFeature); // TODO: check order if bug is fixed - expected = new Reference(new And(new Literal("root"), new Implies(new Literal("Test1"), new Literal("root")))); + expected = new Reference(new And( +// new Literal("root"), + new Implies(new Literal("Test1"), new Literal("root")) + )); executeTest(); } @@ -141,4 +219,16 @@ private void executeTest() { // assert assertEquals(expected, resultFormula); } + + private void executeSimpleTest() { + + ComputeConstant computeConstant = new ComputeConstant(featureModel); + ComputeFormula computeFormula = new ComputeFormula(computeConstant); + + computeFormula.SetSimple(); + IFormula resultFormula = computeFormula.computeResult().get(); + + // assert + assertEquals(expected, resultFormula); + } } From 36eeb65c1a01a1c00a9de3ec70267dfd919418b6 Mon Sep 17 00:00:00 2001 From: Lara Date: Tue, 7 Oct 2025 12:38:06 +0200 Subject: [PATCH 15/67] fix: changed Literal to IFormula --- .../model/transformer/ComputeFormula.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java index 42adc106..ebf377c2 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java @@ -77,13 +77,13 @@ public Result compute(List dependencyList, Progress progress) Variable variable = new Variable(featureName, feature.getType()); variables.add(variable); - IFormula featureFormula; + IFormula featureLiteral; if (feature.getType().equals(Boolean.class)) { - featureFormula = Expressions.literal(featureName); + featureLiteral = Expressions.literal(featureName); } else if (feature.getType().equals(Integer.class)) { - featureFormula = new NotEquals(variable, new Constant(0)); + featureLiteral = new NotEquals(variable, new Constant(0)); } else if(feature.getType().equals(Float.class)) { - featureFormula = new NotEquals(variable, new Constant(0.0)); + featureLiteral = new NotEquals(variable, new Constant(0.0)); } else { FeatJAR.log().warning("Could not handle type "+ feature.getType()); return; @@ -91,7 +91,7 @@ public Result compute(List dependencyList, Progress progress) // TODO take featureRanges into Account Result potentialParentTree = node.getParent(); - Literal featureLiteral = Expressions.literal(featureName); + //Literal featureLiteral = Expressions.literal(featureName); if (potentialParentTree.isEmpty()) { handleRoot(constraints, featureLiteral, node); @@ -105,20 +105,20 @@ public Result compute(List dependencyList, Progress progress) return Result.of(reference); } - private void handleParent(ArrayList constraints, Literal featureLiteral, IFeatureTree node) { + private void handleParent(ArrayList constraints, IFormula featureLiteral, IFeatureTree node) { constraints.add(new Implies( featureLiteral, Expressions.literal( node.getParent().get().getFeature().getName().orElse("")))); } - private void handleRoot(ArrayList constraints, Literal featureLiteral, IFeatureTree node) { + private void handleRoot(ArrayList constraints, IFormula featureLiteral, IFeatureTree node) { if (node.isMandatory()) { constraints.add(featureLiteral); } } - - private void handleGroups(ArrayList constraints, Literal featureLiteral, IFeatureTree node) { +// Literal -> IFormula + private void handleGroups(ArrayList constraints, IFormula featureLiteral, IFeatureTree node) { List childrenGroups = node.getChildrenGroups(); int groupCount = childrenGroups.size(); ArrayList> groupLiterals = new ArrayList<>(groupCount); From 28780d1a138fdeccae6d722e68b5dad07a371485 Mon Sep 17 00:00:00 2001 From: Malena Horstmann Date: Tue, 7 Oct 2025 16:15:19 +0200 Subject: [PATCH 16/67] fix within cloneTree(). Missing initialization of an array --- src/main/java/de/featjar/feature/model/FeatureTree.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/de/featjar/feature/model/FeatureTree.java b/src/main/java/de/featjar/feature/model/FeatureTree.java index 7ba8dc89..b2abd8e6 100644 --- a/src/main/java/de/featjar/feature/model/FeatureTree.java +++ b/src/main/java/de/featjar/feature/model/FeatureTree.java @@ -134,6 +134,7 @@ protected FeatureTree(FeatureTree otherFeatureTree) { feature = otherFeatureTree.feature; parentGroupID = otherFeatureTree.parentGroupID; cardinality = otherFeatureTree.cardinality.clone(); + childrenGroups = new ArrayList<>(otherFeatureTree.childrenGroups.size()); otherFeatureTree.childrenGroups.stream().map(Group::clone).forEach(childrenGroups::add); attributeValues = otherFeatureTree.cloneAttributes(); } From 803ed23046b5636346cbc059ba4c301e114d0cd7 Mon Sep 17 00:00:00 2001 From: Lara Date: Tue, 7 Oct 2025 16:46:45 +0200 Subject: [PATCH 17/67] feat: Test float, int, boolean for Formula translations --- .../feature/model/TranslateFormulaTest.java | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/test/java/de/featjar/feature/model/TranslateFormulaTest.java diff --git a/src/test/java/de/featjar/feature/model/TranslateFormulaTest.java b/src/test/java/de/featjar/feature/model/TranslateFormulaTest.java new file mode 100644 index 00000000..a2649f61 --- /dev/null +++ b/src/test/java/de/featjar/feature/model/TranslateFormulaTest.java @@ -0,0 +1,108 @@ +package de.featjar.feature.model; + +import de.featjar.base.FeatJAR; +import de.featjar.base.computation.Computations; +import de.featjar.base.data.identifier.Identifiers; +import de.featjar.feature.model.transformer.ComputeFormula; +import de.featjar.formula.structure.Expressions; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.connective.And; +import de.featjar.formula.structure.connective.Reference; +import de.featjar.formula.structure.predicate.Literal; +import de.featjar.formula.structure.predicate.NotEquals; +import de.featjar.formula.structure.term.value.Constant; +import de.featjar.formula.structure.term.value.Variable; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class TranslateFormulaTest { + + @BeforeAll + public static void insert() { + FeatJAR.testConfiguration().initialize(); + } + + @Test + public void testInteger() { + IFeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + addValues(featureModel, Integer.class); + + IFormula result = Computations.of(featureModel) + .map(ComputeFormula::new) + .compute(); + IFormula formula = buildFormula(Integer.class, 0); + Assertions.assertEquals(result.print(), formula.print()); + FeatJAR.log().info("Integer Test expected value: " + formula.print()); + FeatJAR.log().info("Integer Test result output: " + result.print()); + } + + @Test + public void testBoolean() { + IFeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + addValues(featureModel, Boolean.class); + + IFormula result = Computations.of(featureModel) + .map(ComputeFormula::new) + .compute(); + + IFormula formula = buildBooleanForumla(); + Assertions.assertEquals(result.print(), formula.print()); + FeatJAR.log().info("Boolean Test expected value: " + formula.print()); + FeatJAR.log().info("Boolean Test result output: " + result.print()); + } + + @Test + public void testFloat() { + IFeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + addValues(featureModel, Float.class); + + IFormula result = Computations.of(featureModel) + .map(ComputeFormula::new) + .compute(); + IFormula formula = buildFormula(Float.class, (float) 0.0); + Assertions.assertEquals(result.print(), formula.print()); + FeatJAR.log().info("Float Test expected value: " + formula.print()); + FeatJAR.log().info("Float Test result output: " + result.print()); + } + + private void addValues(IFeatureModel featureModel, Class type) { + IFeature root = featureModel.mutate().addFeature("root"); + IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(root); + rootTree.getRoot().mutate().toAndGroup(); + for (short i = 0; i < 5; i++) { + IFeature feature = featureModel.mutate().addFeature(i + "feature"); + feature.mutate().setName("feature"+i); + feature.mutate().setType(type); + rootTree.mutate().addFeatureBelow(feature); + + FeatJAR.log().info("Added Feature " + feature.getName().get() + " with type " + feature.getType()); + } + } + + private IFormula buildFormula(Class type, Object expectedValue) { + return new Reference(new And( + Expressions.implies(new NotEquals(new Variable("feature0", type), + new Constant(expectedValue, type)), new Literal("root")), + Expressions.implies(new NotEquals(new Variable("feature1", Boolean.class), + new Constant(expectedValue, type)), new Literal("root")), + Expressions.implies(new NotEquals(new Variable("feature2", Boolean.class), + new Constant(expectedValue, type)), new Literal("root")), + Expressions.implies(new NotEquals(new Variable("feature3", Boolean.class), + new Constant(expectedValue, type)), new Literal("root")), + Expressions.implies(new NotEquals(new Variable("feature4", Boolean.class), + new Constant(expectedValue, type)), new Literal("root")) + )); + } + + private IFormula buildBooleanForumla() { + return new Reference(new And( + Expressions.implies(Expressions.literal("feature0"), Expressions.literal("root")), + Expressions.implies(Expressions.literal("feature1"), Expressions.literal("root")), + Expressions.implies(Expressions.literal("feature2"), Expressions.literal("root")), + Expressions.implies(Expressions.literal("feature3"), Expressions.literal("root")), + Expressions.implies(Expressions.literal("feature4"), Expressions.literal("root")) + )); + } + +} From 3491d994706feb6938602f01d0389b92341d4213 Mon Sep 17 00:00:00 2001 From: Lara Date: Tue, 7 Oct 2025 16:50:09 +0200 Subject: [PATCH 18/67] fix: removed empty lines --- .../de/featjar/feature/model/TranslateFormulaTest.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/test/java/de/featjar/feature/model/TranslateFormulaTest.java b/src/test/java/de/featjar/feature/model/TranslateFormulaTest.java index a2649f61..9ab2e720 100644 --- a/src/test/java/de/featjar/feature/model/TranslateFormulaTest.java +++ b/src/test/java/de/featjar/feature/model/TranslateFormulaTest.java @@ -72,7 +72,7 @@ private void addValues(IFeatureModel featureModel, Class type) { rootTree.getRoot().mutate().toAndGroup(); for (short i = 0; i < 5; i++) { IFeature feature = featureModel.mutate().addFeature(i + "feature"); - feature.mutate().setName("feature"+i); + feature.mutate().setName("feature" + i); feature.mutate().setType(type); rootTree.mutate().addFeatureBelow(feature); @@ -92,7 +92,7 @@ private IFormula buildFormula(Class type, Object expectedValue) { new Constant(expectedValue, type)), new Literal("root")), Expressions.implies(new NotEquals(new Variable("feature4", Boolean.class), new Constant(expectedValue, type)), new Literal("root")) - )); + )); } private IFormula buildBooleanForumla() { @@ -104,5 +104,4 @@ private IFormula buildBooleanForumla() { Expressions.implies(Expressions.literal("feature4"), Expressions.literal("root")) )); } - -} +} \ No newline at end of file From aa3a23491da8ef233416afabbb435c434c12c602 Mon Sep 17 00:00:00 2001 From: Lara Date: Wed, 8 Oct 2025 09:32:50 +0200 Subject: [PATCH 19/67] remove: comments and lines --- .../featjar/feature/model/transformer/ComputeFormula.java | 3 +-- .../de/featjar/feature/model/TranslateFormulaTest.java | 7 ++++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java index ebf377c2..1e4157a3 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java @@ -91,7 +91,6 @@ public Result compute(List dependencyList, Progress progress) // TODO take featureRanges into Account Result potentialParentTree = node.getParent(); - //Literal featureLiteral = Expressions.literal(featureName); if (potentialParentTree.isEmpty()) { handleRoot(constraints, featureLiteral, node); @@ -117,7 +116,7 @@ private void handleRoot(ArrayList constraints, IFormula featureLiteral constraints.add(featureLiteral); } } -// Literal -> IFormula + private void handleGroups(ArrayList constraints, IFormula featureLiteral, IFeatureTree node) { List childrenGroups = node.getChildrenGroups(); int groupCount = childrenGroups.size(); diff --git a/src/test/java/de/featjar/feature/model/TranslateFormulaTest.java b/src/test/java/de/featjar/feature/model/TranslateFormulaTest.java index 9ab2e720..2ab1127f 100644 --- a/src/test/java/de/featjar/feature/model/TranslateFormulaTest.java +++ b/src/test/java/de/featjar/feature/model/TranslateFormulaTest.java @@ -16,6 +16,12 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +/** + * @author Lara Merza + * @author Felix Behme + * @author Jonas Hanke + */ + public class TranslateFormulaTest { @BeforeAll @@ -45,7 +51,6 @@ public void testBoolean() { IFormula result = Computations.of(featureModel) .map(ComputeFormula::new) .compute(); - IFormula formula = buildBooleanForumla(); Assertions.assertEquals(result.print(), formula.print()); FeatJAR.log().info("Boolean Test expected value: " + formula.print()); From 780c006bed01d3d63321719d03ef89093851f811 Mon Sep 17 00:00:00 2001 From: Lara Date: Wed, 8 Oct 2025 10:42:54 +0200 Subject: [PATCH 20/67] fix: cast float and changed assertqueals to asserttrue in the test --- .../feature/model/transformer/ComputeFormula.java | 2 +- .../feature/model/TranslateFormulaTest.java | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java index 1e4157a3..c8ac897c 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java @@ -83,7 +83,7 @@ public Result compute(List dependencyList, Progress progress) } else if (feature.getType().equals(Integer.class)) { featureLiteral = new NotEquals(variable, new Constant(0)); } else if(feature.getType().equals(Float.class)) { - featureLiteral = new NotEquals(variable, new Constant(0.0)); + featureLiteral = new NotEquals(variable, new Constant((float) 0.0)); } else { FeatJAR.log().warning("Could not handle type "+ feature.getType()); return; diff --git a/src/test/java/de/featjar/feature/model/TranslateFormulaTest.java b/src/test/java/de/featjar/feature/model/TranslateFormulaTest.java index 2ab1127f..63eeb12b 100644 --- a/src/test/java/de/featjar/feature/model/TranslateFormulaTest.java +++ b/src/test/java/de/featjar/feature/model/TranslateFormulaTest.java @@ -3,6 +3,7 @@ import de.featjar.base.FeatJAR; import de.featjar.base.computation.Computations; import de.featjar.base.data.identifier.Identifiers; +import de.featjar.base.tree.Trees; import de.featjar.feature.model.transformer.ComputeFormula; import de.featjar.formula.structure.Expressions; import de.featjar.formula.structure.IFormula; @@ -38,7 +39,7 @@ public void testInteger() { .map(ComputeFormula::new) .compute(); IFormula formula = buildFormula(Integer.class, 0); - Assertions.assertEquals(result.print(), formula.print()); + Assertions.assertTrue(Trees.equals(result, formula), result.print() + "\n" + formula.print()); FeatJAR.log().info("Integer Test expected value: " + formula.print()); FeatJAR.log().info("Integer Test result output: " + result.print()); } @@ -52,7 +53,7 @@ public void testBoolean() { .map(ComputeFormula::new) .compute(); IFormula formula = buildBooleanForumla(); - Assertions.assertEquals(result.print(), formula.print()); + Assertions.assertTrue(Trees.equals(result, formula), result.print() + "\n" + formula.print()); FeatJAR.log().info("Boolean Test expected value: " + formula.print()); FeatJAR.log().info("Boolean Test result output: " + result.print()); } @@ -66,7 +67,7 @@ public void testFloat() { .map(ComputeFormula::new) .compute(); IFormula formula = buildFormula(Float.class, (float) 0.0); - Assertions.assertEquals(result.print(), formula.print()); + Assertions.assertTrue(Trees.equals(result, formula), result.print() + "\n" + formula.print()); FeatJAR.log().info("Float Test expected value: " + formula.print()); FeatJAR.log().info("Float Test result output: " + result.print()); } @@ -89,13 +90,13 @@ private IFormula buildFormula(Class type, Object expectedValue) { return new Reference(new And( Expressions.implies(new NotEquals(new Variable("feature0", type), new Constant(expectedValue, type)), new Literal("root")), - Expressions.implies(new NotEquals(new Variable("feature1", Boolean.class), + Expressions.implies(new NotEquals(new Variable("feature1", type), new Constant(expectedValue, type)), new Literal("root")), - Expressions.implies(new NotEquals(new Variable("feature2", Boolean.class), + Expressions.implies(new NotEquals(new Variable("feature2", type), new Constant(expectedValue, type)), new Literal("root")), - Expressions.implies(new NotEquals(new Variable("feature3", Boolean.class), + Expressions.implies(new NotEquals(new Variable("feature3", type), new Constant(expectedValue, type)), new Literal("root")), - Expressions.implies(new NotEquals(new Variable("feature4", Boolean.class), + Expressions.implies(new NotEquals(new Variable("feature4", type), new Constant(expectedValue, type)), new Literal("root")) )); } From bfa7943f2d67e0f2e81fab05613b5b18f046939d Mon Sep 17 00:00:00 2001 From: Jonas Hanke Date: Wed, 8 Oct 2025 12:19:01 +0200 Subject: [PATCH 21/67] feat: attribute aggregates in constraints will be replaced with corresponding formulas --- .../ComputeAttributeAggregate.java | 29 ++++ .../model/transformer/ComputeFormula.java | 22 ++- .../ReplaceAttributeAggregate.java | 70 +++++++++ .../ReplaceAttributeAggregateTest.java | 146 ++++++++++++++++++ 4 files changed, 263 insertions(+), 4 deletions(-) create mode 100644 src/main/java/de/featjar/feature/model/transformer/ComputeAttributeAggregate.java create mode 100644 src/main/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregate.java create mode 100644 src/test/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregateTest.java diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeAttributeAggregate.java b/src/main/java/de/featjar/feature/model/transformer/ComputeAttributeAggregate.java new file mode 100644 index 00000000..773ce021 --- /dev/null +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeAttributeAggregate.java @@ -0,0 +1,29 @@ +package de.featjar.feature.model.transformer; + +import de.featjar.base.computation.AComputation; +import de.featjar.base.computation.Progress; +import de.featjar.base.data.Result; +import de.featjar.formula.structure.IFormula; + +import java.util.List; + +public class ComputeAttributeAggregate extends AComputation { + + /*protected static final Dependency ATTRIBUTE_AGGREGATE = Dependency.newDependency(IAttributeAggregate.class); + protected static final Dependency> VARIABLES = Dependency.newDependency(List.class); + + public ComputeAttributeAggregate(IComputation attributeAggregate, + IComputation> variables, + IComputation> values) { + super(attributeAggregate, variables, values); + } + + protected ComputeAttributeAggregate(ComputeFormula other) { + super(other); + }*/ + + @Override + public Result compute(List dependencyList, Progress progress) { + return Result.empty(); + } +} diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java index dcacf5bb..dccc116a 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java @@ -24,8 +24,10 @@ import de.featjar.base.computation.Dependency; import de.featjar.base.computation.IComputation; import de.featjar.base.computation.Progress; +import de.featjar.base.data.IAttribute; import de.featjar.base.data.Range; import de.featjar.base.data.Result; +import de.featjar.base.tree.Trees; import de.featjar.feature.model.FeatureTree.Group; import de.featjar.feature.model.IFeature; import de.featjar.feature.model.IFeatureModel; @@ -42,9 +44,8 @@ import de.featjar.formula.structure.connective.Reference; import de.featjar.formula.structure.predicate.Literal; import de.featjar.formula.structure.term.value.Variable; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; + +import java.util.*; /** * Transforms a feature model into a boolean formula. @@ -67,6 +68,8 @@ public Result compute(List dependencyList, Progress progress) IFeatureModel featureModel = FEATURE_MODEL.get(dependencyList); ArrayList constraints = new ArrayList<>(); HashSet variables = new HashSet<>(); + Map, Object>> attributes = new LinkedHashMap<>(); + featureModel.getFeatureTreeStream().forEach(node -> { // TODO use better error value IFeature feature = node.getFeature(); @@ -74,6 +77,10 @@ public Result compute(List dependencyList, Progress progress) Variable variable = new Variable(featureName, feature.getType()); variables.add(variable); + if(node.getAttributes().isPresent()) { + attributes.put(variable, node.getAttributes().get()); + } + // TODO take featureRanges into Account Result potentialParentTree = node.getParent(); Literal featureLiteral = Expressions.literal(featureName); @@ -84,6 +91,13 @@ public Result compute(List dependencyList, Progress progress) } handleGroups(constraints, featureLiteral, node); }); + + ReplaceAttributeAggregate replaceAttributeAggregate = new ReplaceAttributeAggregate(attributes); + featureModel.getConstraints().forEach(constraint -> { + Trees.traverse(constraint.getFormula(), replaceAttributeAggregate); + constraints.add(constraint.getFormula()); + }); + Reference reference = new Reference(new And(constraints)); reference.setFreeVariables(variables); return Result.of(reference); @@ -151,4 +165,4 @@ private void handleGroups(ArrayList constraints, Literal featureLitera } } } -} +} \ No newline at end of file diff --git a/src/main/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregate.java b/src/main/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregate.java new file mode 100644 index 00000000..145affcb --- /dev/null +++ b/src/main/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregate.java @@ -0,0 +1,70 @@ +package de.featjar.feature.model.transformer; + +import de.featjar.base.data.IAttribute; +import de.featjar.base.data.Result; +import de.featjar.base.data.Void; +import de.featjar.base.tree.visitor.ITreeVisitor; +import de.featjar.formula.structure.IExpression; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.term.aggregate.IAttributeAggregate; +import de.featjar.formula.structure.term.value.Variable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Implements tree visitor {@link ITreeVisitor}. + * Each {@link IAttributeAggregate} placeholder in a formula will be replaced with the correct formula. + * + * @author Lara Merza + * @author Felix Behme + * @author Jonas Hanke + */ +public class ReplaceAttributeAggregate implements ITreeVisitor { + + private final Map, Object>> attributes; + + public ReplaceAttributeAggregate(Map, Object>> attributes) { + this.attributes = attributes; + } + + @Override + public TraversalAction lastVisit(List path) { + final IExpression formula = ITreeVisitor.getCurrentNode(path); + + if(formula instanceof IAttributeAggregate) { + final Result parent = ITreeVisitor.getParentNode(path); + + if(parent.isPresent()) { + ArrayList filteredVariables = new ArrayList<>(); + ArrayList values = new ArrayList<>(); + String attributeFilter = ((IAttributeAggregate) formula).getAttributeFilter(); + + attributes.forEach((variable, value) -> { + Optional, Object>> attributeMatch = value.entrySet().stream() + .filter(predicate -> predicate.getKey().getName().equals(attributeFilter)) + .findFirst(); + + if (attributeMatch.isPresent()) { + filteredVariables.add(variable); + values.add(attributeMatch.get().getValue()); + } + }); + + Result result = ((IAttributeAggregate) formula).translate(filteredVariables, values); + if(result.isPresent()) { + parent.get().replaceChild(formula, result.get()); + } + } + } + + return TraversalAction.CONTINUE; + } + + @Override + public Result getResult() { + return Result.ofVoid(); + } +} \ No newline at end of file diff --git a/src/test/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregateTest.java b/src/test/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregateTest.java new file mode 100644 index 00000000..f1af994c --- /dev/null +++ b/src/test/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregateTest.java @@ -0,0 +1,146 @@ +package de.featjar.feature.model.transformer; + +import de.featjar.base.FeatJAR; +import de.featjar.base.data.Attribute; +import de.featjar.base.data.IAttribute; +import de.featjar.base.tree.Trees; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.connective.And; +import de.featjar.formula.structure.connective.Implies; +import de.featjar.formula.structure.predicate.LessThan; +import de.featjar.formula.structure.predicate.Literal; +import de.featjar.formula.structure.term.aggregate.AttributeAverage; +import de.featjar.formula.structure.term.aggregate.AttributeSum; +import de.featjar.formula.structure.term.function.IfThenElse; +import de.featjar.formula.structure.term.function.IntegerAdd; +import de.featjar.formula.structure.term.function.RealAdd; +import de.featjar.formula.structure.term.function.RealDivide; +import de.featjar.formula.structure.term.value.Constant; +import de.featjar.formula.structure.term.value.Variable; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class ReplaceAttributeAggregateTest { + + private static Map, Object>> attributes; + + @BeforeAll + public static void init() { + FeatJAR.testConfiguration().initialize(); + + attributes = new LinkedHashMap<>(); + attributes.put(new Variable("cpu", Boolean.class), + Map.of( + new Attribute<>("cost", Long.class), 10L, + new Attribute<>("required", Boolean.class), true, + new Attribute<>("power", Float.class), 104.5f + ) + ); + attributes.put(new Variable("gpu", Boolean.class), + Map.of( + new Attribute<>("cost", Long.class), 100L, + new Attribute<>("required", Boolean.class), false, + new Attribute<>("power", Float.class), 200.5f + ) + ); + attributes.put(new Variable("ram", Boolean.class), + Map.of( + new Attribute<>("cost", Long.class), 20L, + new Attribute<>("required", Boolean.class), true + ) + ); + attributes.put(new Variable("motherboard", Boolean.class), + Map.of( + new Attribute<>("required", Boolean.class), true, + new Attribute<>("power", Float.class), 3.5f + ) + ); + attributes.put(new Variable("power_supply", Boolean.class), Collections.emptyMap()); + } + + @Test + public void test1() { + IFormula test = new LessThan(new AttributeSum("cost"), new Constant(200L, Long.class)); + ReplaceAttributeAggregate replaceAttributeAggregate = new ReplaceAttributeAggregate(attributes); + Trees.traverse(test, replaceAttributeAggregate); + + IFormula comparison = new LessThan( + new IntegerAdd( + new IfThenElse(new Variable("cpu", Boolean.class), new Constant(10L, Long.class), new Constant(0L, Long.class)), + new IfThenElse(new Variable("gpu", Boolean.class), new Constant(100L, Long.class), new Constant(0L, Long.class)), + new IfThenElse(new Variable("ram", Boolean.class), new Constant(20L, Long.class), new Constant(0L, Long.class)) + ), + new Constant(200L, Long.class) + ); + + assertTrue(test.equalsTree(comparison)); + } + + @Test + public void test2() { + IFormula test = new LessThan(new AttributeSum("cost"), new AttributeAverage("power")); + ReplaceAttributeAggregate replaceAttributeAggregate = new ReplaceAttributeAggregate(attributes); + Trees.traverse(test, replaceAttributeAggregate); + + IFormula comparison = new LessThan( + new IntegerAdd( + new IfThenElse(new Variable("cpu", Boolean.class), new Constant(10L, Long.class), new Constant(0L, Long.class)), + new IfThenElse(new Variable("gpu", Boolean.class), new Constant(100L, Long.class), new Constant(0L, Long.class)), + new IfThenElse(new Variable("ram", Boolean.class), new Constant(20L, Long.class), new Constant(0L, Long.class)) + ), + new RealDivide( + new RealAdd( + new IfThenElse(new Variable("cpu", Boolean.class), new Constant(104.5, Double.class), new Constant(0.0, Double.class)), + new IfThenElse(new Variable("gpu", Boolean.class), new Constant(200.5, Double.class), new Constant(0.0, Double.class)), + new IfThenElse(new Variable("motherboard", Boolean.class), new Constant(3.5, Double.class), new Constant(0.0, Double.class)) + ), + new RealAdd( + new IfThenElse(new Variable("cpu", Boolean.class), new Constant(1.0, Double.class), new Constant(0.0, Double.class)), + new IfThenElse(new Variable("gpu", Boolean.class), new Constant(1.0, Double.class), new Constant(0.0, Double.class)), + new IfThenElse(new Variable("motherboard", Boolean.class), new Constant(1.0, Double.class), new Constant(0.0, Double.class)) + ) + ) + ); + + assertTrue(test.equalsTree(comparison)); + } + + @Test + public void test3() { + IFormula test = new And( + new Implies( + new Literal("cables"), + new LessThan( + new AttributeSum("cost"), + new Constant(200L, Long.class)) + ), + new Literal("case") + ); + ReplaceAttributeAggregate replaceAttributeAggregate = new ReplaceAttributeAggregate(attributes); + Trees.traverse(test, replaceAttributeAggregate); + + IFormula comparison = new And( + new Implies( + new Literal("cables"), + new LessThan( + new IntegerAdd( + new IfThenElse(new Variable("cpu", Boolean.class), new Constant(10L, Long.class), new Constant(0L, Long.class)), + new IfThenElse(new Variable("gpu", Boolean.class), new Constant(100L, Long.class), new Constant(0L, Long.class)), + new IfThenElse(new Variable("ram", Boolean.class), new Constant(20L, Long.class), new Constant(0L, Long.class)) + ), + new Constant(200L, Long.class) + ) + ), + new Literal("case") + ); + + assertTrue(test.equalsTree(comparison)); + } +} \ No newline at end of file From fbeef5ffe7eac7d7f115ec256e9658f45151894c Mon Sep 17 00:00:00 2001 From: Malena Horstmann Date: Wed, 8 Oct 2025 16:24:27 +0200 Subject: [PATCH 22/67] added recursive tree traversal in ComputeFormula to build cardinality constraints. Also added a few unit tests --- .../model/transformer/ComputeFormula.java | 271 ++++++++++++++---- .../ComputeSimpleFormulaVisitor.java | 3 + .../model/transformer/ComputeFormulaTest.java | 174 ++++++++++- 3 files changed, 386 insertions(+), 62 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java index 76d4ac37..6272e2fd 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java @@ -22,19 +22,31 @@ import java.util.ArrayList; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import de.featjar.base.computation.AComputation; +import de.featjar.base.computation.Computations; import de.featjar.base.computation.Dependency; import de.featjar.base.computation.IComputation; import de.featjar.base.computation.Progress; +import de.featjar.base.data.Attribute; +import de.featjar.base.data.Range; import de.featjar.base.data.Result; import de.featjar.base.tree.Trees; +import de.featjar.feature.model.FeatureTree.Group; import de.featjar.feature.model.IFeatureModel; import de.featjar.feature.model.IFeatureTree; import de.featjar.formula.structure.IFormula; import de.featjar.formula.structure.connective.And; +import de.featjar.formula.structure.connective.AtLeast; +import de.featjar.formula.structure.connective.AtMost; +import de.featjar.formula.structure.connective.Between; +import de.featjar.formula.structure.connective.Choose; +import de.featjar.formula.structure.connective.Implies; +import de.featjar.formula.structure.connective.Or; import de.featjar.formula.structure.connective.Reference; +import de.featjar.formula.structure.predicate.Literal; import de.featjar.formula.structure.term.value.Variable; /** @@ -44,61 +56,208 @@ */ public class ComputeFormula extends AComputation { protected static final Dependency FEATURE_MODEL = Dependency.newDependency(IFeatureModel.class); - protected boolean doSimpleCardinalityTranslation = false; - - public ComputeFormula(IComputation formula) { - super(formula); - } - - protected ComputeFormula(ComputeFormula other) { - super(other); - } - - public void SetSimple() { - doSimpleCardinalityTranslation = true; - } - - - @Override - public Result compute(List dependencyList, Progress progress) { - IFeatureModel featureModel = FEATURE_MODEL.get(dependencyList); - ArrayList constraints = new ArrayList<>(); - HashSet variables = new HashSet<>(); - - IFeatureTree iFeatureTree = featureModel.getRoots().get(0); - - if(doSimpleCardinalityTranslation) { - Trees.traverse(iFeatureTree, new ComputeSimpleFormulaVisitor(constraints, variables)); - } - else { - Trees.traverse(iFeatureTree, new ComputeFormulaVisitor(constraints, variables)); - } - -// featureModel.getFeatureTreeStream().forEach(node -> { -// // TODO use better error value -// IFeature feature = node.getFeature(); -// String featureName = feature.getName().orElse(""); -// Variable variable = new Variable(featureName, feature.getType()); -// variables.add(variable); -// -// // TODO take featureRanges into Account -// Result potentialParentTree = node.getParent(); -// Literal featureLiteral = Expressions.literal(featureName); -// if (potentialParentTree.isEmpty()) { -// handleRoot(constraints, featureLiteral, node); -// } -// else { -// handleParent(constraints, featureLiteral, node); -// } -// -// if(node.getFeatureCardinalityUpperBound() > 1) { -// handleCardinalityFeature(constraints, featureLiteral, node); -// } -// handleGroups(constraints, featureLiteral, node); -// }); - Reference reference = new Reference(new And(constraints)); - reference.setFreeVariables(variables); - return Result.of(reference); - } + protected static final Dependency SIMPLE_TRANSLATION = Dependency.newDependency(Boolean.class); + + static Attribute literalNameAttribute = new Attribute<>("literalName", String.class); +// protected boolean traverseRecursive = false; + + public ComputeFormula(IComputation formula) { + super(formula, Computations.of(Boolean.FALSE)); + } + + protected ComputeFormula(ComputeFormula other) { + super(other); + } + +// public void setRecursive() { +// traverseRecursive = true; +// } + + @Override + public Result compute(List dependencyList, Progress progress) { + IFeatureModel featureModel = FEATURE_MODEL.get(dependencyList); + ArrayList constraints = new ArrayList<>(); + HashSet variables = new HashSet<>(); + + IFeatureTree iFeatureTree = featureModel.getRoots().get(0); + +// if (traverseRecursive) { +// } else { + if (SIMPLE_TRANSLATION.get(dependencyList)) { + Trees.traverse(iFeatureTree, new ComputeSimpleFormulaVisitor(constraints, variables)); + } else { + traverseFeatureModel(featureModel, constraints, variables); +// Trees.traverse(iFeatureTree, new ComputeFormulaVisitor(constraints, variables)); + } +// } + + Reference reference = new Reference(new And(constraints)); + reference.setFreeVariables(variables); + return Result.of(reference); + } + + private void traverseFeatureModel(IFeatureModel featureModel, ArrayList constraints, + HashSet variables) { + + // copy FM to not change the original one + IFeatureModel temporaryFM = featureModel.clone(); + + for (IFeatureTree root : temporaryFM.getRoots()) { + constraints.add(new Literal(root.getFeature().getName().orElse(""))); + addChildConstraints(root, constraints, temporaryFM); + } + } + + private void addChildConstraints(IFeatureTree node, ArrayList constraints, IFeatureModel fm) { + + Literal parentLiteral = new Literal(getLiteralName(node)); + + for (IFeatureTree child : node.getChildren()) { + + if (isCardinalityFeature(child)) { + + int upperBound = child.getFeatureCardinalityUpperBound(); + int lowerBound = child.getFeatureCardinalityLowerBound(); + + LinkedList constraintGroupLiterals = new LinkedList(); + + for (int i = 1; i <= upperBound; i++) { + + String literalName = getLiteralName(child) + "_" + i; + if (cardinalityFeatureAbove(child)) { + literalName += "." + getLiteralName(node); + } + + IFeatureTree cardinalityClone = child.cloneTree(); + cardinalityClone.mutate().setAttributeValue(literalNameAttribute, literalName); + +// node.addChild(cardinalityClone); + + Literal currentLiteral = new Literal(literalName); + + // add all the constraints + // imply parent + constraints.add(new Implies(currentLiteral, parentLiteral)); + // implication chain part + if (i > 1) { + Literal previousLiteral = constraintGroupLiterals.getLast(); + constraints.add(new Implies(currentLiteral, previousLiteral)); + } + // group constraints + handleGroups(currentLiteral, cardinalityClone, constraints); + + constraintGroupLiterals.add(currentLiteral); + + addChildConstraints(cardinalityClone, constraints, fm); + } + // TODO: check if 0 and do not add then + constraints.add(new Implies(parentLiteral, new AtLeast(lowerBound, constraintGroupLiterals))); + + return; + } else { + + String literalName = getLiteralName(child); + if (cardinalityFeatureAbove(child)) { + literalName += "." + getLiteralName(node); + } + + Literal childFeatureLiteral = new Literal(literalName); + child.mutate().setAttributeValue(literalNameAttribute, literalName); + + // add constraints + // always add parent implications (child implies parent) + constraints.add(new Implies(childFeatureLiteral, parentLiteral)); + + // handle group + handleGroups(childFeatureLiteral, child, constraints); + + addChildConstraints(child, constraints, fm); + } + } + } + + private String getLiteralName(IFeatureTree node) { + String literalName = ""; + if (node.getAttributeValue(literalNameAttribute).isEmpty()) { + literalName = node.getFeature().getName().orElse(""); + } else { + literalName = node.getAttributeValue(literalNameAttribute).orElse(""); + } + return literalName; + } + + private boolean cardinalityFeatureAbove(IFeatureTree child) { + + if (!child.getParent().isPresent()) + return false; + + if (isCardinalityFeature(child.getParent().get())) { + return true; + } else { + return cardinalityFeatureAbove(child.getParent().get()); + } + } + + private boolean isCardinalityFeature(IFeatureTree node) { + + if (node.getFeatureCardinalityUpperBound() > 1) { + return true; + } + return false; + } + + private void handleGroups(Literal featureLiteral, IFeatureTree node, ArrayList constraints) { + List childrenGroups = node.getChildrenGroups(); + int groupCount = childrenGroups.size(); + ArrayList> groupLiterals = new ArrayList<>(groupCount); + for (int i = 0; i < groupCount; i++) { + groupLiterals.add(null); + } + List children = node.getChildren(); + for (IFeatureTree childNode : children) { + + String childLiteralName = getLiteralName(childNode); + if (childNode.getAttributeValue(literalNameAttribute).isEmpty()) + childLiteralName += "." + getLiteralName(node); + + Literal childLiteral = new Literal(childLiteralName); + + if (childNode.isMandatory()) { + constraints.add(new Implies(featureLiteral, childLiteral)); + } + + int groupID = childNode.getParentGroupID(); + List list = groupLiterals.get(groupID); + if (list == null) { + groupLiterals.set(groupID, list = new ArrayList<>()); + } + list.add(childLiteral); + } + for (int i = 0; i < groupCount; i++) { + Group group = childrenGroups.get(i); + if (group != null) { + if (group.isOr()) { + constraints.add(new Implies(featureLiteral, new Or(groupLiterals.get(i)))); + } else if (group.isAlternative()) { + constraints.add(new Implies(featureLiteral, new Choose(1, groupLiterals.get(i)))); + } else { + int lowerBound = group.getLowerBound(); + int upperBound = group.getUpperBound(); + if (lowerBound > 0) { + if (upperBound != Range.OPEN) { + constraints.add(new Implies(featureLiteral, + new Between(lowerBound, upperBound, groupLiterals.get(i)))); + } else { + constraints.add(new Implies(featureLiteral, new AtMost(upperBound, groupLiterals.get(i)))); + } + } else { + if (upperBound != Range.OPEN) { + constraints.add(new Implies(featureLiteral, new AtLeast(lowerBound, groupLiterals.get(i)))); + } + } + } + } + } + } } diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java b/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java index d351ccac..92fc1adb 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java @@ -142,6 +142,8 @@ private void handleGroups(Literal featureLiteral, IFeatureTree node) { featureLiteral = getNextNonCardinalityParent(node); } + + // TODO: check for cross-tree-constraints related to original feature and also copy it to match each newly created pseudo-literal List children = node.getChildren(); for (IFeatureTree childNode : children) { Literal childLiteral = Expressions.literal(childNode.getFeature().getName().orElse("")); @@ -157,6 +159,7 @@ private void handleGroups(Literal featureLiteral, IFeatureTree node) { } list.add(childLiteral); } + for (int i = 0; i < groupCount; i++) { Group group = childrenGroups.get(i); if (group != null) { diff --git a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java index e3ca0346..51581592 100644 --- a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java +++ b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java @@ -39,6 +39,7 @@ import de.featjar.formula.structure.connective.AtLeast; import de.featjar.formula.structure.connective.Choose; import de.featjar.formula.structure.connective.Implies; +import de.featjar.formula.structure.connective.Or; import de.featjar.formula.structure.connective.Reference; import de.featjar.formula.structure.predicate.Literal; @@ -83,6 +84,43 @@ void simpleWithTwoCardinalies() { } + @Test + void withTwoCardinaliesRecursive() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + IFeatureTree childFeature2Tree = childFeature1Tree.mutate().addFeatureBelow(childFeature2); + childFeature2Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("B_1.A_1"), new Literal("A_1")), + new Implies(new Literal("B_2.A_1"), new Literal("A_1")), + new Implies(new Literal("B_2.A_1"), new Literal("B_1.A_1")), + new Implies(new Literal("A_1"), new AtLeast(0, Arrays.asList(new Literal("B_1.A_1"), new Literal("B_2.A_1")))), + + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("B_1.A_2"), new Literal("A_2")), + new Implies(new Literal("B_2.A_2"), new Literal("A_2")), + new Implies(new Literal("B_2.A_2"), new Literal("B_1.A_2")), + new Implies(new Literal("A_2"), new AtLeast(0, Arrays.asList(new Literal("B_1.A_2"), new Literal("B_2.A_2")))), + + new Implies(new Literal("root"), new AtLeast(0, Arrays.asList(new Literal("A_1"), new Literal("A_2")))) + )); + + executeRecursiveTest(); + + } + @Test void simpleWithCardinalityAndChildGroup() { IFeatureTree rootTree = @@ -119,6 +157,105 @@ void simpleWithCardinalityAndChildGroup() { } + @Test + void withCardinalityAndChildGroupRecursive() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + childFeature1Tree.mutate().toAlternativeGroup(); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + childFeature1Tree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + childFeature1Tree.mutate().addFeatureBelow(childFeature3); + + + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("A_1"), new Choose(1, Arrays.asList(new Literal("B.A_1"), new Literal("C.A_1")))), + new Implies(new Literal("B.A_1"), new Literal("A_1")), + new Implies(new Literal("C.A_1"), new Literal("A_1")), + + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("A_2"), new Choose(1, Arrays.asList(new Literal("B.A_2"), new Literal("C.A_2")))), + new Implies(new Literal("B.A_2"), new Literal("A_2")), + new Implies(new Literal("C.A_2"), new Literal("A_2")), + + new Implies(new Literal("root"), new AtLeast(0, Arrays.asList(new Literal("A_1"), new Literal("A_2")))) + + )); + + executeRecursiveTest(); + } + + @Test + void withCardinalityAndChildChildGroupRecursive() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + childFeature1Tree.mutate().toAlternativeGroup(); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + childFeature1Tree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + IFeatureTree childFeature1Tree2 = childFeature1Tree.mutate().addFeatureBelow(childFeature3); + + childFeature1Tree2.mutate().toOrGroup(); + + IFeature childFeature4 = featureModel.mutate().addFeature("D"); + childFeature1Tree2.mutate().addFeatureBelow(childFeature4); + + IFeature childFeature5 = featureModel.mutate().addFeature("E"); + childFeature1Tree2.mutate().addFeatureBelow(childFeature5); + + + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("A_1"), new Choose(1, Arrays.asList(new Literal("B.A_1"), new Literal("C.A_1")))), + new Implies(new Literal("B.A_1"), new Literal("A_1")), + new Implies(new Literal("C.A_1"), new Literal("A_1")), + + //sub-subtree + new Implies(new Literal("C.A_1"), new Or(Arrays.asList(new Literal("D.C.A_1"), new Literal("E.C.A_1")))), + new Implies(new Literal("D.C.A_1"), new Literal("C.A_1")), + new Implies(new Literal("E.C.A_1"), new Literal("C.A_1")), + + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("A_2"), new Choose(1, Arrays.asList(new Literal("B.A_2"), new Literal("C.A_2")))), + new Implies(new Literal("B.A_2"), new Literal("A_2")), + new Implies(new Literal("C.A_2"), new Literal("A_2")), + + //second sub-subtree + new Implies(new Literal("C.A_2"), new Or(Arrays.asList(new Literal("D.C.A_2"), new Literal("E.C.A_2")))), + new Implies(new Literal("D.C.A_2"), new Literal("C.A_2")), + new Implies(new Literal("E.C.A_2"), new Literal("C.A_2")), + + new Implies(new Literal("root"), new AtLeast(0, Arrays.asList(new Literal("A_1"), new Literal("A_2")))) + + // missing + )); + + executeRecursiveTest(); + } + @Test void onlyRoot() { @@ -184,9 +321,15 @@ void withOneCardinalityFeature() { rootTree.mutate().toAndGroup(); // create and set cardinality for the child feature - IFeature childFeature = featureModel.mutate().addFeature("Test1"); - rootTree.mutate().addFeatureBelow(childFeature).mutate().setFeatureCardinality(Range.of(0, 2)); - + IFeature childFeature = featureModel.mutate().addFeature("A"); + IFeatureTree childFeatureTree1 = rootTree.mutate().addFeatureBelow(childFeature); + childFeatureTree1.mutate().setFeatureCardinality(Range.of(0, 2)); + + // add normal feature below + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + childFeatureTree1.mutate().addFeatureBelow(childFeature2); + + executeTest(); } @@ -197,14 +340,18 @@ void withTwoCardinalityFeatures() { rootTree.mutate().toAndGroup(); // create and set cardinality for the child feature - IFeature childFeature1 = featureModel.mutate().addFeature("Test1"); + IFeature childFeature1 = featureModel.mutate().addFeature("A"); IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); - IFeature childFeature2 = featureModel.mutate().addFeature("Test2"); + IFeature childFeature2 = featureModel.mutate().addFeature("B"); IFeatureTree childFeature2Tree = childFeature1Tree.mutate().addFeatureBelow(childFeature2); childFeature2Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + // add normal feature below + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + childFeature2Tree.mutate().addFeatureBelow(childFeature3); + executeTest(); } @@ -225,7 +372,22 @@ private void executeSimpleTest() { ComputeConstant computeConstant = new ComputeConstant(featureModel); ComputeFormula computeFormula = new ComputeFormula(computeConstant); - computeFormula.SetSimple(); + IFormula resultFormula = computeFormula + .set(ComputeFormula.SIMPLE_TRANSLATION, Boolean.TRUE) + .computeResult().get(); + + // assert + assertEquals(expected, resultFormula); + } + + private void executeRecursiveTest() { + + ComputeConstant computeConstant = new ComputeConstant(featureModel); + ComputeFormula computeFormula = new ComputeFormula(computeConstant); + + // recursive is default now +// computeFormula.setRecursive(); + IFormula resultFormula = computeFormula.computeResult().get(); // assert From 7fdcbee0eb501d3d4135f7f8a2e61a5c189f97f5 Mon Sep 17 00:00:00 2001 From: Lara Date: Wed, 8 Oct 2025 16:38:35 +0200 Subject: [PATCH 23/67] feat: starting to add support for tikz export --- .../tikz/TikzGraphicalFeatureModelFormat.java | 59 +++++ .../model/io/tikz/color/FeatureColor.java | 35 +++ .../io/tikz/format/IGraphicalFormat.java | 17 ++ .../model/io/tikz/format/TikzBodyFormat.java | 55 +++++ .../model/io/tikz/format/TikzHeadFormat.java | 21 ++ .../model/io/tikz/format/TikzMainFormat.java | 220 ++++++++++++++++++ src/main/resources/head.tex | 125 ++++++++++ .../feature/model/io/tikz/HeadLoaderTest.java | 25 ++ 8 files changed, 557 insertions(+) create mode 100644 src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java create mode 100644 src/main/java/de/featjar/feature/model/io/tikz/color/FeatureColor.java create mode 100644 src/main/java/de/featjar/feature/model/io/tikz/format/IGraphicalFormat.java create mode 100644 src/main/java/de/featjar/feature/model/io/tikz/format/TikzBodyFormat.java create mode 100644 src/main/java/de/featjar/feature/model/io/tikz/format/TikzHeadFormat.java create mode 100644 src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java create mode 100644 src/main/resources/head.tex create mode 100644 src/test/java/de/featjar/feature/model/io/tikz/HeadLoaderTest.java diff --git a/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java new file mode 100644 index 00000000..d20faa0e --- /dev/null +++ b/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java @@ -0,0 +1,59 @@ +package de.featjar.feature.model.io.tikz; + +import de.featjar.base.data.Problem; +import de.featjar.base.data.Result; +import de.featjar.base.io.format.IFormat; +import de.featjar.base.io.format.ParseException; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.io.tikz.format.TikzHeadFormat; +import de.featjar.feature.model.io.tikz.format.TikzMainFormat; + +import java.util.ArrayList; +import java.util.List; + +public class TikzGraphicalFeatureModelFormat implements IFormat { + + public static String LINE_SEPERATOR = System.lineSeparator(); + + private final boolean[] legend = new boolean[7]; + + private final StringBuilder stringBuilder; + private final IFeatureModel featureModel; + private final List problemList; + + public TikzGraphicalFeatureModelFormat(IFeatureModel featureModel) { + this.stringBuilder = new StringBuilder(); + this.featureModel = featureModel; + this.problemList = new ArrayList<>(); + } + + public Result serialize() { + stringBuilder.append("\\documentclass[border=5pt]{standalone}"); + stringBuilder.append(LINE_SEPERATOR); + TikzHeadFormat.header(stringBuilder); + stringBuilder.append("\\begin{document}" + LINE_SEPERATOR + " %---The Feature Diagram-----------------------------------------------------" + LINE_SEPERATOR); + new TikzMainFormat(featureModel, stringBuilder, problemList).printForest(); + stringBuilder.append(LINE_SEPERATOR); + stringBuilder.append("\t%---------------------------------------------------------------------------" + LINE_SEPERATOR + "\\end{document}"); + return Result.of(stringBuilder.toString()); + } + + public IFeatureModel getFeatureModel() { + return featureModel; + } + + public StringBuilder getStringBuilder() { + return stringBuilder; + } + + @Override + public String getFileExtension() { + return ".tex"; + } + + @Override + public String getName() { + return "LaTeX-Document with TikZ"; + } +} diff --git a/src/main/java/de/featjar/feature/model/io/tikz/color/FeatureColor.java b/src/main/java/de/featjar/feature/model/io/tikz/color/FeatureColor.java new file mode 100644 index 00000000..627d9ede --- /dev/null +++ b/src/main/java/de/featjar/feature/model/io/tikz/color/FeatureColor.java @@ -0,0 +1,35 @@ +package de.featjar.feature.model.io.tikz.color; + +public enum FeatureColor { + + RED("redColor"), + ORANGE("orangeColor"), + YELLOW("yellowColor"), + DARK_GREEN("darkGreenColor"), + LIGHT_GREEN("lightGreenColor"), + CYAN("cyanColor"), + LIGHT_GRAY("lightGrayColor"), + BLUE("blueColor"), + MAGENTA("magentaColor"), + PINK("pinkColor"), + NO_COLOR(""); + + public static String color(FeatureColor featureColor) { + for (FeatureColor featureColors : values()) { + if (featureColor.equals(featureColors)) { + return featureColors.getColor(); + } + } + return NO_COLOR.getColor(); + } + + final String color; + + FeatureColor(String color) { + this.color = color; + } + + public String getColor() { + return color; + } +} diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/IGraphicalFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/IGraphicalFormat.java new file mode 100644 index 00000000..66189124 --- /dev/null +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/IGraphicalFormat.java @@ -0,0 +1,17 @@ +package de.featjar.feature.model.io.tikz.format; + +public interface IGraphicalFormat { + + void write(); + + boolean supportWirte(); + + boolean supportRead(); + + String getSuffix(); + + String getName(); + + String getId(); + +} diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzBodyFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzBodyFormat.java new file mode 100644 index 00000000..731d0c30 --- /dev/null +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzBodyFormat.java @@ -0,0 +1,55 @@ +package de.featjar.feature.model.io.tikz.format; + +public class TikzBodyFormat implements IGraphicalFormat{ + + private final StringBuilder stringBuilder; + private final String fileName; + + public TikzBodyFormat(String fileName, StringBuilder stringBuilder) { + this.fileName = fileName; + this.stringBuilder = (stringBuilder == null) ? new StringBuilder() : stringBuilder; + } + + @Override + public void write() { + stringBuilder.append("\\documentclass[border=5pt]{standalone}") + .append(System.lineSeparator()); + stringBuilder.append("\\input{head.tex}") + .append(System.lineSeparator()); // Include head + stringBuilder.append("\\begin{document}") + .append(System.lineSeparator()) + .append(" "); + stringBuilder.append("\\sffamily") + .append(System.lineSeparator()); + stringBuilder.append(" \\input{") + .append(fileName) + .append("}") + .append(System.lineSeparator()); // Include main + stringBuilder.append("\\end{document}"); + } + + @Override + public boolean supportWirte() { + return true; + } + + @Override + public boolean supportRead() { + return false; + } + + @Override + public String getSuffix() { + return ".tex"; + } + + @Override + public String getName() { + return "LaTeX-Document with TikZ"; + } + + @Override + public String getId() { + return ""; + } +} diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzHeadFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzHeadFormat.java new file mode 100644 index 00000000..6bc80bc1 --- /dev/null +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzHeadFormat.java @@ -0,0 +1,21 @@ +package de.featjar.feature.model.io.tikz.format; + +import java.io.*; +import java.util.Objects; + +public class TikzHeadFormat { + + public static void header(StringBuilder stringBuilder) { + try (BufferedReader bufferedReader = new BufferedReader( + new InputStreamReader( + Objects.requireNonNull(TikzHeadFormat.class + .getClassLoader() + .getResourceAsStream("head.tlx"))))) { + + bufferedReader.lines() + .forEach(line -> stringBuilder.append(line).append("\n")); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java new file mode 100644 index 00000000..4d73817a --- /dev/null +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java @@ -0,0 +1,220 @@ +package de.featjar.feature.model.io.tikz.format; + +import de.featjar.base.data.Problem; +import de.featjar.feature.model.IConstraint; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.io.tikz.TikzGraphicalFeatureModelFormat; + +import java.util.HashSet; +import java.util.List; + +public class TikzMainFormat implements IGraphicalFormat{ + + private final boolean[] LEGEND = new boolean[7]; + + private final IFeatureModel featureModel; + private final StringBuilder stringBuilder; + private final List problemList; + + public TikzMainFormat(IFeatureModel featureModel, StringBuilder stringBuilder, List problemList) { + this.featureModel = featureModel; + this.stringBuilder = stringBuilder; + this.problemList = problemList; + } + + @Override + public void write() { + printForest(); + } + + @Override + public boolean supportWirte() { + return true; + } + + @Override + public boolean supportRead() { + return false; + } + + @Override + public String getSuffix() { + return ".tex"; + } + + @Override + public String getName() { + return "LaTeX-Document with TikZ"; + } + + @Override + public String getId() { + return ""; + } + + private void insertNodeHead(String featureName) { + stringBuilder.append("[").append(featureName); + IFeature feature = featureModel.getFeature(featureName).orElse(null); + + if (feature == null) { + problemList.add(new Problem("The feature " + featureName + " is null")); + return; + } + + if (feature.isAbstract()) { + stringBuilder.append(",abstract"); + LEGEND[0] = true; + } + if (feature.isConcrete()) { + stringBuilder.append(",concrete"); + LEGEND[1] = true; + } + + if (!isRootFeature(feature) && feature.getFeatureTree().isPresent() + && feature.getFeatureTree().get().getParentGroup().isEmpty()) { + if (feature.getFeatureTree().get().isMandatory()) { + stringBuilder.append(",mandatory"); + LEGEND[2] = true; + } else { + stringBuilder.append(",optional"); + LEGEND[3] = true; + } + } + } + + private boolean isRootFeature(IFeature feature) { + return featureModel.getRootFeatures().contains(feature); + } + + private void insertNodeTail() { + stringBuilder.append("]"); + } + + private void printTree() { + + } + + private int countFeatures() { + return featureModel.getFeatures().size(); + //return -1; // todo: change later + } + + private String getRoot() { + return "-1"; // todo: change later + } + + public void printForest() { + stringBuilder.append("\\begin{forest}" + TikzGraphicalFeatureModelFormat.LINE_SEPERATOR + "\tfeatureDiagram" + TikzGraphicalFeatureModelFormat.LINE_SEPERATOR + "\t"); + //printTree(getRoot(object), object, treeStringBuilder); + //postProcessing(treeStringBuilder); + stringBuilder.append("\t").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + /*if (!object.isLegendHidden()) { + printLegend(str, object); + } + + */ + //printConstraints(str, object); + stringBuilder.append("\\end{forest}"); + } + + private void printLegend() { + boolean check = false; + final StringBuilder sb = new StringBuilder(); + if (LEGEND[0] && LEGEND[1]) { + check = true; + sb.append(" \\node [abstract,label=right:Abstract Feature] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + LEGEND[0] = false; + sb.append(" \\node [concrete,label=right:Concrete Feature] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + LEGEND[1] = false; + } + if (LEGEND[0]) { + check = true; + sb.append(" \\node [abstract,label=right:Feature] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + LEGEND[0] = false; + } + if (LEGEND[1]) { + check = true; + sb.append(" \\node [concrete,label=right:Feature] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + LEGEND[1] = false; + } + if (LEGEND[2]) { + check = true; + sb.append(" \\node [mandatory,label=right:Mandatory] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + LEGEND[2] = false; + } + if (LEGEND[3]) { + check = true; + sb.append(" \\node [optional,label=right:Optional] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + LEGEND[3] = false; + } + if (LEGEND[4]) { + check = true; + // myString.append(" \\filldraw[drawColor] (0.45,0.15) ++ (225:0.3) arc[start angle=315,end angle=225,radius=0.2]; " + lnSep + // + " \\node [or,label=right:Or] {}; \\\\" + lnSep); + sb.append(" \\filldraw[drawColor] (0.1,0) - +(-0,-0.2) - +(0.2,-0.2)- +(0.1,0);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\draw[drawColor] (0.1,0) -- +(-0.2, -0.4);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\draw[drawColor] (0.1,0) -- +(0.2,-0.4);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\fill[drawColor] (0,-0.2) arc (240:300:0.2);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\node [or,label=right:Or Group] {}; \\\\"); + LEGEND[4] = false; + } + if (LEGEND[5]) { + check = true; + // myString.append(" \\draw[drawColor] (0.45,0.15) ++ (225:0.3) arc[start angle=315,end angle=225,radius=0.2] -- cycle; " + lnSep + // + " \\node [alternative,label=right:Alternative] {}; \\\\" + lnSep); + sb.append(" \\draw[drawColor] (0.1,0) -- +(-0.2, -0.4);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\draw[drawColor] (0.1,0) -- +(0.2,-0.4);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\draw[drawColor] (0,-0.2) arc (240:300:0.2);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\node [alternative,label=right:Alternative Group] {}; \\\\"); + LEGEND[5] = false; + } + if (LEGEND[6]) { + check = true; + sb.append(" \\node [hiddenNodes,label=center:1,label=right:Collapsed Nodes] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + LEGEND[6] = false; + } +/*Color + final ColorScheme colorScheme = FeatureColorManager.getCurrentColorScheme(graphicalFeatureModel.getFeatureModelManager().getSnapshot()); + int colorIndex = 1; + + for (final FeatureColor currentColor : new HashSet<>(colorScheme.getColors().values())) { + if (currentColor != FeatureColor.NO_COLOR) { + String meaning = currentColor.getMeaning(); + if (meaning.isEmpty()) { + meaning = "Custom Color " + String.format("%02d", colorIndex); + colorIndex++; + } + sb.append(" \\node [" + featureColorToTikzStyle(currentColor) + ",label=right:" + meaning + "] {}; \\\\" + lnSep); + } + } + + */ + + if (check) { + stringBuilder.append(" \\matrix [anchor=north west] at (current bounding box.north east) {").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\node [placeholder] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" };").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\matrix [draw=drawColor,anchor=north west] at (current bounding box.north east) {").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\node [label=center:\\underline{Legend:}] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + stringBuilder.append(sb); + stringBuilder.append(" };").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + check = false; + } else { + for (int i = 0; i < LEGEND.length; ++i) { + LEGEND[i] = false; + } + check = false; + } + } + + private void printConstraints() { + stringBuilder.append(" \\matrix [below=1mm of current bounding box] {").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + for (IConstraint constraint : featureModel.getConstraints()) { + String text = constraint.getFormula().print(); + text = text.replaceAll("\"([\\w\" ]+)\"", " \\\\text\\{$1\\} "); // wrap all words in \text{} // replace with $2 + text = text.replaceAll("\\s+", " "); // remove unnecessary whitespace characters + stringBuilder.append(" \\node {\\(").append(text).append("\\)}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + } + stringBuilder.append(" };").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + /*for (final IGraphicalConstraint constraint : graphicalFeatureModel.getConstraints()) { + String text = constraint.getObject().getNode().toString(NodeWriter.latexSymbols, true); + text = text.replaceAll("\"([\\w\" ]+)\"", " \\\\text\\{$1\\} "); // wrap all words in \text{} // replace with $2 + text = text.replaceAll("\\s+", " "); // remove unnecessary whitespace characters + str.append(" \\node {\\(" + text + "\\)}; \\\\" + lnSep); + } + str.append(" };" + lnSep); + + */ + } + +} diff --git a/src/main/resources/head.tex b/src/main/resources/head.tex new file mode 100644 index 00000000..dbba6b0e --- /dev/null +++ b/src/main/resources/head.tex @@ -0,0 +1,125 @@ +%---required packages & variable definitions------------------------------------ +\usepackage{forest} +\usepackage{amsmath} +\usepackage{xcolor} +\usetikzlibrary{angles} +\usetikzlibrary{positioning} +\definecolor{drawColor}{RGB}{128 128 128} +\newcommand{\circleSize}{0.25em} +\newcommand{\angleSize}{0.8em} +%------------------------------------------------------------------------------- +%---Define the style of the tree------------------------------------------------ +\forestset{ + /tikz/mandatory/.style={ + circle,fill=drawColor, + draw=drawColor, + inner sep=\circleSize + }, + /tikz/optional/.style={ + circle, + fill=white, + draw=drawColor, + inner sep=\circleSize + }, + featureDiagram/.style={ + for tree={ + minimum height = 0.6cm, + text depth = 0, + draw = drawColor, + edge = {draw=drawColor}, + parent anchor = south, + child anchor = north, + l sep = 2em, + s sep = 1em, + } + }, + /tikz/redColor/.style={ + fill = red!60, + draw = drawColor + }, + /tikz/orangeColor/.style={ + fill = orange!50, + draw = drawColor + }, + /tikz/yellowColor/.style={ + fill = yellow!50, + draw = drawColor + }, + /tikz/darkGreenColor/.style={ + fill = black!30!green, + draw = drawColor + }, + /tikz/lightGreenColor/.style={ + fill = green!30, + draw = drawColor + }, + /tikz/cyanColor/.style={ + fill = cyan!30, + draw = drawColor + }, + /tikz/lightGrayColor/.style={ + fill = black!10, + draw = drawColor + }, + /tikz/blueColor/.style={ + fill = blue!50, + draw = drawColor + }, + /tikz/magentaColor/.style={ + fill = magenta, + draw = drawColor + }, + /tikz/pinkColor/.style={ + fill = pink!90, + draw = drawColor + }, + /tikz/abstract/.style={ + fill = blue!85!cyan!5, + draw = drawColor + }, + /tikz/concrete/.style={ + fill = blue!85!cyan!20, + draw = drawColor + }, + mandatory/.style={ + edge label={node [mandatory] {} } + }, + optional/.style={ + edge label={node [optional] {} } + }, + or/.style={ + tikz+={ + \path (.parent) coordinate (A) -- (!u.children) coordinate (B) -- (!ul.parent) coordinate (C) pic[fill=drawColor, angle radius=\angleSize]{angle}; + } + }, + /tikz/or/.style={ + }, + alternative/.style={ + tikz+={ + \path (.parent) coordinate (A) -- (!u.children) coordinate (B) -- (!ul.parent) coordinate (C) pic[draw=drawColor, angle radius=\angleSize]{angle}; + } + }, + /tikz/alternative/.style={ + }, + /tikz/placeholder/.style={ + }, + collapsed/.style={ + rounded corners, + no edge, + for tree={ + fill opacity=0, + draw opacity=0, + l = 0em, + } + }, + /tikz/hiddenNodes/.style={ + midway, + rounded corners, + draw=drawColor, + fill=white, + minimum size = 1.2em, + minimum width = 0.8em, + scale=0.9 + }, +} +%------------------------------------------------------------------------------- diff --git a/src/test/java/de/featjar/feature/model/io/tikz/HeadLoaderTest.java b/src/test/java/de/featjar/feature/model/io/tikz/HeadLoaderTest.java new file mode 100644 index 00000000..0e7d0479 --- /dev/null +++ b/src/test/java/de/featjar/feature/model/io/tikz/HeadLoaderTest.java @@ -0,0 +1,25 @@ +package de.featjar.feature.model.io.tikz; + +import org.junit.jupiter.api.Test; +import java.io.*; + +public class HeadLoaderTest { + + @Test + public void loadHead() { + StringBuilder stringBuilder = new StringBuilder(); + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("head.tex"); + + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"))) { + String line; + while (((line = bufferedReader.readLine()) != null)) { + stringBuilder.append(line).append("\n"); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + System.out.print(stringBuilder.toString()); + } + +} From 275c30a92c47ed44100af2076a22294a5f8eb834 Mon Sep 17 00:00:00 2001 From: Malena Horstmann Date: Wed, 8 Oct 2025 17:01:00 +0200 Subject: [PATCH 24/67] Additional unit test for recursive compute formula --- .../model/transformer/ComputeFormulaTest.java | 47 ++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java index 51581592..f94f4dff 100644 --- a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java +++ b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java @@ -157,6 +157,51 @@ void simpleWithCardinalityAndChildGroup() { } + @Test + void withCardinalityAndChildInbetweenRecursive() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + IFeatureTree childFeature1Tree2 = childFeature1Tree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + IFeatureTree childFeature1Tree3 = childFeature1Tree2.mutate().addFeatureBelow(childFeature3); + childFeature1Tree3.mutate().setFeatureCardinality(Range.of(0, 2)); + + + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("B.A_1"), new Literal("A_1")), + new Implies(new Literal("C_1.B.A_1"), new Literal("B.A_1")), + new Implies(new Literal("C_2.B.A_1"), new Literal("B.A_1")), + new Implies(new Literal("C_2.B.A_1"), new Literal("C_1.B.A_1")), + new Implies(new Literal("B.A_1"), new AtLeast(0, Arrays.asList(new Literal("C_1.B.A_1"), new Literal("C_2.B.A_1")))), + + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("B.A_2"), new Literal("A_2")), + new Implies(new Literal("C_1.B.A_2"), new Literal("B.A_2")), + new Implies(new Literal("C_2.B.A_2"), new Literal("B.A_2")), + new Implies(new Literal("C_2.B.A_2"), new Literal("C_1.B.A_2")), + new Implies(new Literal("B.A_2"), new AtLeast(0, Arrays.asList(new Literal("C_1.B.A_2"), new Literal("C_2.B.A_2")))), + + new Implies(new Literal("root"), new AtLeast(0, Arrays.asList(new Literal("A_1"), new Literal("A_2")))) + + + )); + + executeTest(); + + } + @Test void withCardinalityAndChildGroupRecursive() { IFeatureTree rootTree = @@ -308,8 +353,6 @@ void withCardinalityGroup() { IFeature childFeature3 = featureModel.mutate().addFeature("Test3"); rootTree.mutate().addFeatureBelow(childFeature3); - - executeTest(); } From 3ab3cfa8661cd1e7cef0ab4822048524e08847cd Mon Sep 17 00:00:00 2001 From: Malena Horstmann Date: Wed, 8 Oct 2025 18:38:46 +0200 Subject: [PATCH 25/67] fix of root having a group and cleanup of unit tests --- .../model/transformer/ComputeFormula.java | 16 +- .../model/transformer/ComputeFormulaTest.java | 784 +++++++++--------- 2 files changed, 404 insertions(+), 396 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java index 6272e2fd..ef18463a 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java @@ -103,12 +103,16 @@ private void traverseFeatureModel(IFeatureModel featureModel, ArrayList constraints, IFeatureModel fm) { + private void addChildConstraints(IFeatureTree node, ArrayList constraints) { Literal parentLiteral = new Literal(getLiteralName(node)); @@ -148,7 +152,7 @@ private void addChildConstraints(IFeatureTree node, ArrayList constrai constraintGroupLiterals.add(currentLiteral); - addChildConstraints(cardinalityClone, constraints, fm); + addChildConstraints(cardinalityClone, constraints); } // TODO: check if 0 and do not add then constraints.add(new Implies(parentLiteral, new AtLeast(lowerBound, constraintGroupLiterals))); @@ -171,7 +175,7 @@ private void addChildConstraints(IFeatureTree node, ArrayList constrai // handle group handleGroups(childFeatureLiteral, child, constraints); - addChildConstraints(child, constraints, fm); + addChildConstraints(child, constraints); } } } @@ -217,7 +221,7 @@ private void handleGroups(Literal featureLiteral, IFeatureTree node, ArrayList computeConstant = new ComputeConstant(featureModel); - ComputeFormula computeFormula = new ComputeFormula(computeConstant); - - IFormula resultFormula = computeFormula.computeResult().get(); - - // assert - assertEquals(expected, resultFormula); - } - - private void executeSimpleTest() { - - ComputeConstant computeConstant = new ComputeConstant(featureModel); - ComputeFormula computeFormula = new ComputeFormula(computeConstant); - - IFormula resultFormula = computeFormula - .set(ComputeFormula.SIMPLE_TRANSLATION, Boolean.TRUE) - .computeResult().get(); - - // assert - assertEquals(expected, resultFormula); - } - - private void executeRecursiveTest() { - - ComputeConstant computeConstant = new ComputeConstant(featureModel); - ComputeFormula computeFormula = new ComputeFormula(computeConstant); - - // recursive is default now -// computeFormula.setRecursive(); - - IFormula resultFormula = computeFormula.computeResult().get(); - - // assert - assertEquals(expected, resultFormula); - } + new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("root"), new AtLeast(0, Arrays.asList(new Literal("A_1"), new Literal("A_2")))), + + new Implies(new Literal("root"), new Choose(1, Arrays.asList(new Literal("B"), new Literal("C")))), + new Implies(new Literal("B"), new Literal("root")), + new Implies(new Literal("C"), new Literal("root")))); + + executeSimpleTest(); + + } + + @Test + void withCardinalityAndChildInbetween() { + IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + IFeatureTree childFeature1Tree2 = childFeature1Tree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + IFeatureTree childFeature1Tree3 = childFeature1Tree2.mutate().addFeatureBelow(childFeature3); + childFeature1Tree3.mutate().setFeatureCardinality(Range.of(0, 2)); + + expected = new Reference(new And(new Literal("root"), new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("B.A_1"), new Literal("A_1")), + new Implies(new Literal("C_1.B.A_1"), new Literal("B.A_1")), + new Implies(new Literal("C_2.B.A_1"), new Literal("B.A_1")), + new Implies(new Literal("C_2.B.A_1"), new Literal("C_1.B.A_1")), + new Implies(new Literal("B.A_1"), + new AtLeast(0, Arrays.asList(new Literal("C_1.B.A_1"), new Literal("C_2.B.A_1")))), + + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("B.A_2"), new Literal("A_2")), + new Implies(new Literal("C_1.B.A_2"), new Literal("B.A_2")), + new Implies(new Literal("C_2.B.A_2"), new Literal("B.A_2")), + new Implies(new Literal("C_2.B.A_2"), new Literal("C_1.B.A_2")), + new Implies(new Literal("B.A_2"), + new AtLeast(0, Arrays.asList(new Literal("C_1.B.A_2"), new Literal("C_2.B.A_2")))), + + new Implies(new Literal("root"), new AtLeast(0, Arrays.asList(new Literal("A_1"), new Literal("A_2")))) + + )); + + executeTest(); + + } + + @Test + void withTwoGroups() { + IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAndGroup(); + + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeatureTree1 = rootTree.mutate().addFeatureBelow(childFeature1); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + childFeatureTree1.addChild(rootTree.mutate().addFeatureBelow(childFeature2)); + + childFeatureTree1.mutate().toAlternativeGroup(); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + IFeatureTree childFeatureTree2 = rootTree.mutate().addFeatureBelow(childFeature3); + + IFeature childFeature4 = featureModel.mutate().addFeature("D"); + childFeatureTree2.addChild(rootTree.mutate().addFeatureBelow(childFeature4)); + + childFeatureTree2.mutate().toOrGroup(); + + expected = new Reference(new And(new Literal("root"), new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("B.A_1"), new Literal("A_1")), + new Implies(new Literal("C_1.B.A_1"), new Literal("B.A_1")), + new Implies(new Literal("C_2.B.A_1"), new Literal("B.A_1")), + new Implies(new Literal("C_2.B.A_1"), new Literal("C_1.B.A_1")), + new Implies(new Literal("B.A_1"), + new AtLeast(0, Arrays.asList(new Literal("C_1.B.A_1"), new Literal("C_2.B.A_1")))), + + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("B.A_2"), new Literal("A_2")), + new Implies(new Literal("C_1.B.A_2"), new Literal("B.A_2")), + new Implies(new Literal("C_2.B.A_2"), new Literal("B.A_2")), + new Implies(new Literal("C_2.B.A_2"), new Literal("C_1.B.A_2")), + new Implies(new Literal("B.A_2"), + new AtLeast(0, Arrays.asList(new Literal("C_1.B.A_2"), new Literal("C_2.B.A_2")))), + + new Implies(new Literal("root"), new AtLeast(0, Arrays.asList(new Literal("A_1"), new Literal("A_2")))) + + )); + + executeTest(); + + } + + @Test + void withCardinalityAndChildGroup() { + IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + childFeature1Tree.mutate().toAlternativeGroup(); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + childFeature1Tree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + childFeature1Tree.mutate().addFeatureBelow(childFeature3); + + expected = new Reference(new And(new Literal("root"), new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("A_1"), + new Choose(1, Arrays.asList(new Literal("B.A_1"), new Literal("C.A_1")))), + new Implies(new Literal("B.A_1"), new Literal("A_1")), + new Implies(new Literal("C.A_1"), new Literal("A_1")), + + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("A_2"), + new Choose(1, Arrays.asList(new Literal("B.A_2"), new Literal("C.A_2")))), + new Implies(new Literal("B.A_2"), new Literal("A_2")), + new Implies(new Literal("C.A_2"), new Literal("A_2")), + + new Implies(new Literal("root"), + new AtLeast(0, Arrays.asList(new Literal("A_1"), new Literal("A_2")))))); + + executeTest(); + } + + @Test + void withCardinalityAndChildChildGroup() { + IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + childFeature1Tree.mutate().toAlternativeGroup(); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + childFeature1Tree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + IFeatureTree childFeature1Tree2 = childFeature1Tree.mutate().addFeatureBelow(childFeature3); + + childFeature1Tree2.mutate().toOrGroup(); + + IFeature childFeature4 = featureModel.mutate().addFeature("D"); + childFeature1Tree2.mutate().addFeatureBelow(childFeature4); + + IFeature childFeature5 = featureModel.mutate().addFeature("E"); + childFeature1Tree2.mutate().addFeatureBelow(childFeature5); + + expected = new Reference(new And(new Literal("root"), new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("A_1"), + new Choose(1, Arrays.asList(new Literal("B.A_1"), new Literal("C.A_1")))), + new Implies(new Literal("B.A_1"), new Literal("A_1")), + new Implies(new Literal("C.A_1"), new Literal("A_1")), + + // sub-subtree + new Implies(new Literal("C.A_1"), + new Or(Arrays.asList(new Literal("D.C.A_1"), new Literal("E.C.A_1")))), + new Implies(new Literal("D.C.A_1"), new Literal("C.A_1")), + new Implies(new Literal("E.C.A_1"), new Literal("C.A_1")), + + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("A_2"), + new Choose(1, Arrays.asList(new Literal("B.A_2"), new Literal("C.A_2")))), + new Implies(new Literal("B.A_2"), new Literal("A_2")), + new Implies(new Literal("C.A_2"), new Literal("A_2")), + + // second sub-subtree + new Implies(new Literal("C.A_2"), + new Or(Arrays.asList(new Literal("D.C.A_2"), new Literal("E.C.A_2")))), + new Implies(new Literal("D.C.A_2"), new Literal("C.A_2")), + new Implies(new Literal("E.C.A_2"), new Literal("C.A_2")), + + new Implies(new Literal("root"), new AtLeast(0, Arrays.asList(new Literal("A_1"), new Literal("A_2")))) + + // missing + )); + + executeTest(); + } + + @Test + void onlyRoot() { + + // root and nothing else + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + + // root must be selected + expected = new Reference(new And(new Literal("root"))); + + executeTest(); + } + + @Test + void oneFeature() { + + // root + IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAndGroup(); + + // TODO: check if setting root feature is missing here or if compute misses + // adding root feature literal + + // create and add our only child + IFeature childFeature = featureModel.mutate().addFeature("Test1"); + rootTree.mutate().addFeatureBelow(childFeature); + + expected = new Reference(new And(new Literal("root"), new Implies(new Literal("Test1"), new Literal("root")))); + + executeTest(); + } + + @Test + void withCardinalityGroup() { + IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toCardinalityGroup(Range.of(2, 3)); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + rootTree.mutate().addFeatureBelow(childFeature1); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + rootTree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + rootTree.mutate().addFeatureBelow(childFeature3); + + expected = new Reference(new And(new Literal("root"), + new Implies(new Literal("root"), + new Between(2, 3, Arrays.asList(new Literal("A"), new Literal("B"), new Literal("C")))), + new Implies(new Literal("A"), new Literal("root")), new Implies(new Literal("B"), new Literal("root")), + new Implies(new Literal("C"), new Literal("root")))); + + executeTest(); + } + + @Test + void withOneCardinalityFeature() { + IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature = featureModel.mutate().addFeature("A"); + IFeatureTree childFeatureTree1 = rootTree.mutate().addFeatureBelow(childFeature); + childFeatureTree1.mutate().setFeatureCardinality(Range.of(0, 2)); + + // add normal feature below + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + childFeatureTree1.mutate().addFeatureBelow(childFeature2); + + expected = new Reference(new And(new Literal("root"), new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("B.A_1"), new Literal("A_1")), + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("B.A_2"), new Literal("A_2")), + + new Implies(new Literal("root"), new AtLeast(0, Arrays.asList(new Literal("A_1"), new Literal("A_2")))) + + )); + + executeTest(); + } + + private void executeTest() { + + ComputeConstant computeConstant = new ComputeConstant(featureModel); + ComputeFormula computeFormula = new ComputeFormula(computeConstant); + + IFormula resultFormula = computeFormula.computeResult().get(); + + // assert + assertEquals(expected, resultFormula); + } + + private void executeSimpleTest() { + + ComputeConstant computeConstant = new ComputeConstant(featureModel); + ComputeFormula computeFormula = new ComputeFormula(computeConstant); + + IFormula resultFormula = computeFormula.set(ComputeFormula.SIMPLE_TRANSLATION, Boolean.TRUE).computeResult() + .get(); + + // assert + assertEquals(expected, resultFormula); + } } From b81f5a65d8635e3317a8ed4352e9b7c541e2eb31 Mon Sep 17 00:00:00 2001 From: Malena Horstmann Date: Thu, 9 Oct 2025 10:11:06 +0200 Subject: [PATCH 26/67] finished cardinality translation and additional unit test --- .../model/transformer/CardinalityObject.java | 21 -- .../model/transformer/ComputeFormula.java | 26 +- .../transformer/ComputeFormulaVisitor.java | 226 ------------------ .../ComputeSimpleFormulaVisitor.java | 54 ++--- .../model/transformer/ComputeFormulaTest.java | 111 +++------ 5 files changed, 65 insertions(+), 373 deletions(-) delete mode 100644 src/main/java/de/featjar/feature/model/transformer/CardinalityObject.java delete mode 100644 src/main/java/de/featjar/feature/model/transformer/ComputeFormulaVisitor.java diff --git a/src/main/java/de/featjar/feature/model/transformer/CardinalityObject.java b/src/main/java/de/featjar/feature/model/transformer/CardinalityObject.java deleted file mode 100644 index 5b918a92..00000000 --- a/src/main/java/de/featjar/feature/model/transformer/CardinalityObject.java +++ /dev/null @@ -1,21 +0,0 @@ -package de.featjar.feature.model.transformer; - -public class CardinalityObject { - - String name; - int number; - - public CardinalityObject(String name, int number) { - - this.name = name; - this.number = number; - } - - public String getName() { - return name; - } - - public int getNumber() { - return number; - } -} diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java index ef18463a..8a0fe13f 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java @@ -59,7 +59,6 @@ public class ComputeFormula extends AComputation { protected static final Dependency SIMPLE_TRANSLATION = Dependency.newDependency(Boolean.class); static Attribute literalNameAttribute = new Attribute<>("literalName", String.class); -// protected boolean traverseRecursive = false; public ComputeFormula(IComputation formula) { super(formula, Computations.of(Boolean.FALSE)); @@ -69,10 +68,6 @@ protected ComputeFormula(ComputeFormula other) { super(other); } -// public void setRecursive() { -// traverseRecursive = true; -// } - @Override public Result compute(List dependencyList, Progress progress) { IFeatureModel featureModel = FEATURE_MODEL.get(dependencyList); @@ -81,15 +76,11 @@ public Result compute(List dependencyList, Progress progress) IFeatureTree iFeatureTree = featureModel.getRoots().get(0); -// if (traverseRecursive) { -// } else { if (SIMPLE_TRANSLATION.get(dependencyList)) { Trees.traverse(iFeatureTree, new ComputeSimpleFormulaVisitor(constraints, variables)); } else { traverseFeatureModel(featureModel, constraints, variables); -// Trees.traverse(iFeatureTree, new ComputeFormulaVisitor(constraints, variables)); } -// } Reference reference = new Reference(new And(constraints)); reference.setFreeVariables(variables); @@ -99,13 +90,12 @@ public Result compute(List dependencyList, Progress progress) private void traverseFeatureModel(IFeatureModel featureModel, ArrayList constraints, HashSet variables) { - // copy FM to not change the original one - IFeatureModel temporaryFM = featureModel.clone(); - - for (IFeatureTree root : temporaryFM.getRoots()) { + for (IFeatureTree root : featureModel.getRoots()) { Literal rootLiteral = new Literal(root.getFeature().getName().orElse("")); - constraints.add(rootLiteral); + if(root.isMandatory()) { + constraints.add(rootLiteral); + } handleGroups(rootLiteral, root, constraints); addChildConstraints(root, constraints); @@ -132,11 +122,10 @@ private void addChildConstraints(IFeatureTree node, ArrayList constrai literalName += "." + getLiteralName(node); } + // clone only tree for traversal, not its children IFeatureTree cardinalityClone = child.cloneTree(); cardinalityClone.mutate().setAttributeValue(literalNameAttribute, literalName); -// node.addChild(cardinalityClone); - Literal currentLiteral = new Literal(literalName); // add all the constraints @@ -154,8 +143,9 @@ private void addChildConstraints(IFeatureTree node, ArrayList constrai addChildConstraints(cardinalityClone, constraints); } - // TODO: check if 0 and do not add then - constraints.add(new Implies(parentLiteral, new AtLeast(lowerBound, constraintGroupLiterals))); + // check if 0 and do not add implication + if(lowerBound != 0) + constraints.add(new Implies(parentLiteral, new AtLeast(lowerBound, constraintGroupLiterals))); return; } else { diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormulaVisitor.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormulaVisitor.java deleted file mode 100644 index 81c7b58b..00000000 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormulaVisitor.java +++ /dev/null @@ -1,226 +0,0 @@ -package de.featjar.feature.model.transformer; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Stack; - -import de.featjar.base.data.Range; -import de.featjar.base.data.Result; -import de.featjar.base.tree.visitor.ITreeVisitor; -import de.featjar.feature.model.FeatureTree.Group; -import de.featjar.feature.model.IFeature; -import de.featjar.feature.model.IFeatureTree; -import de.featjar.formula.structure.Expressions; -import de.featjar.formula.structure.IFormula; -import de.featjar.formula.structure.connective.AtLeast; -import de.featjar.formula.structure.connective.AtMost; -import de.featjar.formula.structure.connective.Between; -import de.featjar.formula.structure.connective.Choose; -import de.featjar.formula.structure.connective.Implies; -import de.featjar.formula.structure.connective.Or; -import de.featjar.formula.structure.predicate.Literal; -import de.featjar.formula.structure.term.value.Variable; - -public class ComputeFormulaVisitor implements ITreeVisitor { - - protected ArrayList constraints = new ArrayList<>(); - protected HashSet variables = new HashSet<>(); - protected Stack cardinalityStack = new Stack<>(); - - public ComputeFormulaVisitor(ArrayList constraints, HashSet variables) { - - this.constraints = constraints; - this.variables = variables; - } - - @Override - public TraversalAction firstVisit(List path) { - IFeatureTree node = ITreeVisitor.getCurrentNode(path); - - // TODO use better error value - IFeature feature = node.getFeature(); - String featureName = feature.getName().orElse(""); - // TODO: do not add variable if its a cardinality var. Add duplicates instead - Variable variable = new Variable(featureName, feature.getType()); - variables.add(variable); - - // TODO take featureRanges into Account - Result potentialParentTree = node.getParent(); - Literal featureLiteral = Expressions.literal(featureName); - if (potentialParentTree.isEmpty()) { - handleRoot(featureLiteral, node); - } - else if(node.getFeatureCardinalityUpperBound() > 1) { - handleCardinalityFeature(featureLiteral, node); - } - else{ - handleParent(featureLiteral, node); - } - - handleGroups(featureLiteral, node); - - - return ITreeVisitor.super.firstVisit(path); - } - - @Override - public TraversalAction lastVisit(List path) { - // TODO Auto-generated method stub - return ITreeVisitor.super.lastVisit(path); - } - - private void handleParent(Literal featureLiteral, IFeatureTree node) { - constraints.add(new Implies( - featureLiteral, - Expressions.literal( - node.getParent().get().getFeature().getName().orElse("")))); - } - - private void handleRoot(Literal featureLiteral, IFeatureTree node) { - if (node.isMandatory()) { - constraints.add(featureLiteral); - } - } - - private void handleCardinalityFeature(Literal featureLiteral, IFeatureTree node) { - - // step 0: check stack - if (cardinalityStack.empty()) { - int lowerBound = node.getFeatureCardinalityLowerBound(); - int upperBound = node.getFeatureCardinalityUpperBound(); - - ArrayList featureList = new ArrayList(); - - // step 2: add literals and implication to parent - String literalName = ""; - for (int i = 1; i<= upperBound; i++) { - - literalName = node.getFeature().getName().get() + "_" + i; - featureLiteral = new Literal(literalName); - handleParent(featureLiteral, node); - - if (i > 1) { - // step 3: add to implication chain - IFormula previousLiteral = featureList.get(featureList.size()-1); - constraints.add(new Implies(featureLiteral, previousLiteral)); - } - - featureList.add(featureLiteral); - } - - // step 4: add cardinality constraint - // TODO: feature literal must be parent! - if (lowerBound > 0) { - if (upperBound != Range.OPEN) { - constraints.add(new Implies( - featureLiteral, new Between(lowerBound, upperBound, featureList))); - } else { - constraints.add(new Implies(featureLiteral, new AtMost(upperBound, featureList))); - } - } else { - if (upperBound != Range.OPEN) { - constraints.add(new Implies(featureLiteral, new AtLeast(lowerBound, featureList))); - } - } - } else { - int lowerBound = node.getFeatureCardinalityLowerBound(); - int upperBound = node.getFeatureCardinalityUpperBound(); - - CardinalityObject parentCardObject = cardinalityStack.pop(); - int parentUpperBound = parentCardObject.getNumber(); - - String literalName = ""; - for (int i = 1; i <= parentUpperBound; i++) { - - ArrayList featureList = new ArrayList(); - for (int j = 1; j <= upperBound; j++) { - - literalName = node.getFeature().getName().get() + "_" + i + "_" + j; - featureLiteral = new Literal(literalName); - handleParent(featureLiteral, node); - - if (j > 1) { - // step 3: add to implication chain - IFormula previousLiteral = featureList.get(featureList.size()-1); - constraints.add(new Implies(featureLiteral, previousLiteral)); - } - - featureList.add(featureLiteral); - } - - // step 4: add cardinality constraint - if (lowerBound > 0) { - if (upperBound != Range.OPEN) { - constraints.add(new Implies( - featureLiteral, new Between(lowerBound, upperBound, featureList))); - } else { - constraints.add(new Implies(featureLiteral, new AtMost(upperBound, featureList))); - } - } else { - if (upperBound != Range.OPEN) { - constraints.add(new Implies(featureLiteral, new AtLeast(lowerBound, featureList))); - } - } - - } - - } - - - // step 1: add cardinality feature to stack: - cardinalityStack.add(new CardinalityObject(node.getFeature().getName().get(), node.getFeatureCardinalityUpperBound())); - - - } - - private void handleGroups(Literal featureLiteral, IFeatureTree node) { - List childrenGroups = node.getChildrenGroups(); - int groupCount = childrenGroups.size(); - ArrayList> groupLiterals = new ArrayList<>(groupCount); - for (int i = 0; i < groupCount; i++) { - groupLiterals.add(null); - } - List children = node.getChildren(); - for (IFeatureTree childNode : children) { - Literal childLiteral = - Expressions.literal(childNode.getFeature().getName().orElse("")); - - if (childNode.isMandatory()) { - constraints.add(new Implies(featureLiteral, childLiteral)); - } - - int groupID = childNode.getParentGroupID(); - List list = groupLiterals.get(groupID); - if (list == null) { - groupLiterals.set(groupID, list = new ArrayList<>()); - } - list.add(childLiteral); - } - for (int i = 0; i < groupCount; i++) { - Group group = childrenGroups.get(i); - if (group != null) { - if (group.isOr()) { - constraints.add(new Implies(featureLiteral, new Or(groupLiterals.get(i)))); - } else if (group.isAlternative()) { - constraints.add(new Implies(featureLiteral, new Choose(1, groupLiterals.get(i)))); - } else { - int lowerBound = group.getLowerBound(); - int upperBound = group.getUpperBound(); - if (lowerBound > 0) { - if (upperBound != Range.OPEN) { - constraints.add(new Implies( - featureLiteral, new Between(lowerBound, upperBound, groupLiterals.get(i)))); - } else { - constraints.add(new Implies(featureLiteral, new AtMost(upperBound, groupLiterals.get(i)))); - } - } else { - if (upperBound != Range.OPEN) { - constraints.add(new Implies(featureLiteral, new AtLeast(lowerBound, groupLiterals.get(i)))); - } - } - } - } - } - } -} diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java b/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java index 92fc1adb..90f1f105 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java @@ -22,9 +22,10 @@ import de.featjar.formula.structure.term.value.Variable; /** - * This visitor implements a simple translation of IFeatureModel to boolean formula. - * In this implementation, a cardinality feature can not be a parent. The next non-cardinality - * feature will be the parent instead within the boolean representation. + * This visitor implements a simple translation of IFeatureModel to boolean + * formula. In this implementation, a cardinality feature can not be a parent. + * The next non-cardinality feature will be the parent instead within the + * boolean representation. */ public class ComputeSimpleFormulaVisitor implements ITreeVisitor { @@ -57,7 +58,7 @@ public TraversalAction firstVisit(List path) { handleCardinalityFeature(featureLiteral, node); } else { handleParent(featureLiteral, node); - } + } handleGroups(featureLiteral, node); @@ -83,7 +84,7 @@ private void handleCardinalityFeature(Literal featureLiteral, IFeatureTree node) ArrayList featureList = new ArrayList(); - // step 2: add literals and implication to parent + // add literals and implication to parent String literalName = ""; Literal parentLiteral = getNextNonCardinalityParent(node); for (int i = 1; i <= upperBound; i++) { @@ -93,7 +94,7 @@ private void handleCardinalityFeature(Literal featureLiteral, IFeatureTree node) handleParent(featureLiteral, node); if (i > 1) { - // step 3: add to implication chain + // add to implication chain IFormula previousLiteral = featureList.get(featureList.size() - 1); constraints.add(new Implies(featureLiteral, previousLiteral)); } @@ -101,31 +102,23 @@ private void handleCardinalityFeature(Literal featureLiteral, IFeatureTree node) featureList.add(featureLiteral); } - // step 4: add cardinality constraint -// Literal parentLiteral = Expressions.literal(node.getParent().get().getFeature().getName().orElse("")); - - if (lowerBound > 0) { - if (upperBound != Range.OPEN) { - constraints.add(new Implies(parentLiteral, new Between(lowerBound, upperBound, featureList))); - } else { - constraints.add(new Implies(parentLiteral, new AtMost(upperBound, featureList))); - } - } else { - if (upperBound != Range.OPEN) { - constraints.add(new Implies(parentLiteral, new AtLeast(lowerBound, featureList))); - } - } + // add cardinality constraint + // check if 0 and do not add implication + if (lowerBound != 0) + constraints.add(new Implies(parentLiteral, new AtLeast(lowerBound, featureList))); + } private Literal getNextNonCardinalityParent(IFeatureTree node) { - - // TODO: if it is possible that root can be as well a cardinality feature - there must be an alternative + + // if it is possible that root can be as well a cardinality feature - there must + // be an alternative node = node.getParent().get(); - + if (node.getFeatureCardinalityUpperBound() > 1) { return getNextNonCardinalityParent(node); } - + return Expressions.literal(node.getFeature().getName().orElse("")); } @@ -136,14 +129,13 @@ private void handleGroups(Literal featureLiteral, IFeatureTree node) { for (int i = 0; i < groupCount; i++) { groupLiterals.add(null); } - - // if node is cardinality feature, set feature literal to parent with no cardinality - if(node.getFeatureCardinalityUpperBound() > 1) { + + // if node is cardinality feature, set feature literal to parent with no + // cardinality + if (node.getFeatureCardinalityUpperBound() > 1) { featureLiteral = getNextNonCardinalityParent(node); } - - - // TODO: check for cross-tree-constraints related to original feature and also copy it to match each newly created pseudo-literal + List children = node.getChildren(); for (IFeatureTree childNode : children) { Literal childLiteral = Expressions.literal(childNode.getFeature().getName().orElse("")); @@ -159,7 +151,7 @@ private void handleGroups(Literal featureLiteral, IFeatureTree node) { } list.add(childLiteral); } - + for (int i = 0; i < groupCount; i++) { Group group = childrenGroups.get(i); if (group != null) { diff --git a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java index 0e82f4fe..9345e503 100644 --- a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java +++ b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java @@ -36,7 +36,6 @@ import de.featjar.feature.model.IFeatureTree; import de.featjar.formula.structure.IFormula; import de.featjar.formula.structure.connective.And; -import de.featjar.formula.structure.connective.AtLeast; import de.featjar.formula.structure.connective.Between; import de.featjar.formula.structure.connective.Choose; import de.featjar.formula.structure.connective.Implies; @@ -56,6 +55,7 @@ public void createFeatureModel() { @Test void simpleWithTwoCardinalies() { IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); rootTree.mutate().toAndGroup(); // create and set cardinality for the child feature @@ -67,25 +67,21 @@ void simpleWithTwoCardinalies() { IFeatureTree childFeature2Tree = childFeature1Tree.mutate().addFeatureBelow(childFeature2); childFeature2Tree.mutate().setFeatureCardinality(Range.of(0, 2)); - expected = new Reference(new And( - new Literal("root"), - new Implies(new Literal("A_1"), new Literal("root")), + expected = new Reference(new And(new Literal("root"), new Implies(new Literal("A_1"), new Literal("root")), new Implies(new Literal("A_2"), new Literal("root")), new Implies(new Literal("A_2"), new Literal("A_1")), - new Implies(new Literal("root"), new AtLeast(0, Arrays.asList(new Literal("A_1"), new Literal("A_2")))), new Implies(new Literal("B_1"), new Literal("root")), new Implies(new Literal("B_2"), new Literal("root")), - new Implies(new Literal("B_2"), new Literal("B_1")), new Implies(new Literal("root"), - new AtLeast(0, Arrays.asList(new Literal("B_1"), new Literal("B_2")))))); + new Implies(new Literal("B_2"), new Literal("B_1")))); executeSimpleTest(); - } @Test void withTwoCardinalies() { IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); rootTree.mutate().toAndGroup(); // create and set cardinality for the child feature @@ -101,27 +97,20 @@ void withTwoCardinalies() { new Implies(new Literal("B_1.A_1"), new Literal("A_1")), new Implies(new Literal("B_2.A_1"), new Literal("A_1")), new Implies(new Literal("B_2.A_1"), new Literal("B_1.A_1")), - new Implies(new Literal("A_1"), - new AtLeast(0, Arrays.asList(new Literal("B_1.A_1"), new Literal("B_2.A_1")))), new Implies(new Literal("A_2"), new Literal("root")), new Implies(new Literal("A_2"), new Literal("A_1")), new Implies(new Literal("B_1.A_2"), new Literal("A_2")), new Implies(new Literal("B_2.A_2"), new Literal("A_2")), - new Implies(new Literal("B_2.A_2"), new Literal("B_1.A_2")), - new Implies(new Literal("A_2"), - new AtLeast(0, Arrays.asList(new Literal("B_1.A_2"), new Literal("B_2.A_2")))), - - new Implies(new Literal("root"), - new AtLeast(0, Arrays.asList(new Literal("A_1"), new Literal("A_2")))))); + new Implies(new Literal("B_2.A_2"), new Literal("B_1.A_2")))); executeTest(); - } @Test void simpleWithCardinalityAndChildGroup() { IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); rootTree.mutate().toAndGroup(); // create and set cardinality for the child feature @@ -137,24 +126,21 @@ void simpleWithCardinalityAndChildGroup() { IFeature childFeature3 = featureModel.mutate().addFeature("C"); childFeature1Tree.mutate().addFeatureBelow(childFeature3); - expected = new Reference(new And( - new Literal("root"), - new Implies(new Literal("A_1"), new Literal("root")), + expected = new Reference(new And(new Literal("root"), new Implies(new Literal("A_1"), new Literal("root")), new Implies(new Literal("A_2"), new Literal("root")), new Implies(new Literal("A_2"), new Literal("A_1")), - new Implies(new Literal("root"), new AtLeast(0, Arrays.asList(new Literal("A_1"), new Literal("A_2")))), new Implies(new Literal("root"), new Choose(1, Arrays.asList(new Literal("B"), new Literal("C")))), new Implies(new Literal("B"), new Literal("root")), new Implies(new Literal("C"), new Literal("root")))); executeSimpleTest(); - } @Test void withCardinalityAndChildInbetween() { IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); rootTree.mutate().toAndGroup(); // create and set cardinality for the child feature @@ -174,21 +160,13 @@ void withCardinalityAndChildInbetween() { new Implies(new Literal("C_1.B.A_1"), new Literal("B.A_1")), new Implies(new Literal("C_2.B.A_1"), new Literal("B.A_1")), new Implies(new Literal("C_2.B.A_1"), new Literal("C_1.B.A_1")), - new Implies(new Literal("B.A_1"), - new AtLeast(0, Arrays.asList(new Literal("C_1.B.A_1"), new Literal("C_2.B.A_1")))), new Implies(new Literal("A_2"), new Literal("root")), new Implies(new Literal("A_2"), new Literal("A_1")), new Implies(new Literal("B.A_2"), new Literal("A_2")), new Implies(new Literal("C_1.B.A_2"), new Literal("B.A_2")), new Implies(new Literal("C_2.B.A_2"), new Literal("B.A_2")), - new Implies(new Literal("C_2.B.A_2"), new Literal("C_1.B.A_2")), - new Implies(new Literal("B.A_2"), - new AtLeast(0, Arrays.asList(new Literal("C_1.B.A_2"), new Literal("C_2.B.A_2")))), - - new Implies(new Literal("root"), new AtLeast(0, Arrays.asList(new Literal("A_1"), new Literal("A_2")))) - - )); + new Implies(new Literal("C_2.B.A_2"), new Literal("C_1.B.A_2")))); executeTest(); @@ -197,44 +175,33 @@ void withCardinalityAndChildInbetween() { @Test void withTwoGroups() { IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); rootTree.mutate().toAndGroup(); IFeature childFeature1 = featureModel.mutate().addFeature("A"); - IFeatureTree childFeatureTree1 = rootTree.mutate().addFeatureBelow(childFeature1); + rootTree.mutate().addFeatureBelow(childFeature1); IFeature childFeature2 = featureModel.mutate().addFeature("B"); - childFeatureTree1.addChild(rootTree.mutate().addFeatureBelow(childFeature2)); + rootTree.mutate().addFeatureBelow(childFeature2); - childFeatureTree1.mutate().toAlternativeGroup(); + rootTree.mutate().toAlternativeGroup(); + int orGroupId = rootTree.mutate().addOrGroup(); IFeature childFeature3 = featureModel.mutate().addFeature("C"); - IFeatureTree childFeatureTree2 = rootTree.mutate().addFeatureBelow(childFeature3); + IFeatureTree childFeatureTree3 = rootTree.mutate().addFeatureBelow(childFeature3); + childFeatureTree3.mutate().setParentGroupID(orGroupId); IFeature childFeature4 = featureModel.mutate().addFeature("D"); - childFeatureTree2.addChild(rootTree.mutate().addFeatureBelow(childFeature4)); - - childFeatureTree2.mutate().toOrGroup(); - - expected = new Reference(new And(new Literal("root"), new Implies(new Literal("A_1"), new Literal("root")), - new Implies(new Literal("B.A_1"), new Literal("A_1")), - new Implies(new Literal("C_1.B.A_1"), new Literal("B.A_1")), - new Implies(new Literal("C_2.B.A_1"), new Literal("B.A_1")), - new Implies(new Literal("C_2.B.A_1"), new Literal("C_1.B.A_1")), - new Implies(new Literal("B.A_1"), - new AtLeast(0, Arrays.asList(new Literal("C_1.B.A_1"), new Literal("C_2.B.A_1")))), - - new Implies(new Literal("A_2"), new Literal("root")), - new Implies(new Literal("A_2"), new Literal("A_1")), - new Implies(new Literal("B.A_2"), new Literal("A_2")), - new Implies(new Literal("C_1.B.A_2"), new Literal("B.A_2")), - new Implies(new Literal("C_2.B.A_2"), new Literal("B.A_2")), - new Implies(new Literal("C_2.B.A_2"), new Literal("C_1.B.A_2")), - new Implies(new Literal("B.A_2"), - new AtLeast(0, Arrays.asList(new Literal("C_1.B.A_2"), new Literal("C_2.B.A_2")))), + IFeatureTree addFeatureBelow4 = rootTree.mutate().addFeatureBelow(childFeature4); + addFeatureBelow4.mutate().setParentGroupID(orGroupId); - new Implies(new Literal("root"), new AtLeast(0, Arrays.asList(new Literal("A_1"), new Literal("A_2")))) + expected = new Reference(new And(new Literal("root"), + new Implies(new Literal("root"), new Choose(1, Arrays.asList(new Literal("A"), new Literal("B")))), + new Implies(new Literal("root"), new Or(Arrays.asList(new Literal("C"), new Literal("D")))), - )); + new Implies(new Literal("A"), new Literal("root")), new Implies(new Literal("B"), new Literal("root")), + new Implies(new Literal("C"), new Literal("root")), + new Implies(new Literal("D"), new Literal("root")))); executeTest(); @@ -243,6 +210,7 @@ void withTwoGroups() { @Test void withCardinalityAndChildGroup() { IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); rootTree.mutate().toAndGroup(); // create and set cardinality for the child feature @@ -269,10 +237,7 @@ void withCardinalityAndChildGroup() { new Implies(new Literal("A_2"), new Choose(1, Arrays.asList(new Literal("B.A_2"), new Literal("C.A_2")))), new Implies(new Literal("B.A_2"), new Literal("A_2")), - new Implies(new Literal("C.A_2"), new Literal("A_2")), - - new Implies(new Literal("root"), - new AtLeast(0, Arrays.asList(new Literal("A_1"), new Literal("A_2")))))); + new Implies(new Literal("C.A_2"), new Literal("A_2")))); executeTest(); } @@ -280,6 +245,7 @@ void withCardinalityAndChildGroup() { @Test void withCardinalityAndChildChildGroup() { IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); rootTree.mutate().toAndGroup(); // create and set cardinality for the child feature @@ -326,12 +292,7 @@ void withCardinalityAndChildChildGroup() { new Implies(new Literal("C.A_2"), new Or(Arrays.asList(new Literal("D.C.A_2"), new Literal("E.C.A_2")))), new Implies(new Literal("D.C.A_2"), new Literal("C.A_2")), - new Implies(new Literal("E.C.A_2"), new Literal("C.A_2")), - - new Implies(new Literal("root"), new AtLeast(0, Arrays.asList(new Literal("A_1"), new Literal("A_2")))) - - // missing - )); + new Implies(new Literal("E.C.A_2"), new Literal("C.A_2")))); executeTest(); } @@ -340,7 +301,7 @@ void withCardinalityAndChildChildGroup() { void onlyRoot() { // root and nothing else - featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")).mutate().makeMandatory(); // root must be selected expected = new Reference(new And(new Literal("root"))); @@ -353,15 +314,13 @@ void oneFeature() { // root IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); rootTree.mutate().toAndGroup(); - // TODO: check if setting root feature is missing here or if compute misses - // adding root feature literal - // create and add our only child IFeature childFeature = featureModel.mutate().addFeature("Test1"); rootTree.mutate().addFeatureBelow(childFeature); - + expected = new Reference(new And(new Literal("root"), new Implies(new Literal("Test1"), new Literal("root")))); executeTest(); @@ -370,6 +329,7 @@ void oneFeature() { @Test void withCardinalityGroup() { IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); rootTree.mutate().toCardinalityGroup(Range.of(2, 3)); // create and set cardinality for the child feature @@ -394,6 +354,7 @@ void withCardinalityGroup() { @Test void withOneCardinalityFeature() { IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); rootTree.mutate().toAndGroup(); // create and set cardinality for the child feature @@ -409,11 +370,7 @@ void withOneCardinalityFeature() { new Implies(new Literal("B.A_1"), new Literal("A_1")), new Implies(new Literal("A_2"), new Literal("root")), new Implies(new Literal("A_2"), new Literal("A_1")), - new Implies(new Literal("B.A_2"), new Literal("A_2")), - - new Implies(new Literal("root"), new AtLeast(0, Arrays.asList(new Literal("A_1"), new Literal("A_2")))) - - )); + new Implies(new Literal("B.A_2"), new Literal("A_2")))); executeTest(); } From 3604026c314de7be16ee63b6f46bd312ad700a40 Mon Sep 17 00:00:00 2001 From: Sebastian Krieter Date: Thu, 9 Oct 2025 13:11:56 +0200 Subject: [PATCH 27/67] Fixes line endings --- .../feature/configuration/Configuration.java | 934 +++++++++--------- .../configuration/io/FeatureIDEFormat.java | 358 +++---- .../feature/model/AFeatureModelElement.java | 204 ++-- .../de/featjar/feature/model/Attributes.java | 196 ++-- .../de/featjar/feature/model/Constraint.java | 168 ++-- .../de/featjar/feature/model/Feature.java | 164 +-- .../featjar/feature/model/FeatureModel.java | 546 +++++----- .../de/featjar/feature/model/FeatureTree.java | 578 +++++------ .../de/featjar/feature/model/IConstraint.java | 172 ++-- .../de/featjar/feature/model/IFeature.java | 264 ++--- .../featjar/feature/model/IFeatureModel.java | 180 ++-- .../feature/model/IFeatureModelElement.java | 56 +- .../featjar/feature/model/IFeatureTree.java | 510 +++++----- .../featjar/feature/model/io/AttributeIO.java | 236 ++--- .../feature/model/io/FeatureModelFormats.java | 74 +- .../io/xml/GraphVizFeatureModelFormat.java | 290 +++--- .../model/io/xml/XMLFeatureModelFormat.java | 150 +-- .../model/io/xml/XMLFeatureModelParser.java | 458 ++++----- .../model/io/xml/XMLFeatureModelWriter.java | 534 +++++----- .../model/mixins/IHasCommonAttributes.java | 98 +- .../feature/model/mixins/IHasConstraints.java | 112 +-- .../feature/model/mixins/IHasFeatureTree.java | 192 ++-- .../model/transformer/ComputeFormula.java | 511 +++++----- .../ComputeSimpleFormulaVisitor.java | 324 +++--- .../featjar/feature/model/AttributeTest.java | 234 ++--- .../feature/model/FeatureModelTest.java | 204 ++-- .../de/featjar/feature/model/FeatureTest.java | 234 ++--- .../featjar/feature/model/IdentifierTest.java | 174 ++-- .../configuration/ConfigurationTest.java | 712 ++++++------- .../io/GraphVizFeatureModelFormatTest.java | 108 +- .../io/XMLFeatureModelFormulaFormatTest.java | 318 +++--- .../model/transformer/ComputeFormulaTest.java | 824 +++++++-------- 32 files changed, 5078 insertions(+), 5039 deletions(-) diff --git a/src/main/java/de/featjar/feature/configuration/Configuration.java b/src/main/java/de/featjar/feature/configuration/Configuration.java index c6f6bac9..fc06f307 100644 --- a/src/main/java/de/featjar/feature/configuration/Configuration.java +++ b/src/main/java/de/featjar/feature/configuration/Configuration.java @@ -1,467 +1,467 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.configuration; - -import de.featjar.base.FeatJAR; -import de.featjar.base.data.Result; -import de.featjar.feature.model.IFeature; -import de.featjar.feature.model.IFeatureModel; -import de.featjar.formula.VariableMap; -import de.featjar.formula.assignment.BooleanAssignment; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.ListIterator; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Represents a configuration and provides operations for the configuration process. - */ -public class Configuration implements Cloneable { - - public static final class IllegalSelectionTypeException extends RuntimeException { - private static final long serialVersionUID = 1793844229871267311L; - - public IllegalSelectionTypeException(Class otherType, Class thisType) { - super(String.format( - "Trying to set values of type %s to a feature of type %s", - String.valueOf(otherType), String.valueOf(thisType))); - } - - public IllegalSelectionTypeException(Selection feature, Object selection) { - super(String.format( - "Trying to set the value %s (of type %s) to a feature of type %s", - String.valueOf(selection), String.valueOf(selection.getClass()), feature.getType())); - } - } - - public static final class SelectionNotPossibleException extends RuntimeException { - private static final long serialVersionUID = 1793844229871267311L; - - public SelectionNotPossibleException(Object selection) { - super(String.format("Feature cannot be set to %s", String.valueOf(selection))); - } - } - - /** - * The value of a variable. - * Has an automatic and manual value, which can be set independently. - * - * @param the type of possible values - */ - public static class Selection { - - private final Class type; - private T manual, automatic; - - /** - * Constructs a new selection. - * @param type the type of the selection - */ - public Selection(Class type) { - this.type = type; - } - - private Selection(Selection oldSelectableFeature) { - type = oldSelectableFeature.type; - } - - /** - * {@return type of this selection} - */ - public Class getType() { - return type; - } - - /** - * {@return the combined automatic and manual selection} - * If an automatic value is set, this is returned. Otherwise the manual value is returned. - */ - public T getSelection() { - return automatic == null ? manual : automatic; - } - - /** - * {@return the manual selection} - */ - public T getManual() { - return manual; - } - - /** - * {@return the automatic selection} - */ - public T getAutomatic() { - return automatic; - } - - /** - * Sets the manual value. - * @param selection the value - * - * @throws IllegalSelectionTypeException if the given value is of a different type than this selection - * @throws SelectionNotPossibleException if the value contradicts the automatic value - */ - @SuppressWarnings("unchecked") - public void setManual(Object selection) { - checkIfSelectionPossible(selection, automatic); - manual = (T) selection; - } - - /** - * Sets the automatic value. - * @param selection the value - * - * @throws IllegalSelectionTypeException if the given value is of a different type than this selection - * @throws SelectionNotPossibleException if the value contradicts the manual value - */ - @SuppressWarnings("unchecked") - public void setAutomatic(Object selection) { - checkIfSelectionPossible(selection, manual); - automatic = (T) selection; - } - - private void checkIfSelectionPossible(Object selection, Object current) { - if (selection != null) { - if (!type.isInstance(selection)) { - throw new IllegalSelectionTypeException(this, selection); - } - if ((current != null) && (current != selection)) { - throw new SelectionNotPossibleException(selection); - } - } - } - - /** - * Converts the automatic selection into a manual selection. - */ - public void makeManual() { - if (automatic != null) { - manual = automatic; - automatic = null; - } - } - - /** - * Adopts the values from a given selection. - * @param selection the other selection - * @throws IllegalSelectionTypeException if the type of the given selection is different from this selection's type - */ - @SuppressWarnings("unchecked") - public void adopt(Selection selection) { - if (selection.type != type) { - throw new IllegalSelectionTypeException(selection.type, type); - } - manual = (T) selection.manual; - automatic = (T) selection.automatic; - } - - /** - * Sets manual and automatic values to {@code null}. - */ - public void reset() { - manual = null; - automatic = null; - } - /** - * Sets automatic value to {@code null}. - */ - public void resetAutomatic() { - automatic = null; - } - - @SuppressWarnings("unchecked") - @Override - public Selection clone() { - if (!this.getClass().equals(Selection.class)) { - try { - return (Selection) super.clone(); - } catch (final CloneNotSupportedException e) { - FeatJAR.log().error(e); - throw new RuntimeException("Cloning is not supported for " + this.getClass()); - } - } - Selection selectableFeature = new Selection<>(this); - selectableFeature.manual = this.manual; - selectableFeature.automatic = this.automatic; - return selectableFeature; - } - } - - private VariableMap variableMap; - private ArrayList> selections; - - /** - * Creates an empty configuration. - */ - public Configuration() { - variableMap = new VariableMap(); - selections = new ArrayList<>(); - } - - /** - * Creates a configuration with the same features as the given feature model. - * - * @param featureModel the underlying feature model. - */ - public Configuration(IFeatureModel featureModel) { - variableMap = new VariableMap(); - selections = new ArrayList<>(featureModel.getNumberOfFeatures()); - for (final IFeature child : featureModel.getFeatures()) { - String featureName = child.getName().get(); - int index = variableMap.add(featureName); - for (int i = selections.size(); i <= index; i++) { - selections.add(null); - } - selections.add(index, new Selection<>(child.getType())); - } - } - - /** - * Copy constructor. Copies the status of a given configuration. - * - * @param configuration The configuration to clone - */ - protected Configuration(Configuration configuration) { - variableMap = configuration.variableMap.clone(); - selections = new ArrayList<>(configuration.selections.size()); - for (Selection selection : configuration.selections) { - selections.add(selection != null ? selection.clone() : null); - } - } - - /** - * Creates configuration from literal set. - * - * @param booleanAssignment contains literals with truth values. - * @param variableMap mapping of variable names to indices. Is used to link a literal index in a {@link BooleanAssignment}. - */ - public Configuration(BooleanAssignment booleanAssignment, VariableMap variableMap) { - variableMap = variableMap.clone(); - selections = new ArrayList<>(variableMap.maxIndex()); - adopt(booleanAssignment, variableMap); - } - - /** - * Adopts the values from this assignment. - * - * @param assignment the assignment to adopt - * @param variableMap maps the literals in the assignments to feature names - */ - public void adopt(BooleanAssignment assignment, VariableMap variableMap) { - for (int literal : assignment.get()) { - if (literal != 0) { - int adapedLiteral = variableMap.adapt(literal, this.variableMap, true); - if (adapedLiteral != 0) { - int index = Math.abs(adapedLiteral); - for (int i = selections.size(); i <= index; i++) { - selections.add(null); - } - Selection selection = selections.get(index); - if (selection == null) { - selection = new Selection<>(Boolean.class); - selections.add(index, selection); - } - selection.setManual(adapedLiteral > 0); - } - } - } - } - - /** - * Adopts the values from this assignment. - * Assumes that the variable map of the given assignment is the same as in this configuration. - * - * @param assignment the assignment to adopt - */ - public void adopt(BooleanAssignment assignment) { - for (int literal : assignment.get()) { - if (literal != 0) { - int index = Math.abs(literal); - for (int i = selections.size(); i <= index; i++) { - selections.add(null); - } - Selection selection = selections.get(index); - if (selection == null) { - selection = new Selection<>(Boolean.class); - selections.add(index, selection); - } - selection.setManual(literal > 0); - } - } - } - - /** - * Adopts the values from the given configuration. - * Features not - * - * @param configuration the configuration to adopt - */ - public void adopt(Configuration configuration) { - ListIterator> it = configuration.selections.listIterator(); - while (it.hasNext()) { - Selection otherSelection = it.next(); - if (otherSelection != null) { - int adapedIndex = configuration.variableMap.adapt(it.previousIndex(), variableMap, true); - if (adapedIndex != 0) { - for (int i = selections.size(); i <= adapedIndex; i++) { - selections.add(null); - } - Selection selection = selections.get(adapedIndex); - if (selection == null) { - selection = otherSelection.clone(); - selections.add(adapedIndex, selection); - } else { - selection.adopt(otherSelection); - } - } - } - } - } - - public List> select(Collection features) { - return select(features.stream()).collect(Collectors.toList()); - } - - public Stream> select(Stream features) { - return features.map(this::getSelection).filter(Result::isPresent).map(Result::get); - } - - public VariableMap getVariableMap() { - return variableMap; - } - - public void adapt(VariableMap newVariableMap) { - ArrayList> newSelections = new ArrayList<>(selections.size()); - ListIterator> it = selections.listIterator(); - while (it.hasNext()) { - Selection otherSelection = it.next(); - if (otherSelection != null) { - int adapedIndex = variableMap.adapt(it.previousIndex(), newVariableMap, true); - if (adapedIndex != 0) { - for (int i = selections.size(); i <= adapedIndex; i++) { - newSelections.add(null); - } - newSelections.add(adapedIndex, otherSelection.clone()); - } - } - } - selections.clear(); - selections.addAll(newSelections); - newSelections.clear(); - this.variableMap = newVariableMap; - } - - public List> getSelections() { - return Collections.unmodifiableList(selections); - } - - /** - * {@return a list of all features that have a manual and no automatic value} - */ - public List> getManualFeatures() { - return getSelectionStream() - .filter(f -> f.getAutomatic() == null && f.getManual() != null) - .collect(Collectors.toList()); - } - - /** - * {@return a list of all features that have a automatic value} - */ - public List> getAutomaticFeatures() { - return getSelectionStream().filter(f -> f.getAutomatic() != null).collect(Collectors.toList()); - } - - private Stream> getSelectionStream() { - return selections.stream().filter(Objects::nonNull); - } - - public Result> getSelection(String name) { - return Result.ofNullable(name).flatMap(variableMap::get).map(selections::get); - } - - public Selection get(String name) { - return getSelection(name).orElseThrow(); - } - - public Result> getSelection(IFeature feature) { - return Result.ofNullable(feature) - .flatMap(IFeature::getName) - .flatMap(variableMap::get) - .map(selections::get); - } - - /** - * Turns all automatic into manual values. - */ - public void makeManual() { - getSelectionStream().forEach(Selection::makeManual); - } - - /** - * Resets all values to undefined. - */ - public void reset() { - getSelectionStream().forEach(Selection::reset); - } - - /** - * Resets all automatic values to undefined. - */ - public void resetAutomatic() { - getSelectionStream().forEach(Selection::resetAutomatic); - } - - /** - * Resets automatic values that equal the given selection. - * - * @param selection the selection to reset - */ - public void resetAutomatic(Object selection) { - getSelectionStream().filter(f -> f.getAutomatic() == selection).forEach(Selection::resetAutomatic); - } - - /** - * Creates and returns a copy of this configuration. - * - * @return configuration a clone of this configuration. - */ - @Override - public Configuration clone() { - if (!this.getClass().equals(Configuration.class)) { - try { - return (Configuration) super.clone(); - } catch (final CloneNotSupportedException e) { - FeatJAR.log().error(e); - throw new RuntimeException("Cloning is not supported for " + this.getClass()); - } - } - return new Configuration(this); - } - - @Override - public String toString() { - return getSelectionStream().map(Selection::toString).collect(Collectors.joining("\n")); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.configuration; + +import de.featjar.base.FeatJAR; +import de.featjar.base.data.Result; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.formula.VariableMap; +import de.featjar.formula.assignment.BooleanAssignment; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Represents a configuration and provides operations for the configuration process. + */ +public class Configuration implements Cloneable { + + public static final class IllegalSelectionTypeException extends RuntimeException { + private static final long serialVersionUID = 1793844229871267311L; + + public IllegalSelectionTypeException(Class otherType, Class thisType) { + super(String.format( + "Trying to set values of type %s to a feature of type %s", + String.valueOf(otherType), String.valueOf(thisType))); + } + + public IllegalSelectionTypeException(Selection feature, Object selection) { + super(String.format( + "Trying to set the value %s (of type %s) to a feature of type %s", + String.valueOf(selection), String.valueOf(selection.getClass()), feature.getType())); + } + } + + public static final class SelectionNotPossibleException extends RuntimeException { + private static final long serialVersionUID = 1793844229871267311L; + + public SelectionNotPossibleException(Object selection) { + super(String.format("Feature cannot be set to %s", String.valueOf(selection))); + } + } + + /** + * The value of a variable. + * Has an automatic and manual value, which can be set independently. + * + * @param the type of possible values + */ + public static class Selection { + + private final Class type; + private T manual, automatic; + + /** + * Constructs a new selection. + * @param type the type of the selection + */ + public Selection(Class type) { + this.type = type; + } + + private Selection(Selection oldSelectableFeature) { + type = oldSelectableFeature.type; + } + + /** + * {@return type of this selection} + */ + public Class getType() { + return type; + } + + /** + * {@return the combined automatic and manual selection} + * If an automatic value is set, this is returned. Otherwise the manual value is returned. + */ + public T getSelection() { + return automatic == null ? manual : automatic; + } + + /** + * {@return the manual selection} + */ + public T getManual() { + return manual; + } + + /** + * {@return the automatic selection} + */ + public T getAutomatic() { + return automatic; + } + + /** + * Sets the manual value. + * @param selection the value + * + * @throws IllegalSelectionTypeException if the given value is of a different type than this selection + * @throws SelectionNotPossibleException if the value contradicts the automatic value + */ + @SuppressWarnings("unchecked") + public void setManual(Object selection) { + checkIfSelectionPossible(selection, automatic); + manual = (T) selection; + } + + /** + * Sets the automatic value. + * @param selection the value + * + * @throws IllegalSelectionTypeException if the given value is of a different type than this selection + * @throws SelectionNotPossibleException if the value contradicts the manual value + */ + @SuppressWarnings("unchecked") + public void setAutomatic(Object selection) { + checkIfSelectionPossible(selection, manual); + automatic = (T) selection; + } + + private void checkIfSelectionPossible(Object selection, Object current) { + if (selection != null) { + if (!type.isInstance(selection)) { + throw new IllegalSelectionTypeException(this, selection); + } + if ((current != null) && (current != selection)) { + throw new SelectionNotPossibleException(selection); + } + } + } + + /** + * Converts the automatic selection into a manual selection. + */ + public void makeManual() { + if (automatic != null) { + manual = automatic; + automatic = null; + } + } + + /** + * Adopts the values from a given selection. + * @param selection the other selection + * @throws IllegalSelectionTypeException if the type of the given selection is different from this selection's type + */ + @SuppressWarnings("unchecked") + public void adopt(Selection selection) { + if (selection.type != type) { + throw new IllegalSelectionTypeException(selection.type, type); + } + manual = (T) selection.manual; + automatic = (T) selection.automatic; + } + + /** + * Sets manual and automatic values to {@code null}. + */ + public void reset() { + manual = null; + automatic = null; + } + /** + * Sets automatic value to {@code null}. + */ + public void resetAutomatic() { + automatic = null; + } + + @SuppressWarnings("unchecked") + @Override + public Selection clone() { + if (!this.getClass().equals(Selection.class)) { + try { + return (Selection) super.clone(); + } catch (final CloneNotSupportedException e) { + FeatJAR.log().error(e); + throw new RuntimeException("Cloning is not supported for " + this.getClass()); + } + } + Selection selectableFeature = new Selection<>(this); + selectableFeature.manual = this.manual; + selectableFeature.automatic = this.automatic; + return selectableFeature; + } + } + + private VariableMap variableMap; + private ArrayList> selections; + + /** + * Creates an empty configuration. + */ + public Configuration() { + variableMap = new VariableMap(); + selections = new ArrayList<>(); + } + + /** + * Creates a configuration with the same features as the given feature model. + * + * @param featureModel the underlying feature model. + */ + public Configuration(IFeatureModel featureModel) { + variableMap = new VariableMap(); + selections = new ArrayList<>(featureModel.getNumberOfFeatures()); + for (final IFeature child : featureModel.getFeatures()) { + String featureName = child.getName().get(); + int index = variableMap.add(featureName); + for (int i = selections.size(); i <= index; i++) { + selections.add(null); + } + selections.add(index, new Selection<>(child.getType())); + } + } + + /** + * Copy constructor. Copies the status of a given configuration. + * + * @param configuration The configuration to clone + */ + protected Configuration(Configuration configuration) { + variableMap = configuration.variableMap.clone(); + selections = new ArrayList<>(configuration.selections.size()); + for (Selection selection : configuration.selections) { + selections.add(selection != null ? selection.clone() : null); + } + } + + /** + * Creates configuration from literal set. + * + * @param booleanAssignment contains literals with truth values. + * @param variableMap mapping of variable names to indices. Is used to link a literal index in a {@link BooleanAssignment}. + */ + public Configuration(BooleanAssignment booleanAssignment, VariableMap variableMap) { + variableMap = variableMap.clone(); + selections = new ArrayList<>(variableMap.maxIndex()); + adopt(booleanAssignment, variableMap); + } + + /** + * Adopts the values from this assignment. + * + * @param assignment the assignment to adopt + * @param variableMap maps the literals in the assignments to feature names + */ + public void adopt(BooleanAssignment assignment, VariableMap variableMap) { + for (int literal : assignment.get()) { + if (literal != 0) { + int adapedLiteral = variableMap.adapt(literal, this.variableMap, true); + if (adapedLiteral != 0) { + int index = Math.abs(adapedLiteral); + for (int i = selections.size(); i <= index; i++) { + selections.add(null); + } + Selection selection = selections.get(index); + if (selection == null) { + selection = new Selection<>(Boolean.class); + selections.add(index, selection); + } + selection.setManual(adapedLiteral > 0); + } + } + } + } + + /** + * Adopts the values from this assignment. + * Assumes that the variable map of the given assignment is the same as in this configuration. + * + * @param assignment the assignment to adopt + */ + public void adopt(BooleanAssignment assignment) { + for (int literal : assignment.get()) { + if (literal != 0) { + int index = Math.abs(literal); + for (int i = selections.size(); i <= index; i++) { + selections.add(null); + } + Selection selection = selections.get(index); + if (selection == null) { + selection = new Selection<>(Boolean.class); + selections.add(index, selection); + } + selection.setManual(literal > 0); + } + } + } + + /** + * Adopts the values from the given configuration. + * Features not + * + * @param configuration the configuration to adopt + */ + public void adopt(Configuration configuration) { + ListIterator> it = configuration.selections.listIterator(); + while (it.hasNext()) { + Selection otherSelection = it.next(); + if (otherSelection != null) { + int adapedIndex = configuration.variableMap.adapt(it.previousIndex(), variableMap, true); + if (adapedIndex != 0) { + for (int i = selections.size(); i <= adapedIndex; i++) { + selections.add(null); + } + Selection selection = selections.get(adapedIndex); + if (selection == null) { + selection = otherSelection.clone(); + selections.add(adapedIndex, selection); + } else { + selection.adopt(otherSelection); + } + } + } + } + } + + public List> select(Collection features) { + return select(features.stream()).collect(Collectors.toList()); + } + + public Stream> select(Stream features) { + return features.map(this::getSelection).filter(Result::isPresent).map(Result::get); + } + + public VariableMap getVariableMap() { + return variableMap; + } + + public void adapt(VariableMap newVariableMap) { + ArrayList> newSelections = new ArrayList<>(selections.size()); + ListIterator> it = selections.listIterator(); + while (it.hasNext()) { + Selection otherSelection = it.next(); + if (otherSelection != null) { + int adapedIndex = variableMap.adapt(it.previousIndex(), newVariableMap, true); + if (adapedIndex != 0) { + for (int i = selections.size(); i <= adapedIndex; i++) { + newSelections.add(null); + } + newSelections.add(adapedIndex, otherSelection.clone()); + } + } + } + selections.clear(); + selections.addAll(newSelections); + newSelections.clear(); + this.variableMap = newVariableMap; + } + + public List> getSelections() { + return Collections.unmodifiableList(selections); + } + + /** + * {@return a list of all features that have a manual and no automatic value} + */ + public List> getManualFeatures() { + return getSelectionStream() + .filter(f -> f.getAutomatic() == null && f.getManual() != null) + .collect(Collectors.toList()); + } + + /** + * {@return a list of all features that have a automatic value} + */ + public List> getAutomaticFeatures() { + return getSelectionStream().filter(f -> f.getAutomatic() != null).collect(Collectors.toList()); + } + + private Stream> getSelectionStream() { + return selections.stream().filter(Objects::nonNull); + } + + public Result> getSelection(String name) { + return Result.ofNullable(name).flatMap(variableMap::get).map(selections::get); + } + + public Selection get(String name) { + return getSelection(name).orElseThrow(); + } + + public Result> getSelection(IFeature feature) { + return Result.ofNullable(feature) + .flatMap(IFeature::getName) + .flatMap(variableMap::get) + .map(selections::get); + } + + /** + * Turns all automatic into manual values. + */ + public void makeManual() { + getSelectionStream().forEach(Selection::makeManual); + } + + /** + * Resets all values to undefined. + */ + public void reset() { + getSelectionStream().forEach(Selection::reset); + } + + /** + * Resets all automatic values to undefined. + */ + public void resetAutomatic() { + getSelectionStream().forEach(Selection::resetAutomatic); + } + + /** + * Resets automatic values that equal the given selection. + * + * @param selection the selection to reset + */ + public void resetAutomatic(Object selection) { + getSelectionStream().filter(f -> f.getAutomatic() == selection).forEach(Selection::resetAutomatic); + } + + /** + * Creates and returns a copy of this configuration. + * + * @return configuration a clone of this configuration. + */ + @Override + public Configuration clone() { + if (!this.getClass().equals(Configuration.class)) { + try { + return (Configuration) super.clone(); + } catch (final CloneNotSupportedException e) { + FeatJAR.log().error(e); + throw new RuntimeException("Cloning is not supported for " + this.getClass()); + } + } + return new Configuration(this); + } + + @Override + public String toString() { + return getSelectionStream().map(Selection::toString).collect(Collectors.joining("\n")); + } +} diff --git a/src/main/java/de/featjar/feature/configuration/io/FeatureIDEFormat.java b/src/main/java/de/featjar/feature/configuration/io/FeatureIDEFormat.java index 580ad731..d4b1406e 100644 --- a/src/main/java/de/featjar/feature/configuration/io/FeatureIDEFormat.java +++ b/src/main/java/de/featjar/feature/configuration/io/FeatureIDEFormat.java @@ -1,179 +1,179 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.configuration.io; - -import de.featjar.base.data.Problem; -import de.featjar.base.data.Problem.Severity; -import de.featjar.base.data.Result; -import de.featjar.base.io.format.IFormat; -import de.featjar.base.io.format.ParseProblem; -import de.featjar.base.io.input.AInputMapper; -import de.featjar.feature.configuration.Configuration; -import de.featjar.feature.configuration.Configuration.Selection; -import de.featjar.feature.configuration.Configuration.SelectionNotPossibleException; -import java.io.BufferedReader; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -/** - * Extended configuration format for FeatureIDE projects.
Lists all features and indicates the manual and automatic selection. - * - * @author Sebastian Krieter - */ -public class FeatureIDEFormat implements IFormat { - - private static final String NEWLINE = System.lineSeparator(); - - /** - * Parses a String representation of a FeatureIDE Format into a Configuration. - * - * @param inputmapper the input mapper - * @return Configuration inside the Result wrapper - */ - @Override - public Result parse(AInputMapper inputmapper) { - Configuration configuration = new Configuration(); - List warnings = new ArrayList<>(); - - String line = null; - int lineNumber = 1; - try (BufferedReader reader = inputmapper.get().getReader(); ) { - while ((line = reader.readLine()) != null) { - if (line.startsWith("#")) { - continue; - } - line = line.trim(); - if (!line.isEmpty()) { - Boolean manual = null; - Boolean automatic = null; - try { - switch (Integer.parseInt(line.substring(0, 1))) { - case 0: - manual = Boolean.FALSE; - break; - case 1: - manual = Boolean.TRUE; - break; - case 2: - break; - default: - warnings.add(new ParseProblem(line, Severity.WARNING, lineNumber)); - break; - } - switch (Integer.parseInt(line.substring(1, 2))) { - case 0: - automatic = Boolean.FALSE; - break; - case 1: - automatic = Boolean.TRUE; - break; - case 2: - break; - default: - warnings.add(new ParseProblem(line, Severity.WARNING, lineNumber)); - break; - } - } catch (final NumberFormatException e) { - warnings.add(new ParseProblem(e, lineNumber)); - } - - final String name = line.substring(2); - - final Result> feature = configuration.getSelection(name); - if (feature.isEmpty()) { - warnings.add(new ParseProblem(name, Severity.ERROR, lineNumber)); - } else { - try { - feature.get().setManual(manual); - feature.get().setAutomatic(automatic); - } catch (final SelectionNotPossibleException e) { - warnings.add(new ParseProblem(e, lineNumber)); - } - } - } - lineNumber++; - } - } catch (final IOException e) { - warnings.add(new Problem(e)); - } - - return Result.of(configuration, warnings); - } - - /** - * Returns the String representation of a Configuration in the FeatureIDE Format. - * - * @param configuration the object - * @return String representation of the Configuration inside the Result wrapper - */ - @Override - public Result serialize(Configuration configuration) { - final StringBuilder buffer = new StringBuilder(); - buffer.append( - "# Lists all features from the model with manual (first digit) and automatic (second digit) selection"); - buffer.append(NEWLINE); - buffer.append("# 0 = deselected, 1 = selected, 2 = undefined"); - buffer.append(NEWLINE); - - for (final String name : configuration.getVariableMap().getVariableNames()) { - Selection selection = configuration.get(name); - buffer.append(Integer.toString(getSelectionCode((Boolean) selection.getManual()))); - buffer.append(Integer.toString(getSelectionCode((Boolean) selection.getAutomatic()))); - buffer.append(name); - buffer.append(NEWLINE); - } - - return Result.of(buffer.toString()); - } - - private int getSelectionCode(Boolean selection) { - if (selection == null) { - return 2; - } else if (selection == Boolean.TRUE) { - return 1; - } else if (selection == Boolean.FALSE) { - return 0; - } else { - return 3; - } - } - - @Override - public String getFileExtension() { - return ".config"; - } - - @Override - public boolean supportsParse() { - return true; - } - - @Override - public boolean supportsWrite() { - return true; - } - - @Override - public String getName() { - return "FeatureIDE-Internal"; - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.configuration.io; + +import de.featjar.base.data.Problem; +import de.featjar.base.data.Problem.Severity; +import de.featjar.base.data.Result; +import de.featjar.base.io.format.IFormat; +import de.featjar.base.io.format.ParseProblem; +import de.featjar.base.io.input.AInputMapper; +import de.featjar.feature.configuration.Configuration; +import de.featjar.feature.configuration.Configuration.Selection; +import de.featjar.feature.configuration.Configuration.SelectionNotPossibleException; +import java.io.BufferedReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Extended configuration format for FeatureIDE projects.
Lists all features and indicates the manual and automatic selection. + * + * @author Sebastian Krieter + */ +public class FeatureIDEFormat implements IFormat { + + private static final String NEWLINE = System.lineSeparator(); + + /** + * Parses a String representation of a FeatureIDE Format into a Configuration. + * + * @param inputmapper the input mapper + * @return Configuration inside the Result wrapper + */ + @Override + public Result parse(AInputMapper inputmapper) { + Configuration configuration = new Configuration(); + List warnings = new ArrayList<>(); + + String line = null; + int lineNumber = 1; + try (BufferedReader reader = inputmapper.get().getReader(); ) { + while ((line = reader.readLine()) != null) { + if (line.startsWith("#")) { + continue; + } + line = line.trim(); + if (!line.isEmpty()) { + Boolean manual = null; + Boolean automatic = null; + try { + switch (Integer.parseInt(line.substring(0, 1))) { + case 0: + manual = Boolean.FALSE; + break; + case 1: + manual = Boolean.TRUE; + break; + case 2: + break; + default: + warnings.add(new ParseProblem(line, Severity.WARNING, lineNumber)); + break; + } + switch (Integer.parseInt(line.substring(1, 2))) { + case 0: + automatic = Boolean.FALSE; + break; + case 1: + automatic = Boolean.TRUE; + break; + case 2: + break; + default: + warnings.add(new ParseProblem(line, Severity.WARNING, lineNumber)); + break; + } + } catch (final NumberFormatException e) { + warnings.add(new ParseProblem(e, lineNumber)); + } + + final String name = line.substring(2); + + final Result> feature = configuration.getSelection(name); + if (feature.isEmpty()) { + warnings.add(new ParseProblem(name, Severity.ERROR, lineNumber)); + } else { + try { + feature.get().setManual(manual); + feature.get().setAutomatic(automatic); + } catch (final SelectionNotPossibleException e) { + warnings.add(new ParseProblem(e, lineNumber)); + } + } + } + lineNumber++; + } + } catch (final IOException e) { + warnings.add(new Problem(e)); + } + + return Result.of(configuration, warnings); + } + + /** + * Returns the String representation of a Configuration in the FeatureIDE Format. + * + * @param configuration the object + * @return String representation of the Configuration inside the Result wrapper + */ + @Override + public Result serialize(Configuration configuration) { + final StringBuilder buffer = new StringBuilder(); + buffer.append( + "# Lists all features from the model with manual (first digit) and automatic (second digit) selection"); + buffer.append(NEWLINE); + buffer.append("# 0 = deselected, 1 = selected, 2 = undefined"); + buffer.append(NEWLINE); + + for (final String name : configuration.getVariableMap().getVariableNames()) { + Selection selection = configuration.get(name); + buffer.append(Integer.toString(getSelectionCode((Boolean) selection.getManual()))); + buffer.append(Integer.toString(getSelectionCode((Boolean) selection.getAutomatic()))); + buffer.append(name); + buffer.append(NEWLINE); + } + + return Result.of(buffer.toString()); + } + + private int getSelectionCode(Boolean selection) { + if (selection == null) { + return 2; + } else if (selection == Boolean.TRUE) { + return 1; + } else if (selection == Boolean.FALSE) { + return 0; + } else { + return 3; + } + } + + @Override + public String getFileExtension() { + return ".config"; + } + + @Override + public boolean supportsParse() { + return true; + } + + @Override + public boolean supportsWrite() { + return true; + } + + @Override + public String getName() { + return "FeatureIDE-Internal"; + } +} diff --git a/src/main/java/de/featjar/feature/model/AFeatureModelElement.java b/src/main/java/de/featjar/feature/model/AFeatureModelElement.java index cc87ab75..ee281c43 100644 --- a/src/main/java/de/featjar/feature/model/AFeatureModelElement.java +++ b/src/main/java/de/featjar/feature/model/AFeatureModelElement.java @@ -1,102 +1,102 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.data.Attribute; -import de.featjar.base.data.IAttributable.IMutatableAttributable; -import de.featjar.base.data.IAttribute; -import de.featjar.base.data.identifier.AIdentifier; -import de.featjar.base.data.identifier.IIdentifier; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; - -/** - * Implements identification and attribute valuation. - * Each {@link FeatureModel} and all its {@link Feature features} and {@link Constraint constraints} are - * uniquely identified by some {@link AIdentifier}. - * Also, each element can be annotated with arbitrary {@link Attribute attributes}. - * - * @author Elias Kuiter - */ -public abstract class AFeatureModelElement implements IFeatureModelElement, IMutatableAttributable { - protected final IFeatureModel featureModel; - protected final IIdentifier identifier; - protected final LinkedHashMap, Object> attributeValues; - - public AFeatureModelElement(IFeatureModel featureModel) { - this.featureModel = Objects.requireNonNull(featureModel); - identifier = featureModel.getNewIdentifier(); - attributeValues = new LinkedHashMap<>(4); - } - - protected AFeatureModelElement(AFeatureModelElement otherElement, IFeatureModel featureModel) { - this.featureModel = featureModel; - identifier = otherElement.getNewIdentifier(); - attributeValues = otherElement.cloneAttributes(); - } - - @Override - public IIdentifier getIdentifier() { - return identifier; - } - - @Override - public Optional, Object>> getAttributes() { - return Optional.of(Collections.unmodifiableMap(attributeValues)); - } - - @Override - public void setAttributeValue(Attribute attribute, S value) { - if (value == null) { - removeAttributeValue(attribute); - return; - } - checkType(attribute, value); - validate(attribute, value); - attributeValues.put(attribute, value); - } - - @Override - @SuppressWarnings("unchecked") - public S removeAttributeValue(Attribute attribute) { - return (S) attributeValues.remove(attribute); - } - - @Override - public IFeatureModel getFeatureModel() { - return featureModel; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - return getIdentifier().equals(((AFeatureModelElement) o).getIdentifier()); - } - - @Override - public int hashCode() { - return Objects.hash(getIdentifier()); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.data.Attribute; +import de.featjar.base.data.IAttributable.IMutatableAttributable; +import de.featjar.base.data.IAttribute; +import de.featjar.base.data.identifier.AIdentifier; +import de.featjar.base.data.identifier.IIdentifier; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * Implements identification and attribute valuation. + * Each {@link FeatureModel} and all its {@link Feature features} and {@link Constraint constraints} are + * uniquely identified by some {@link AIdentifier}. + * Also, each element can be annotated with arbitrary {@link Attribute attributes}. + * + * @author Elias Kuiter + */ +public abstract class AFeatureModelElement implements IFeatureModelElement, IMutatableAttributable { + protected final IFeatureModel featureModel; + protected final IIdentifier identifier; + protected final LinkedHashMap, Object> attributeValues; + + public AFeatureModelElement(IFeatureModel featureModel) { + this.featureModel = Objects.requireNonNull(featureModel); + identifier = featureModel.getNewIdentifier(); + attributeValues = new LinkedHashMap<>(4); + } + + protected AFeatureModelElement(AFeatureModelElement otherElement, IFeatureModel featureModel) { + this.featureModel = featureModel; + identifier = otherElement.getNewIdentifier(); + attributeValues = otherElement.cloneAttributes(); + } + + @Override + public IIdentifier getIdentifier() { + return identifier; + } + + @Override + public Optional, Object>> getAttributes() { + return Optional.of(Collections.unmodifiableMap(attributeValues)); + } + + @Override + public void setAttributeValue(Attribute attribute, S value) { + if (value == null) { + removeAttributeValue(attribute); + return; + } + checkType(attribute, value); + validate(attribute, value); + attributeValues.put(attribute, value); + } + + @Override + @SuppressWarnings("unchecked") + public S removeAttributeValue(Attribute attribute) { + return (S) attributeValues.remove(attribute); + } + + @Override + public IFeatureModel getFeatureModel() { + return featureModel; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return getIdentifier().equals(((AFeatureModelElement) o).getIdentifier()); + } + + @Override + public int hashCode() { + return Objects.hash(getIdentifier()); + } +} diff --git a/src/main/java/de/featjar/feature/model/Attributes.java b/src/main/java/de/featjar/feature/model/Attributes.java index fb0ada1a..8c5838a2 100644 --- a/src/main/java/de/featjar/feature/model/Attributes.java +++ b/src/main/java/de/featjar/feature/model/Attributes.java @@ -1,98 +1,98 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.data.Attribute; -import de.featjar.base.data.Sets; -import de.featjar.base.data.identifier.IIdentifiable; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Set; - -/** - * Defines useful {@link Attribute attributes} for {@link FeatureModel feature models}, - * {@link Feature features}, and {@link Constraint constraints}. - * - * @author Elias Kuiter - * @author Sebastian Krieter - */ -public class Attributes { - - private static final LinkedHashMap, Attribute> attributeSet = new LinkedHashMap<>(); - - public static final String NAMESPACE = Attributes.class.getCanonicalName(); - - public static final Attribute NAME = get(NAMESPACE, "name", String.class) - .setDefaultValueFunction(identifiable -> - "@" + ((IIdentifiable) identifiable).getIdentifier().toString()) - .setValidator( - (element, name) -> // TODO: can also be name of feature model or constraint, but this validates only - // feature name uniqueness - ((AFeatureModelElement) element) - .getFeatureModel() - .getFeature((String) name) - .isEmpty()); - - public static final Attribute DESCRIPTION = get(NAMESPACE, "description", String.class); - - @SuppressWarnings({"rawtypes", "unchecked"}) - public static final Attribute> TAGS = getRaw(NAMESPACE, "tags", LinkedHashSet.class) - .setDefaultValueFunction(attributable -> Sets.empty()) - .setCopyValueFunction(set -> new LinkedHashSet((Collection) set)); - - public static final Attribute HIDDEN = - get(NAMESPACE, "hidden", Boolean.class).setDefaultValue(false); - - public static final Attribute ABSTRACT = - get(NAMESPACE, "abstract", Boolean.class).setDefaultValue(false); - - public static Set> getAllAttributes() { - return Collections.unmodifiableSet(attributeSet.keySet()); - } - - public static Attribute get(String name, Class type) { - return get(NAMESPACE, name, type); - } - - @SuppressWarnings("unchecked") - public static Attribute get(String namespace, String name, Class type) { - return getRaw(namespace, name, type); - } - - @SuppressWarnings("rawtypes") - public static Attribute getRaw(String namespace, String name, Class type) { - Attribute attribute = new Attribute<>(namespace, name, type); - Attribute cachedAttribute = attributeSet.get(attribute); - if (cachedAttribute == null) { - attributeSet.put(attribute, attribute); - return attribute; - } else { - if (type != cachedAttribute.getType()) { - throw new IllegalArgumentException(String.format( - "Cannot create attribute for type %s. Attribute already defined for type %s.", - type.toString(), cachedAttribute.getType())); - } - return cachedAttribute; - } - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.data.Attribute; +import de.featjar.base.data.Sets; +import de.featjar.base.data.identifier.IIdentifiable; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Defines useful {@link Attribute attributes} for {@link FeatureModel feature models}, + * {@link Feature features}, and {@link Constraint constraints}. + * + * @author Elias Kuiter + * @author Sebastian Krieter + */ +public class Attributes { + + private static final LinkedHashMap, Attribute> attributeSet = new LinkedHashMap<>(); + + public static final String NAMESPACE = Attributes.class.getCanonicalName(); + + public static final Attribute NAME = get(NAMESPACE, "name", String.class) + .setDefaultValueFunction(identifiable -> + "@" + ((IIdentifiable) identifiable).getIdentifier().toString()) + .setValidator( + (element, name) -> // TODO: can also be name of feature model or constraint, but this validates only + // feature name uniqueness + ((AFeatureModelElement) element) + .getFeatureModel() + .getFeature((String) name) + .isEmpty()); + + public static final Attribute DESCRIPTION = get(NAMESPACE, "description", String.class); + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static final Attribute> TAGS = getRaw(NAMESPACE, "tags", LinkedHashSet.class) + .setDefaultValueFunction(attributable -> Sets.empty()) + .setCopyValueFunction(set -> new LinkedHashSet((Collection) set)); + + public static final Attribute HIDDEN = + get(NAMESPACE, "hidden", Boolean.class).setDefaultValue(false); + + public static final Attribute ABSTRACT = + get(NAMESPACE, "abstract", Boolean.class).setDefaultValue(false); + + public static Set> getAllAttributes() { + return Collections.unmodifiableSet(attributeSet.keySet()); + } + + public static Attribute get(String name, Class type) { + return get(NAMESPACE, name, type); + } + + @SuppressWarnings("unchecked") + public static Attribute get(String namespace, String name, Class type) { + return getRaw(namespace, name, type); + } + + @SuppressWarnings("rawtypes") + public static Attribute getRaw(String namespace, String name, Class type) { + Attribute attribute = new Attribute<>(namespace, name, type); + Attribute cachedAttribute = attributeSet.get(attribute); + if (cachedAttribute == null) { + attributeSet.put(attribute, attribute); + return attribute; + } else { + if (type != cachedAttribute.getType()) { + throw new IllegalArgumentException(String.format( + "Cannot create attribute for type %s. Attribute already defined for type %s.", + type.toString(), cachedAttribute.getType())); + } + return cachedAttribute; + } + } +} diff --git a/src/main/java/de/featjar/feature/model/Constraint.java b/src/main/java/de/featjar/feature/model/Constraint.java index 71c44a8d..4c6a9a78 100644 --- a/src/main/java/de/featjar/feature/model/Constraint.java +++ b/src/main/java/de/featjar/feature/model/Constraint.java @@ -1,84 +1,84 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.tree.Trees; -import de.featjar.feature.model.IConstraint.IMutableConstraint; -import de.featjar.formula.structure.IFormula; -import java.util.LinkedHashSet; - -public class Constraint extends AFeatureModelElement implements IMutableConstraint { - protected IFormula formula; - - protected Constraint(IFeatureModel featureModel, IFormula formula) { - super(featureModel); - setFormula(formula); - } - - protected Constraint(Constraint otherConstraint) { - this(otherConstraint, otherConstraint.featureModel); - } - - protected Constraint(Constraint otherConstraint, IFeatureModel newFeatureModel) { - super(otherConstraint, newFeatureModel); - setFormula(Trees.clone(otherConstraint.formula)); - } - - @Override - public Constraint clone() { - return new Constraint(this); - } - - @Override - public Constraint clone(IFeatureModel newFeatureModel) { - return new Constraint(this); - } - - @Override - public IFormula getFormula() { - return formula; - } - - @Override - public LinkedHashSet getReferencedFeatures() { - return IConstraint.getReferencedFeatures(formula, featureModel); - } - - @Override - public String toString() { - return String.format("Constraint{formula=%s}", formula); - } - - @Override - public void setFormula(IFormula formula) { - this.formula = formula; - } - - @Override - public void setName(String name) { - attributeValues.put(Attributes.NAME, name); - } - - @Override - public void setDescription(String description) { - attributeValues.put(Attributes.DESCRIPTION, description); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.tree.Trees; +import de.featjar.feature.model.IConstraint.IMutableConstraint; +import de.featjar.formula.structure.IFormula; +import java.util.LinkedHashSet; + +public class Constraint extends AFeatureModelElement implements IMutableConstraint { + protected IFormula formula; + + protected Constraint(IFeatureModel featureModel, IFormula formula) { + super(featureModel); + setFormula(formula); + } + + protected Constraint(Constraint otherConstraint) { + this(otherConstraint, otherConstraint.featureModel); + } + + protected Constraint(Constraint otherConstraint, IFeatureModel newFeatureModel) { + super(otherConstraint, newFeatureModel); + setFormula(Trees.clone(otherConstraint.formula)); + } + + @Override + public Constraint clone() { + return new Constraint(this); + } + + @Override + public Constraint clone(IFeatureModel newFeatureModel) { + return new Constraint(this); + } + + @Override + public IFormula getFormula() { + return formula; + } + + @Override + public LinkedHashSet getReferencedFeatures() { + return IConstraint.getReferencedFeatures(formula, featureModel); + } + + @Override + public String toString() { + return String.format("Constraint{formula=%s}", formula); + } + + @Override + public void setFormula(IFormula formula) { + this.formula = formula; + } + + @Override + public void setName(String name) { + attributeValues.put(Attributes.NAME, name); + } + + @Override + public void setDescription(String description) { + attributeValues.put(Attributes.DESCRIPTION, description); + } +} diff --git a/src/main/java/de/featjar/feature/model/Feature.java b/src/main/java/de/featjar/feature/model/Feature.java index 01fd4a9a..3b4266a3 100644 --- a/src/main/java/de/featjar/feature/model/Feature.java +++ b/src/main/java/de/featjar/feature/model/Feature.java @@ -1,82 +1,82 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.data.Result; -import de.featjar.feature.model.IFeature.IMutableFeature; - -public class Feature extends AFeatureModelElement implements IMutableFeature { - protected Class type; - - protected Feature(IFeatureModel featureModel) { - super(featureModel); - type = Boolean.class; - } - - protected Feature(Feature otherFeature) { - this(otherFeature, otherFeature.featureModel); - } - - protected Feature(Feature otherFeature, IFeatureModel newFeatureModel) { - super(otherFeature, newFeatureModel); - type = otherFeature.type; - } - - @Override - public Feature clone() { - return new Feature(this); - } - - @Override - public Feature clone(IFeatureModel newFeatureModel) { - return new Feature(this); - } - - @Override - public Class getType() { - return type; - } - - @Override - public Result getFeatureTree() { - return featureModel.getFeatureTree(this); - } - - @Override - public void setType(Class type) { - this.type = type; - } - - @Override - public String toString() { - return String.format("Feature{name=%s}", getName().orElse("")); - } - - @Override - public void setName(String name) { - attributeValues.put(Attributes.NAME, name); - } - - @Override - public void setDescription(String description) { - attributeValues.put(Attributes.DESCRIPTION, description); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.data.Result; +import de.featjar.feature.model.IFeature.IMutableFeature; + +public class Feature extends AFeatureModelElement implements IMutableFeature { + protected Class type; + + protected Feature(IFeatureModel featureModel) { + super(featureModel); + type = Boolean.class; + } + + protected Feature(Feature otherFeature) { + this(otherFeature, otherFeature.featureModel); + } + + protected Feature(Feature otherFeature, IFeatureModel newFeatureModel) { + super(otherFeature, newFeatureModel); + type = otherFeature.type; + } + + @Override + public Feature clone() { + return new Feature(this); + } + + @Override + public Feature clone(IFeatureModel newFeatureModel) { + return new Feature(this); + } + + @Override + public Class getType() { + return type; + } + + @Override + public Result getFeatureTree() { + return featureModel.getFeatureTree(this); + } + + @Override + public void setType(Class type) { + this.type = type; + } + + @Override + public String toString() { + return String.format("Feature{name=%s}", getName().orElse("")); + } + + @Override + public void setName(String name) { + attributeValues.put(Attributes.NAME, name); + } + + @Override + public void setDescription(String description) { + attributeValues.put(Attributes.DESCRIPTION, description); + } +} diff --git a/src/main/java/de/featjar/feature/model/FeatureModel.java b/src/main/java/de/featjar/feature/model/FeatureModel.java index 312efeaf..6921d5cd 100644 --- a/src/main/java/de/featjar/feature/model/FeatureModel.java +++ b/src/main/java/de/featjar/feature/model/FeatureModel.java @@ -1,273 +1,273 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.data.Attribute; -import de.featjar.base.data.IAttributable.IMutatableAttributable; -import de.featjar.base.data.IAttribute; -import de.featjar.base.data.Maps; -import de.featjar.base.data.Result; -import de.featjar.base.data.identifier.IIdentifier; -import de.featjar.base.data.identifier.UUIDIdentifier; -import de.featjar.base.tree.Trees; -import de.featjar.feature.model.IFeatureModel.IMutableFeatureModel; -import de.featjar.formula.structure.IFormula; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; - -public class FeatureModel implements IMutableFeatureModel, IMutatableAttributable { - - protected final IIdentifier identifier; - - protected final List featureTreeRoots; - protected final LinkedHashMap features; - protected final LinkedHashMap constraints; - - protected final LinkedHashMap, Object> attributeValues; - - public FeatureModel() { - this(UUIDIdentifier.newInstance()); - } - - public FeatureModel(IIdentifier identifier) { - this.identifier = Objects.requireNonNull(identifier); - featureTreeRoots = new ArrayList<>(1); - features = Maps.empty(); - constraints = Maps.empty(); - attributeValues = new LinkedHashMap<>(4); - } - - protected FeatureModel(FeatureModel otherFeatureModel) { - identifier = otherFeatureModel.getNewIdentifier(); - - featureTreeRoots = new ArrayList<>(otherFeatureModel.featureTreeRoots.size()); - otherFeatureModel.featureTreeRoots.stream().forEach(t -> featureTreeRoots.add(Trees.clone(t))); - - features = new LinkedHashMap<>((int) (otherFeatureModel.features.size() * 1.5)); - otherFeatureModel.features.entrySet().stream() - .map(e -> e.getValue().clone(this)) - .forEach(f -> features.put(f.getIdentifier(), f)); - - constraints = new LinkedHashMap<>((int) (otherFeatureModel.constraints.size() * 1.5)); - otherFeatureModel.constraints.entrySet().stream() - .map(e -> e.getValue().clone(this)) - .forEach(c -> constraints.put(c.getIdentifier(), c)); - - attributeValues = otherFeatureModel.cloneAttributes(); - } - - @Override - public FeatureModel clone() { - return new FeatureModel(this); - } - - @Override - public FeatureModel getFeatureModel() { - return this; - } - - @Override - public List getRoots() { - return featureTreeRoots; - } - - @Override - public Collection getFeatures() { - return Collections.unmodifiableCollection(features.values()); - } - - @Override - public Result getFeature(IIdentifier identifier) { - return Result.of(features.get(Objects.requireNonNull(identifier))); - } - - @Override - public Collection getConstraints() { - return Collections.unmodifiableCollection(constraints.values()); - } - - @Override - public Result getConstraint(IIdentifier identifier) { - return Result.of(constraints.get(Objects.requireNonNull(identifier))); - } - - @Override - public boolean hasConstraint(IIdentifier identifier) { - return constraints.containsKey(identifier); - } - - @Override - public boolean hasConstraint(IConstraint constraint) { - return constraints.containsKey(constraint.getIdentifier()); - } - - @Override - public int getNumberOfConstraints() { - return constraints.size(); - } - - @Override - public IIdentifier getIdentifier() { - return identifier; - } - - @Override - public Optional, Object>> getAttributes() { - return Optional.of(Collections.unmodifiableMap(attributeValues)); - } - - @Override - public void setAttributeValue(Attribute attribute, S value) { - if (value == null) { - removeAttributeValue(attribute); - return; - } - checkType(attribute, value); - validate(attribute, value); - attributeValues.put(attribute, value); - } - - @Override - @SuppressWarnings("unchecked") - public S removeAttributeValue(Attribute attribute) { - return (S) attributeValues.remove(attribute); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - return getIdentifier().equals(((FeatureModel) o).getIdentifier()); - } - - @Override - public int hashCode() { - return Objects.hash(getIdentifier()); - } - - @Override - public String toString() { - StringBuilder featureString = new StringBuilder(); - for (IFeatureTree root : featureTreeRoots) { - featureString.append(root.print()); - featureString.append('\n'); - } - return String.format( - "FeatureModel{features=%s, constraints=%s}", featureString.toString(), constraints.toString()); - } - - @Override - public void setName(String name) { - attributeValues.put(Attributes.NAME, name); - } - - @Override - public void setDescription(String description) { - attributeValues.put(Attributes.DESCRIPTION, description); - } - - @Override - public IFeatureTree addFeatureTreeRoot(IFeature feature) { - FeatureTree newTree = new FeatureTree(feature); - featureTreeRoots.add(newTree); - return newTree; - } - - @Override - public void addFeatureTreeRoot(IFeatureTree featureTree) { - featureTreeRoots.add(featureTree); - } - - @Override - public void removeFeatureTreeRoot(IFeature feature) { - for (Iterator it = featureTreeRoots.listIterator(); it.hasNext(); ) { - if (it.next().getFeature().equals(feature)) { - it.remove(); - } - } - } - - @Override - public void removeFeatureTreeRoot(IFeatureTree featureTree) { - for (Iterator it = featureTreeRoots.listIterator(); it.hasNext(); ) { - if (it.next() == featureTree) { - it.remove(); - } - } - } - - @Override - public IConstraint addConstraint(IFormula formula) { - IConstraint newConstraint = new Constraint(this, Trees.clone(formula)); - constraints.put(newConstraint.getIdentifier(), newConstraint); - return newConstraint; - } - - @Override - public boolean removeConstraint(IConstraint constraint) { - Objects.requireNonNull(constraint); - return constraints.remove(constraint.getIdentifier()) != null; - } - - @Override - public IFeature addFeature(String name) { - Objects.requireNonNull(name); - Feature feature = new Feature(this); - feature.setName(name); - features.put(feature.getIdentifier(), feature); - return feature; - } - - @Override - public boolean removeFeature(IFeature feature) { - return features.remove(feature.getIdentifier()) != null; - } - - @Override - public int getNumberOfFeatures() { - return features.size(); - } - - @Override - public Result getFeature(String name) { - return Result.ofOptional(features.entrySet().stream() - .map(e -> e.getValue()) - .filter(f -> f.getName().valueEquals(name)) - .findFirst()); - } - - @Override - public boolean hasFeature(IIdentifier identifier) { - return features.containsKey(identifier); - } - - @Override - public boolean hasFeature(IFeature feature) { - return features.containsKey(feature.getIdentifier()); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.data.Attribute; +import de.featjar.base.data.IAttributable.IMutatableAttributable; +import de.featjar.base.data.IAttribute; +import de.featjar.base.data.Maps; +import de.featjar.base.data.Result; +import de.featjar.base.data.identifier.IIdentifier; +import de.featjar.base.data.identifier.UUIDIdentifier; +import de.featjar.base.tree.Trees; +import de.featjar.feature.model.IFeatureModel.IMutableFeatureModel; +import de.featjar.formula.structure.IFormula; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class FeatureModel implements IMutableFeatureModel, IMutatableAttributable { + + protected final IIdentifier identifier; + + protected final List featureTreeRoots; + protected final LinkedHashMap features; + protected final LinkedHashMap constraints; + + protected final LinkedHashMap, Object> attributeValues; + + public FeatureModel() { + this(UUIDIdentifier.newInstance()); + } + + public FeatureModel(IIdentifier identifier) { + this.identifier = Objects.requireNonNull(identifier); + featureTreeRoots = new ArrayList<>(1); + features = Maps.empty(); + constraints = Maps.empty(); + attributeValues = new LinkedHashMap<>(4); + } + + protected FeatureModel(FeatureModel otherFeatureModel) { + identifier = otherFeatureModel.getNewIdentifier(); + + featureTreeRoots = new ArrayList<>(otherFeatureModel.featureTreeRoots.size()); + otherFeatureModel.featureTreeRoots.stream().forEach(t -> featureTreeRoots.add(Trees.clone(t))); + + features = new LinkedHashMap<>((int) (otherFeatureModel.features.size() * 1.5)); + otherFeatureModel.features.entrySet().stream() + .map(e -> e.getValue().clone(this)) + .forEach(f -> features.put(f.getIdentifier(), f)); + + constraints = new LinkedHashMap<>((int) (otherFeatureModel.constraints.size() * 1.5)); + otherFeatureModel.constraints.entrySet().stream() + .map(e -> e.getValue().clone(this)) + .forEach(c -> constraints.put(c.getIdentifier(), c)); + + attributeValues = otherFeatureModel.cloneAttributes(); + } + + @Override + public FeatureModel clone() { + return new FeatureModel(this); + } + + @Override + public FeatureModel getFeatureModel() { + return this; + } + + @Override + public List getRoots() { + return featureTreeRoots; + } + + @Override + public Collection getFeatures() { + return Collections.unmodifiableCollection(features.values()); + } + + @Override + public Result getFeature(IIdentifier identifier) { + return Result.of(features.get(Objects.requireNonNull(identifier))); + } + + @Override + public Collection getConstraints() { + return Collections.unmodifiableCollection(constraints.values()); + } + + @Override + public Result getConstraint(IIdentifier identifier) { + return Result.of(constraints.get(Objects.requireNonNull(identifier))); + } + + @Override + public boolean hasConstraint(IIdentifier identifier) { + return constraints.containsKey(identifier); + } + + @Override + public boolean hasConstraint(IConstraint constraint) { + return constraints.containsKey(constraint.getIdentifier()); + } + + @Override + public int getNumberOfConstraints() { + return constraints.size(); + } + + @Override + public IIdentifier getIdentifier() { + return identifier; + } + + @Override + public Optional, Object>> getAttributes() { + return Optional.of(Collections.unmodifiableMap(attributeValues)); + } + + @Override + public void setAttributeValue(Attribute attribute, S value) { + if (value == null) { + removeAttributeValue(attribute); + return; + } + checkType(attribute, value); + validate(attribute, value); + attributeValues.put(attribute, value); + } + + @Override + @SuppressWarnings("unchecked") + public S removeAttributeValue(Attribute attribute) { + return (S) attributeValues.remove(attribute); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + return getIdentifier().equals(((FeatureModel) o).getIdentifier()); + } + + @Override + public int hashCode() { + return Objects.hash(getIdentifier()); + } + + @Override + public String toString() { + StringBuilder featureString = new StringBuilder(); + for (IFeatureTree root : featureTreeRoots) { + featureString.append(root.print()); + featureString.append('\n'); + } + return String.format( + "FeatureModel{features=%s, constraints=%s}", featureString.toString(), constraints.toString()); + } + + @Override + public void setName(String name) { + attributeValues.put(Attributes.NAME, name); + } + + @Override + public void setDescription(String description) { + attributeValues.put(Attributes.DESCRIPTION, description); + } + + @Override + public IFeatureTree addFeatureTreeRoot(IFeature feature) { + FeatureTree newTree = new FeatureTree(feature); + featureTreeRoots.add(newTree); + return newTree; + } + + @Override + public void addFeatureTreeRoot(IFeatureTree featureTree) { + featureTreeRoots.add(featureTree); + } + + @Override + public void removeFeatureTreeRoot(IFeature feature) { + for (Iterator it = featureTreeRoots.listIterator(); it.hasNext(); ) { + if (it.next().getFeature().equals(feature)) { + it.remove(); + } + } + } + + @Override + public void removeFeatureTreeRoot(IFeatureTree featureTree) { + for (Iterator it = featureTreeRoots.listIterator(); it.hasNext(); ) { + if (it.next() == featureTree) { + it.remove(); + } + } + } + + @Override + public IConstraint addConstraint(IFormula formula) { + IConstraint newConstraint = new Constraint(this, Trees.clone(formula)); + constraints.put(newConstraint.getIdentifier(), newConstraint); + return newConstraint; + } + + @Override + public boolean removeConstraint(IConstraint constraint) { + Objects.requireNonNull(constraint); + return constraints.remove(constraint.getIdentifier()) != null; + } + + @Override + public IFeature addFeature(String name) { + Objects.requireNonNull(name); + Feature feature = new Feature(this); + feature.setName(name); + features.put(feature.getIdentifier(), feature); + return feature; + } + + @Override + public boolean removeFeature(IFeature feature) { + return features.remove(feature.getIdentifier()) != null; + } + + @Override + public int getNumberOfFeatures() { + return features.size(); + } + + @Override + public Result getFeature(String name) { + return Result.ofOptional(features.entrySet().stream() + .map(e -> e.getValue()) + .filter(f -> f.getName().valueEquals(name)) + .findFirst()); + } + + @Override + public boolean hasFeature(IIdentifier identifier) { + return features.containsKey(identifier); + } + + @Override + public boolean hasFeature(IFeature feature) { + return features.containsKey(feature.getIdentifier()); + } +} diff --git a/src/main/java/de/featjar/feature/model/FeatureTree.java b/src/main/java/de/featjar/feature/model/FeatureTree.java index b2abd8e6..50e43be6 100644 --- a/src/main/java/de/featjar/feature/model/FeatureTree.java +++ b/src/main/java/de/featjar/feature/model/FeatureTree.java @@ -1,289 +1,289 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.data.Attribute; -import de.featjar.base.data.IAttribute; -import de.featjar.base.data.Range; -import de.featjar.base.tree.structure.ARootedTree; -import de.featjar.base.tree.structure.ITree; -import de.featjar.feature.model.IFeatureTree.IMutableFeatureTree; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; - -public class FeatureTree extends ARootedTree implements IMutableFeatureTree { - - public final class Group { - private Range groupCardinality; - - private Group(int lowerBound, int upperBound) { - this.groupCardinality = Range.of(lowerBound, upperBound); - } - - public Group(Range groupRange) { - this.groupCardinality = Range.copy(groupRange); - } - - private Group(Group otherGroup) { - this.groupCardinality = Range.copy(otherGroup.groupCardinality); - } - - private void setBounds(int lowerBound, int upperBound) { - groupCardinality.setBounds(lowerBound, upperBound); - } - - public int getLowerBound() { - return groupCardinality.getLowerBound(); - } - - public int getUpperBound() { - return groupCardinality.getUpperBound(); - } - - public boolean isCardinalityGroup() { - return !isAlternative() && !isOr() && !isAnd(); - } - - public boolean isAlternative() { - return groupCardinality.is(1, 1); - } - - public boolean isOr() { - return groupCardinality.is(1, Range.OPEN); - } - - public boolean isAnd() { - return groupCardinality.is(0, Range.OPEN); - } - - public boolean allowsZero() { - return groupCardinality.getLowerBound() <= 0; - } - - public List getGroupSiblings() { - IFeatureTree parent = getParent().orElse(null); - return (parent == null) - ? List.of() - : parent.getChildren().stream() - .filter(t -> t.getParentGroupID() == parentGroupID) - .collect(Collectors.toList()); - } - - @Override - public int hashCode() { - return System.identityHashCode(this); - } - - @Override - public boolean equals(Object obj) { - return this == obj; - } - - @Override - protected Group clone() { - return new Group(this); - } - - @Override - public String toString() { - return groupCardinality.toString(); - } - } - - protected final IFeature feature; - - protected int parentGroupID; - - protected Range cardinality; - protected ArrayList childrenGroups; - - protected LinkedHashMap, Object> attributeValues; - - protected FeatureTree(IFeature feature) { - this.feature = Objects.requireNonNull(feature); - cardinality = Range.of(0, 1); - childrenGroups = new ArrayList<>(1); - childrenGroups.add(new Group(Range.atLeast(0))); - } - - protected FeatureTree(FeatureTree otherFeatureTree) { - feature = otherFeatureTree.feature; - parentGroupID = otherFeatureTree.parentGroupID; - cardinality = otherFeatureTree.cardinality.clone(); - childrenGroups = new ArrayList<>(otherFeatureTree.childrenGroups.size()); - otherFeatureTree.childrenGroups.stream().map(Group::clone).forEach(childrenGroups::add); - attributeValues = otherFeatureTree.cloneAttributes(); - } - - @Override - public IFeature getFeature() { - return feature; - } - - @Override - public int getParentGroupID() { - return parentGroupID; - } - - @Override - public Optional getParentGroup() { - return parent == null ? Optional.empty() : parent.getChildrenGroup(parentGroupID); - } - - @Override - public List getChildrenGroups() { - return Collections.unmodifiableList(childrenGroups); - } - - @Override - public Optional getChildrenGroup(int groupID) { - return Optional.ofNullable(getChildrenGroups().get(groupID)); - } - - @Override - public List getChildren(int groupID) { - return getChildren().stream() - .filter(c -> c.getParentGroupID() == groupID) - .collect(Collectors.toList()); - } - - @Override - public String toString() { - return feature.getName().orElse(""); - } - - @Override - public Optional, Object>> getAttributes() { - return attributeValues == null ? Optional.empty() : Optional.of(Collections.unmodifiableMap(attributeValues)); - } - - @Override - public List getRoots() { - return List.of(this); - } - - @Override - public int getFeatureCardinalityLowerBound() { - return cardinality.getLowerBound(); - } - - @Override - public int getFeatureCardinalityUpperBound() { - return cardinality.getUpperBound(); - } - - @Override - public ITree cloneNode() { - return new FeatureTree(this); - } - - @Override - public boolean equalsNode(IFeatureTree other) { - if (this == other) return true; - if (other == null || getClass() != other.getClass()) return false; - FeatureTree otherFeatureTree = (FeatureTree) other; - return parentGroupID == otherFeatureTree.parentGroupID - && Objects.equals(feature, otherFeatureTree.feature) - && Objects.equals(childrenGroups, otherFeatureTree.childrenGroups); - } - - @Override - public int hashCodeNode() { - return Objects.hash(feature, parentGroupID, childrenGroups); - } - - @Override - public int addCardinalityGroup(int lowerBound, int upperBound) { - childrenGroups.add(new Group(lowerBound, upperBound)); - return childrenGroups.size() - 1; - } - - @Override - public int addCardinalityGroup(Range groupRange) { - childrenGroups.add(new Group(groupRange)); - return childrenGroups.size() - 1; - } - - public void setParentGroupID(int groupID) { - if (parent == null) throw new IllegalArgumentException("Cannot set groupID for root feature!"); - if (groupID < 0) throw new IllegalArgumentException(String.format("groupID must be positive (%d)", groupID)); - if (groupID >= parent.getChildrenGroups().size()) - throw new IllegalArgumentException( - String.format("groupID must be smaller than number of groups in parent feature (%d)", groupID)); - this.parentGroupID = groupID; - } - - @Override - public void setFeatureCardinality(Range featureCardinality) { - this.cardinality = Range.copy(featureCardinality); - } - - @Override - public void makeMandatory() { - if (cardinality.getUpperBound() == 0) { - cardinality = Range.exactly(1); - } else { - cardinality.setLowerBound(1); - } - } - - @Override - public void makeOptional() { - cardinality.setLowerBound(0); - } - - @Override - public void setAttributeValue(Attribute attribute, S value) { - if (value == null) { - removeAttributeValue(attribute); - return; - } - checkType(attribute, value); - validate(attribute, value); - if (attributeValues == null) { - attributeValues = new LinkedHashMap<>(); - } - attributeValues.put(attribute, value); - } - - @Override - @SuppressWarnings("unchecked") - public S removeAttributeValue(Attribute attribute) { - if (attributeValues == null) { - attributeValues = new LinkedHashMap<>(); - } - return (S) attributeValues.remove(attribute); - } - - @Override - public void toCardinalityGroup(int groupID, int lowerBound, int upperBound) { - Group group = getChildrenGroups().get(groupID); - if (group != null) { - group.setBounds(lowerBound, upperBound); - } - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.data.Attribute; +import de.featjar.base.data.IAttribute; +import de.featjar.base.data.Range; +import de.featjar.base.tree.structure.ARootedTree; +import de.featjar.base.tree.structure.ITree; +import de.featjar.feature.model.IFeatureTree.IMutableFeatureTree; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +public class FeatureTree extends ARootedTree implements IMutableFeatureTree { + + public final class Group { + private Range groupCardinality; + + private Group(int lowerBound, int upperBound) { + this.groupCardinality = Range.of(lowerBound, upperBound); + } + + public Group(Range groupRange) { + this.groupCardinality = Range.copy(groupRange); + } + + private Group(Group otherGroup) { + this.groupCardinality = Range.copy(otherGroup.groupCardinality); + } + + private void setBounds(int lowerBound, int upperBound) { + groupCardinality.setBounds(lowerBound, upperBound); + } + + public int getLowerBound() { + return groupCardinality.getLowerBound(); + } + + public int getUpperBound() { + return groupCardinality.getUpperBound(); + } + + public boolean isCardinalityGroup() { + return !isAlternative() && !isOr() && !isAnd(); + } + + public boolean isAlternative() { + return groupCardinality.is(1, 1); + } + + public boolean isOr() { + return groupCardinality.is(1, Range.OPEN); + } + + public boolean isAnd() { + return groupCardinality.is(0, Range.OPEN); + } + + public boolean allowsZero() { + return groupCardinality.getLowerBound() <= 0; + } + + public List getGroupSiblings() { + IFeatureTree parent = getParent().orElse(null); + return (parent == null) + ? List.of() + : parent.getChildren().stream() + .filter(t -> t.getParentGroupID() == parentGroupID) + .collect(Collectors.toList()); + } + + @Override + public int hashCode() { + return System.identityHashCode(this); + } + + @Override + public boolean equals(Object obj) { + return this == obj; + } + + @Override + protected Group clone() { + return new Group(this); + } + + @Override + public String toString() { + return groupCardinality.toString(); + } + } + + protected final IFeature feature; + + protected int parentGroupID; + + protected Range cardinality; + protected ArrayList childrenGroups; + + protected LinkedHashMap, Object> attributeValues; + + protected FeatureTree(IFeature feature) { + this.feature = Objects.requireNonNull(feature); + cardinality = Range.of(0, 1); + childrenGroups = new ArrayList<>(1); + childrenGroups.add(new Group(Range.atLeast(0))); + } + + protected FeatureTree(FeatureTree otherFeatureTree) { + feature = otherFeatureTree.feature; + parentGroupID = otherFeatureTree.parentGroupID; + cardinality = otherFeatureTree.cardinality.clone(); + childrenGroups = new ArrayList<>(otherFeatureTree.childrenGroups.size()); + otherFeatureTree.childrenGroups.stream().map(Group::clone).forEach(childrenGroups::add); + attributeValues = otherFeatureTree.cloneAttributes(); + } + + @Override + public IFeature getFeature() { + return feature; + } + + @Override + public int getParentGroupID() { + return parentGroupID; + } + + @Override + public Optional getParentGroup() { + return parent == null ? Optional.empty() : parent.getChildrenGroup(parentGroupID); + } + + @Override + public List getChildrenGroups() { + return Collections.unmodifiableList(childrenGroups); + } + + @Override + public Optional getChildrenGroup(int groupID) { + return Optional.ofNullable(getChildrenGroups().get(groupID)); + } + + @Override + public List getChildren(int groupID) { + return getChildren().stream() + .filter(c -> c.getParentGroupID() == groupID) + .collect(Collectors.toList()); + } + + @Override + public String toString() { + return feature.getName().orElse(""); + } + + @Override + public Optional, Object>> getAttributes() { + return attributeValues == null ? Optional.empty() : Optional.of(Collections.unmodifiableMap(attributeValues)); + } + + @Override + public List getRoots() { + return List.of(this); + } + + @Override + public int getFeatureCardinalityLowerBound() { + return cardinality.getLowerBound(); + } + + @Override + public int getFeatureCardinalityUpperBound() { + return cardinality.getUpperBound(); + } + + @Override + public ITree cloneNode() { + return new FeatureTree(this); + } + + @Override + public boolean equalsNode(IFeatureTree other) { + if (this == other) return true; + if (other == null || getClass() != other.getClass()) return false; + FeatureTree otherFeatureTree = (FeatureTree) other; + return parentGroupID == otherFeatureTree.parentGroupID + && Objects.equals(feature, otherFeatureTree.feature) + && Objects.equals(childrenGroups, otherFeatureTree.childrenGroups); + } + + @Override + public int hashCodeNode() { + return Objects.hash(feature, parentGroupID, childrenGroups); + } + + @Override + public int addCardinalityGroup(int lowerBound, int upperBound) { + childrenGroups.add(new Group(lowerBound, upperBound)); + return childrenGroups.size() - 1; + } + + @Override + public int addCardinalityGroup(Range groupRange) { + childrenGroups.add(new Group(groupRange)); + return childrenGroups.size() - 1; + } + + public void setParentGroupID(int groupID) { + if (parent == null) throw new IllegalArgumentException("Cannot set groupID for root feature!"); + if (groupID < 0) throw new IllegalArgumentException(String.format("groupID must be positive (%d)", groupID)); + if (groupID >= parent.getChildrenGroups().size()) + throw new IllegalArgumentException( + String.format("groupID must be smaller than number of groups in parent feature (%d)", groupID)); + this.parentGroupID = groupID; + } + + @Override + public void setFeatureCardinality(Range featureCardinality) { + this.cardinality = Range.copy(featureCardinality); + } + + @Override + public void makeMandatory() { + if (cardinality.getUpperBound() == 0) { + cardinality = Range.exactly(1); + } else { + cardinality.setLowerBound(1); + } + } + + @Override + public void makeOptional() { + cardinality.setLowerBound(0); + } + + @Override + public void setAttributeValue(Attribute attribute, S value) { + if (value == null) { + removeAttributeValue(attribute); + return; + } + checkType(attribute, value); + validate(attribute, value); + if (attributeValues == null) { + attributeValues = new LinkedHashMap<>(); + } + attributeValues.put(attribute, value); + } + + @Override + @SuppressWarnings("unchecked") + public S removeAttributeValue(Attribute attribute) { + if (attributeValues == null) { + attributeValues = new LinkedHashMap<>(); + } + return (S) attributeValues.remove(attribute); + } + + @Override + public void toCardinalityGroup(int groupID, int lowerBound, int upperBound) { + Group group = getChildrenGroups().get(groupID); + if (group != null) { + group.setBounds(lowerBound, upperBound); + } + } +} diff --git a/src/main/java/de/featjar/feature/model/IConstraint.java b/src/main/java/de/featjar/feature/model/IConstraint.java index 456d4bc6..a398f972 100644 --- a/src/main/java/de/featjar/feature/model/IConstraint.java +++ b/src/main/java/de/featjar/feature/model/IConstraint.java @@ -1,86 +1,86 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.data.*; -import de.featjar.feature.model.mixins.IHasCommonAttributes; -import de.featjar.formula.structure.IFormula; -import de.featjar.formula.structure.term.value.Variable; -import java.util.LinkedHashSet; - -/** - * A constraint describes some restriction on the valid configurations represented by a {@link FeatureModel}. - * It is attached to a {@link FeatureModel} and represented as a {@link IFormula} over {@link Feature} variables. - * For safe mutation, rely only on the methods of {@link IMutableConstraint}. - * - * @author Elias Kuiter - */ -public interface IConstraint extends IFeatureModelElement, IHasCommonAttributes { - - IConstraint clone(); - - IConstraint clone(IFeatureModel newFeatureModel); - - IFormula getFormula(); - - static LinkedHashSet getReferencedFeatures(IFormula formula, IFeatureModel featureModel) { - return formula.getVariableStream() - .map(Variable::getName) - .map(name -> { - Result feature = featureModel.getFeature(name); - if (feature.isEmpty()) throw new RuntimeException("encountered unknown feature " + name); - return feature.get(); - }) - .collect(Sets.toSet()); - } - - default LinkedHashSet getReferencedFeatures() { - return getReferencedFeatures(getFormula(), getFeatureModel()); - } - - default LinkedHashSet getTags() { - return getAttributeValue(Attributes.TAGS).get(); - } - - default IMutableConstraint mutate() { - return (IMutableConstraint) this; - } - - static interface IMutableConstraint extends IConstraint, IHasMutableCommonAttributes { - void setFormula(IFormula formula); - - default void remove() { - getFeatureModel().mutate().removeConstraint(this); - } - - default void setTags(LinkedHashSet tags) { - setAttributeValue(Attributes.TAGS, tags); - } - - default boolean addTag(String tag) { - return getTags().add(tag); - } - - default boolean removeTag(String tag) { - return getTags().remove(tag); - } - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.data.*; +import de.featjar.feature.model.mixins.IHasCommonAttributes; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.term.value.Variable; +import java.util.LinkedHashSet; + +/** + * A constraint describes some restriction on the valid configurations represented by a {@link FeatureModel}. + * It is attached to a {@link FeatureModel} and represented as a {@link IFormula} over {@link Feature} variables. + * For safe mutation, rely only on the methods of {@link IMutableConstraint}. + * + * @author Elias Kuiter + */ +public interface IConstraint extends IFeatureModelElement, IHasCommonAttributes { + + IConstraint clone(); + + IConstraint clone(IFeatureModel newFeatureModel); + + IFormula getFormula(); + + static LinkedHashSet getReferencedFeatures(IFormula formula, IFeatureModel featureModel) { + return formula.getVariableStream() + .map(Variable::getName) + .map(name -> { + Result feature = featureModel.getFeature(name); + if (feature.isEmpty()) throw new RuntimeException("encountered unknown feature " + name); + return feature.get(); + }) + .collect(Sets.toSet()); + } + + default LinkedHashSet getReferencedFeatures() { + return getReferencedFeatures(getFormula(), getFeatureModel()); + } + + default LinkedHashSet getTags() { + return getAttributeValue(Attributes.TAGS).get(); + } + + default IMutableConstraint mutate() { + return (IMutableConstraint) this; + } + + static interface IMutableConstraint extends IConstraint, IHasMutableCommonAttributes { + void setFormula(IFormula formula); + + default void remove() { + getFeatureModel().mutate().removeConstraint(this); + } + + default void setTags(LinkedHashSet tags) { + setAttributeValue(Attributes.TAGS, tags); + } + + default boolean addTag(String tag) { + return getTags().add(tag); + } + + default boolean removeTag(String tag) { + return getTags().remove(tag); + } + } +} diff --git a/src/main/java/de/featjar/feature/model/IFeature.java b/src/main/java/de/featjar/feature/model/IFeature.java index 735a09a4..cabc2a68 100644 --- a/src/main/java/de/featjar/feature/model/IFeature.java +++ b/src/main/java/de/featjar/feature/model/IFeature.java @@ -1,132 +1,132 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.data.Problem; -import de.featjar.base.data.Result; -import de.featjar.base.data.Sets; -import de.featjar.base.data.identifier.AIdentifier; -import de.featjar.feature.model.mixins.IHasCommonAttributes; -import java.util.LinkedHashSet; - -/** - * A feature in a {@link FeatureModel} describes some functionality of a software system. - * It is attached to a {@link FeatureModel} and labels a {@link FeatureTree}. - * For safe mutation, rely only on the methods of {@link IMutableFeature}. - * A {@link Feature} is uniquely determined by its immutable {@link AIdentifier} - * or name (obtained with {@link IHasCommonAttributes#getName()}). - * In contrast to a feature's identifier, its name is mutable and should therefore be used sparsely - * to avoid cache invalidation and renaming issues. - * - * @author Elias Kuiter - */ -public interface IFeature extends IFeatureModelElement, IHasCommonAttributes { - - Result getFeatureTree(); - - Class getType(); - - IFeature clone(); - - IFeature clone(IFeatureModel newFeatureModel); - - default boolean isAbstract() { - return (boolean) getAttributeValue(Attributes.ABSTRACT).get(); - } - - default boolean isConcrete() { - return !isAbstract(); - } - - default boolean isHidden() { - return (boolean) getAttributeValue(Attributes.HIDDEN).get(); - } - - /** - * Checks if there is a hidden feature higher up in the hierarchy. - * Does not only check the direct parent, but all parents above. - * @return {@code true} if any of this features's parents are hidden, {@code false} otherwise. - */ - default boolean hasHiddenParent() { - IFeatureTree parentTreeElement = - getFeatureTree().orElseThrow(Problem::toException).getParent().orElse(null); - - while (parentTreeElement != null) { - if (parentTreeElement.getFeature().isHidden()) { - return true; - } - parentTreeElement = parentTreeElement.getParent().orElse(null); - } - return false; - } - - default boolean isVisible() { - return !isHidden(); - } - - default LinkedHashSet getReferencingConstraints() { - return getFeatureModel().getConstraints().stream() - .filter(constraint -> - constraint.getReferencedFeatures().stream().anyMatch(this::equals)) - .collect(Sets.toSet()); - } - - default IMutableFeature mutate() { - return (IMutableFeature) this; - } - - static interface IMutableFeature extends IFeature, IHasMutableCommonAttributes { - - void setType(Class type); - - default void setAbstract(boolean value) { - setAttributeValue(Attributes.ABSTRACT, value); - } - - default boolean toggleAbstract() { - return toggleAttributeValue(Attributes.ABSTRACT); - } - - default void setAbstract() { - setAbstract(true); - } - - default void setConcrete() { - setAbstract(false); - } - - default void setHidden(boolean value) { - setAttributeValue(Attributes.HIDDEN, value); - } - - default boolean toggleHidden() { - return toggleAttributeValue(Attributes.HIDDEN); - } - - default void setHidden() { - setHidden(true); - } - - default void setVisible() { - setHidden(false); - } - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.data.Problem; +import de.featjar.base.data.Result; +import de.featjar.base.data.Sets; +import de.featjar.base.data.identifier.AIdentifier; +import de.featjar.feature.model.mixins.IHasCommonAttributes; +import java.util.LinkedHashSet; + +/** + * A feature in a {@link FeatureModel} describes some functionality of a software system. + * It is attached to a {@link FeatureModel} and labels a {@link FeatureTree}. + * For safe mutation, rely only on the methods of {@link IMutableFeature}. + * A {@link Feature} is uniquely determined by its immutable {@link AIdentifier} + * or name (obtained with {@link IHasCommonAttributes#getName()}). + * In contrast to a feature's identifier, its name is mutable and should therefore be used sparsely + * to avoid cache invalidation and renaming issues. + * + * @author Elias Kuiter + */ +public interface IFeature extends IFeatureModelElement, IHasCommonAttributes { + + Result getFeatureTree(); + + Class getType(); + + IFeature clone(); + + IFeature clone(IFeatureModel newFeatureModel); + + default boolean isAbstract() { + return (boolean) getAttributeValue(Attributes.ABSTRACT).get(); + } + + default boolean isConcrete() { + return !isAbstract(); + } + + default boolean isHidden() { + return (boolean) getAttributeValue(Attributes.HIDDEN).get(); + } + + /** + * Checks if there is a hidden feature higher up in the hierarchy. + * Does not only check the direct parent, but all parents above. + * @return {@code true} if any of this features's parents are hidden, {@code false} otherwise. + */ + default boolean hasHiddenParent() { + IFeatureTree parentTreeElement = + getFeatureTree().orElseThrow(Problem::toException).getParent().orElse(null); + + while (parentTreeElement != null) { + if (parentTreeElement.getFeature().isHidden()) { + return true; + } + parentTreeElement = parentTreeElement.getParent().orElse(null); + } + return false; + } + + default boolean isVisible() { + return !isHidden(); + } + + default LinkedHashSet getReferencingConstraints() { + return getFeatureModel().getConstraints().stream() + .filter(constraint -> + constraint.getReferencedFeatures().stream().anyMatch(this::equals)) + .collect(Sets.toSet()); + } + + default IMutableFeature mutate() { + return (IMutableFeature) this; + } + + static interface IMutableFeature extends IFeature, IHasMutableCommonAttributes { + + void setType(Class type); + + default void setAbstract(boolean value) { + setAttributeValue(Attributes.ABSTRACT, value); + } + + default boolean toggleAbstract() { + return toggleAttributeValue(Attributes.ABSTRACT); + } + + default void setAbstract() { + setAbstract(true); + } + + default void setConcrete() { + setAbstract(false); + } + + default void setHidden(boolean value) { + setAttributeValue(Attributes.HIDDEN, value); + } + + default boolean toggleHidden() { + return toggleAttributeValue(Attributes.HIDDEN); + } + + default void setHidden() { + setHidden(true); + } + + default void setVisible() { + setHidden(false); + } + } +} diff --git a/src/main/java/de/featjar/feature/model/IFeatureModel.java b/src/main/java/de/featjar/feature/model/IFeatureModel.java index 356777d1..1c5b80a4 100644 --- a/src/main/java/de/featjar/feature/model/IFeatureModel.java +++ b/src/main/java/de/featjar/feature/model/IFeatureModel.java @@ -1,90 +1,90 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.data.Result; -import de.featjar.base.data.identifier.IIdentifier; -import de.featjar.feature.model.mixins.IHasCommonAttributes; -import de.featjar.feature.model.mixins.IHasConstraints; -import de.featjar.feature.model.mixins.IHasFeatureTree; -import de.featjar.formula.structure.IFormula; -import java.util.Collection; - -/** - * A feature model represents the configuration space of a software system. - * We equate feature models with feature diagrams - * (i.e., a {@link FeatureTree} labeled with features and a list of {@link Constraint constraints}). - * For safe mutation, rely only on the methods of {@link IMutableFeatureModel}. - * - * cache assumes that features/constraints are only added/deleted through the mutator, not manually - * - * @author Elias Kuiter - */ -public interface IFeatureModel extends IFeatureModelElement, IHasCommonAttributes, IHasFeatureTree, IHasConstraints { - // TODO put flattened fm into store (maybe dispatch mutators of flattened model to original models) - - // TODO: we allow all kinds of modeling constructs, but not all analyses/computations support all constructs. - // e.g., multiplicities are difficult to map to SAT. somehow, this should be checked. - // maybe store required/incompatible capabilities for computations? eg., incompatible with - // Plaisted-Greenbaum/multiplicities/...? - // and then implement different alternative algorithms with different capabilities. - // maybe this could be encoded first-class as a feature model. - // this could even be used to generate query plans (e.g., find some configuration that counts my formula). - // every plugin defines a feature model (uvl) that restricts what its extensions can and cannot do (replacing - // extensions.xml) - - IFeatureModel clone(); - - Collection getFeatures(); - - int getNumberOfFeatures(); - - Result getFeature(IIdentifier identifier); - - Result getFeature(String name); - - boolean hasFeature(IIdentifier identifier); - - boolean hasFeature(IFeature feature); - - default IMutableFeatureModel mutate() { - return (IMutableFeatureModel) this; - } - - static interface IMutableFeatureModel extends IFeatureModel, IHasMutableCommonAttributes { - - IFeature addFeature(String name); - - boolean removeFeature(IFeature feature); - - IConstraint addConstraint(IFormula formula); - - boolean removeConstraint(IConstraint constraint); - - IFeatureTree addFeatureTreeRoot(IFeature feature); - - void addFeatureTreeRoot(IFeatureTree featureTree); - - void removeFeatureTreeRoot(IFeatureTree featureTree); - - void removeFeatureTreeRoot(IFeature feature); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.data.Result; +import de.featjar.base.data.identifier.IIdentifier; +import de.featjar.feature.model.mixins.IHasCommonAttributes; +import de.featjar.feature.model.mixins.IHasConstraints; +import de.featjar.feature.model.mixins.IHasFeatureTree; +import de.featjar.formula.structure.IFormula; +import java.util.Collection; + +/** + * A feature model represents the configuration space of a software system. + * We equate feature models with feature diagrams + * (i.e., a {@link FeatureTree} labeled with features and a list of {@link Constraint constraints}). + * For safe mutation, rely only on the methods of {@link IMutableFeatureModel}. + * + * cache assumes that features/constraints are only added/deleted through the mutator, not manually + * + * @author Elias Kuiter + */ +public interface IFeatureModel extends IFeatureModelElement, IHasCommonAttributes, IHasFeatureTree, IHasConstraints { + // TODO put flattened fm into store (maybe dispatch mutators of flattened model to original models) + + // TODO: we allow all kinds of modeling constructs, but not all analyses/computations support all constructs. + // e.g., multiplicities are difficult to map to SAT. somehow, this should be checked. + // maybe store required/incompatible capabilities for computations? eg., incompatible with + // Plaisted-Greenbaum/multiplicities/...? + // and then implement different alternative algorithms with different capabilities. + // maybe this could be encoded first-class as a feature model. + // this could even be used to generate query plans (e.g., find some configuration that counts my formula). + // every plugin defines a feature model (uvl) that restricts what its extensions can and cannot do (replacing + // extensions.xml) + + IFeatureModel clone(); + + Collection getFeatures(); + + int getNumberOfFeatures(); + + Result getFeature(IIdentifier identifier); + + Result getFeature(String name); + + boolean hasFeature(IIdentifier identifier); + + boolean hasFeature(IFeature feature); + + default IMutableFeatureModel mutate() { + return (IMutableFeatureModel) this; + } + + static interface IMutableFeatureModel extends IFeatureModel, IHasMutableCommonAttributes { + + IFeature addFeature(String name); + + boolean removeFeature(IFeature feature); + + IConstraint addConstraint(IFormula formula); + + boolean removeConstraint(IConstraint constraint); + + IFeatureTree addFeatureTreeRoot(IFeature feature); + + void addFeatureTreeRoot(IFeatureTree featureTree); + + void removeFeatureTreeRoot(IFeatureTree featureTree); + + void removeFeatureTreeRoot(IFeature feature); + } +} diff --git a/src/main/java/de/featjar/feature/model/IFeatureModelElement.java b/src/main/java/de/featjar/feature/model/IFeatureModelElement.java index 2fa60399..68ca8bd6 100644 --- a/src/main/java/de/featjar/feature/model/IFeatureModelElement.java +++ b/src/main/java/de/featjar/feature/model/IFeatureModelElement.java @@ -1,28 +1,28 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.data.IAttributable; -import de.featjar.base.data.identifier.IIdentifiable; - -public interface IFeatureModelElement extends IIdentifiable, IAttributable, Cloneable { - IFeatureModel getFeatureModel(); -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.data.IAttributable; +import de.featjar.base.data.identifier.IIdentifiable; + +public interface IFeatureModelElement extends IIdentifiable, IAttributable, Cloneable { + IFeatureModel getFeatureModel(); +} diff --git a/src/main/java/de/featjar/feature/model/IFeatureTree.java b/src/main/java/de/featjar/feature/model/IFeatureTree.java index 803d0c54..e1c1da0e 100644 --- a/src/main/java/de/featjar/feature/model/IFeatureTree.java +++ b/src/main/java/de/featjar/feature/model/IFeatureTree.java @@ -1,255 +1,255 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import de.featjar.base.data.IAttributable; -import de.featjar.base.data.Range; -import de.featjar.base.data.Result; -import de.featjar.base.tree.structure.ARootedTree; -import de.featjar.base.tree.structure.IRootedTree; -import de.featjar.feature.model.FeatureTree.Group; -import de.featjar.feature.model.mixins.IHasFeatureTree; -import java.util.List; -import java.util.Optional; - -/** - * An ordered {@link ARootedTree} labeled with {@link Feature features}. - * Implements some concepts from feature-oriented domain analysis, such as - * mandatory/optional features and groups. - * - * @author Elias Kuiter - * @author Sebastian Krieter - */ -public interface IFeatureTree extends IRootedTree, IAttributable, IHasFeatureTree { - - IFeature getFeature(); - - /** - * {@return the groups of this feature's children.} - * This list may contain {@code null}. - */ - List getChildrenGroups(); - - /** - * {@return the group of this feature's children with the given id.} - * @param groupID the groupID - */ - Optional getChildrenGroup(int groupID); - - /** - * {@return all children within the group with the given id.} - * @param groupID the groupID - */ - List getChildren(int groupID); - - /** - * {@return the group of this feature. (The group of this feature's parent, in which this feature is contained.)} - * @see #getParentGroupID() - */ - Optional getParentGroup(); - - /** - * {@return the id of the group of this feature.} - * @see #getParentGroup() - */ - int getParentGroupID(); - - int getFeatureCardinalityLowerBound(); - - int getFeatureCardinalityUpperBound(); - - default boolean isMandatory() { - return getFeatureCardinalityLowerBound() > 0; - } - - default boolean isOptional() { - return getFeatureCardinalityLowerBound() <= 0; - } - - default IMutableFeatureTree mutate() { - return (IMutableFeatureTree) this; - } - - static interface IMutableFeatureTree extends IFeatureTree, IMutatableAttributable { - - default IFeatureTree addFeatureBelow(IFeature newFeature) { - return addFeatureBelow(newFeature, getChildrenCount(), 0); - } - - default IFeatureTree addFeatureBelow(IFeature newFeature, int index) { - return addFeatureBelow(newFeature, index, 0); - } - - default IFeatureTree addFeatureBelow(IFeature newFeature, int index, int groupID) { - FeatureTree newTree = new FeatureTree(newFeature); - addChild(index, newTree); - newTree.setParentGroupID(groupID); - return newTree; - } - - default IFeatureTree addFeatureAbove(IFeature newFeature) { - FeatureTree newTree = new FeatureTree(newFeature); - Result parent = getParent(); - if (parent.isPresent()) { - parent.get().replaceChild(this, newTree); - } - newTree.addChild(this); - setParentGroupID(0); - return newTree; - } - - default void removeFromTree() { // TODO what about the containing constraints? - Result parent = getParent(); - if (parent.isPresent()) { - int childIndex = parent.get().getChildIndex(this).orElseThrow(); - parent.get().removeChild(this); - // TODO improve group handling, probably needs slicing - for (Group group : getChildrenGroups()) { - parent.get().mutate().addCardinalityGroup(group.getLowerBound(), group.getUpperBound()); - } - for (IFeatureTree child : getChildren()) { - parent.get().mutate().addChild(childIndex++, child); - } - } - } - - void setParentGroupID(int groupID); - - void setFeatureCardinality(Range featureRange); - - void makeMandatory(); - - void makeOptional(); - - int addCardinalityGroup(int lowerBound, int upperBound); - - default int addCardinalityGroup(Range groupRange) { - return addCardinalityGroup(groupRange.getLowerBound(), groupRange.getUpperBound()); - } - - default int addAndGroup() { - return addCardinalityGroup(0, Range.OPEN); - } - - default int addAlternativeGroup() { - return addCardinalityGroup(1, 1); - } - - default int addOrGroup() { - return addCardinalityGroup(1, Range.OPEN); - } - - /** - * Changes the cardinality of the children group with the given id. - * - * @param groupID the id of the group to change - * @param lowerBound the new lower bound - * @param upperBound the new upper bound - */ - void toCardinalityGroup(int groupID, int lowerBound, int upperBound); - - /** - * Changes the cardinality of the children group with the given id. - * - * @param groupID the id of the group to change - * @param groupCardinality the new cardinality - */ - default void toCardinalityGroup(int groupID, Range groupCardinality) { - toCardinalityGroup(groupID, groupCardinality.getLowerBound(), groupCardinality.getUpperBound()); - } - - /** - * Change children group to and group. - * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(groupID, 0, Range.OPEN)}. - * - * @param groupID the id of the group to change - */ - default void toAndGroup(int groupID) { - toCardinalityGroup(groupID, 0, Range.OPEN); - } - - /** - * Change children group to or group. - * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(groupID, 1, Range.OPEN)}. - * - * @param groupID the id of the group to change - */ - default void toOrGroup(int groupID) { - toCardinalityGroup(groupID, 1, Range.OPEN); - } - - /** - * Change children group to alternative group. - * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(groupID, 1, 1)}. - * - * @param groupID the id of the group to change - */ - default void toAlternativeGroup(int groupID) { - toCardinalityGroup(groupID, 1, 1); - } - - /** - * Changes the cardinality of the first children group. - * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(0, groupCardinality)}. - * - * @param groupCardinality the new cardinality - */ - default void toCardinalityGroup(Range groupCardinality) { - toCardinalityGroup(0, groupCardinality); - } - - /** - * Change first children group to cardinality group. - * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(0, lowerBound, upperBound)}. - * - * @param lowerBound the new lower bound - * @param upperBound the new upper bound - */ - default void toCardinalityGroup(int lowerBound, int upperBound) { - toCardinalityGroup(0, lowerBound, upperBound); - } - - /** - * Change first children group to and group. - * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(0, 0, Range.OPEN)}. - * - */ - default void toAndGroup() { - toAndGroup(0); - } - - /** - * Change first children group to alternative group. - * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(0, 1, Range.OPEN)}. - */ - default void toAlternativeGroup() { - toAlternativeGroup(0); - } - - /** - * Change first children group to or group. - * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(0, 1, 1)}. - */ - default void toOrGroup() { - toOrGroup(0); - } - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import de.featjar.base.data.IAttributable; +import de.featjar.base.data.Range; +import de.featjar.base.data.Result; +import de.featjar.base.tree.structure.ARootedTree; +import de.featjar.base.tree.structure.IRootedTree; +import de.featjar.feature.model.FeatureTree.Group; +import de.featjar.feature.model.mixins.IHasFeatureTree; +import java.util.List; +import java.util.Optional; + +/** + * An ordered {@link ARootedTree} labeled with {@link Feature features}. + * Implements some concepts from feature-oriented domain analysis, such as + * mandatory/optional features and groups. + * + * @author Elias Kuiter + * @author Sebastian Krieter + */ +public interface IFeatureTree extends IRootedTree, IAttributable, IHasFeatureTree { + + IFeature getFeature(); + + /** + * {@return the groups of this feature's children.} + * This list may contain {@code null}. + */ + List getChildrenGroups(); + + /** + * {@return the group of this feature's children with the given id.} + * @param groupID the groupID + */ + Optional getChildrenGroup(int groupID); + + /** + * {@return all children within the group with the given id.} + * @param groupID the groupID + */ + List getChildren(int groupID); + + /** + * {@return the group of this feature. (The group of this feature's parent, in which this feature is contained.)} + * @see #getParentGroupID() + */ + Optional getParentGroup(); + + /** + * {@return the id of the group of this feature.} + * @see #getParentGroup() + */ + int getParentGroupID(); + + int getFeatureCardinalityLowerBound(); + + int getFeatureCardinalityUpperBound(); + + default boolean isMandatory() { + return getFeatureCardinalityLowerBound() > 0; + } + + default boolean isOptional() { + return getFeatureCardinalityLowerBound() <= 0; + } + + default IMutableFeatureTree mutate() { + return (IMutableFeatureTree) this; + } + + static interface IMutableFeatureTree extends IFeatureTree, IMutatableAttributable { + + default IFeatureTree addFeatureBelow(IFeature newFeature) { + return addFeatureBelow(newFeature, getChildrenCount(), 0); + } + + default IFeatureTree addFeatureBelow(IFeature newFeature, int index) { + return addFeatureBelow(newFeature, index, 0); + } + + default IFeatureTree addFeatureBelow(IFeature newFeature, int index, int groupID) { + FeatureTree newTree = new FeatureTree(newFeature); + addChild(index, newTree); + newTree.setParentGroupID(groupID); + return newTree; + } + + default IFeatureTree addFeatureAbove(IFeature newFeature) { + FeatureTree newTree = new FeatureTree(newFeature); + Result parent = getParent(); + if (parent.isPresent()) { + parent.get().replaceChild(this, newTree); + } + newTree.addChild(this); + setParentGroupID(0); + return newTree; + } + + default void removeFromTree() { // TODO what about the containing constraints? + Result parent = getParent(); + if (parent.isPresent()) { + int childIndex = parent.get().getChildIndex(this).orElseThrow(); + parent.get().removeChild(this); + // TODO improve group handling, probably needs slicing + for (Group group : getChildrenGroups()) { + parent.get().mutate().addCardinalityGroup(group.getLowerBound(), group.getUpperBound()); + } + for (IFeatureTree child : getChildren()) { + parent.get().mutate().addChild(childIndex++, child); + } + } + } + + void setParentGroupID(int groupID); + + void setFeatureCardinality(Range featureRange); + + void makeMandatory(); + + void makeOptional(); + + int addCardinalityGroup(int lowerBound, int upperBound); + + default int addCardinalityGroup(Range groupRange) { + return addCardinalityGroup(groupRange.getLowerBound(), groupRange.getUpperBound()); + } + + default int addAndGroup() { + return addCardinalityGroup(0, Range.OPEN); + } + + default int addAlternativeGroup() { + return addCardinalityGroup(1, 1); + } + + default int addOrGroup() { + return addCardinalityGroup(1, Range.OPEN); + } + + /** + * Changes the cardinality of the children group with the given id. + * + * @param groupID the id of the group to change + * @param lowerBound the new lower bound + * @param upperBound the new upper bound + */ + void toCardinalityGroup(int groupID, int lowerBound, int upperBound); + + /** + * Changes the cardinality of the children group with the given id. + * + * @param groupID the id of the group to change + * @param groupCardinality the new cardinality + */ + default void toCardinalityGroup(int groupID, Range groupCardinality) { + toCardinalityGroup(groupID, groupCardinality.getLowerBound(), groupCardinality.getUpperBound()); + } + + /** + * Change children group to and group. + * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(groupID, 0, Range.OPEN)}. + * + * @param groupID the id of the group to change + */ + default void toAndGroup(int groupID) { + toCardinalityGroup(groupID, 0, Range.OPEN); + } + + /** + * Change children group to or group. + * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(groupID, 1, Range.OPEN)}. + * + * @param groupID the id of the group to change + */ + default void toOrGroup(int groupID) { + toCardinalityGroup(groupID, 1, Range.OPEN); + } + + /** + * Change children group to alternative group. + * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(groupID, 1, 1)}. + * + * @param groupID the id of the group to change + */ + default void toAlternativeGroup(int groupID) { + toCardinalityGroup(groupID, 1, 1); + } + + /** + * Changes the cardinality of the first children group. + * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(0, groupCardinality)}. + * + * @param groupCardinality the new cardinality + */ + default void toCardinalityGroup(Range groupCardinality) { + toCardinalityGroup(0, groupCardinality); + } + + /** + * Change first children group to cardinality group. + * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(0, lowerBound, upperBound)}. + * + * @param lowerBound the new lower bound + * @param upperBound the new upper bound + */ + default void toCardinalityGroup(int lowerBound, int upperBound) { + toCardinalityGroup(0, lowerBound, upperBound); + } + + /** + * Change first children group to and group. + * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(0, 0, Range.OPEN)}. + * + */ + default void toAndGroup() { + toAndGroup(0); + } + + /** + * Change first children group to alternative group. + * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(0, 1, Range.OPEN)}. + */ + default void toAlternativeGroup() { + toAlternativeGroup(0); + } + + /** + * Change first children group to or group. + * Equivalent to calling {@link #toCardinalityGroup(int, int, int) toCardinalityGroup(0, 1, 1)}. + */ + default void toOrGroup() { + toOrGroup(0); + } + } +} diff --git a/src/main/java/de/featjar/feature/model/io/AttributeIO.java b/src/main/java/de/featjar/feature/model/io/AttributeIO.java index 3a78c046..17305854 100644 --- a/src/main/java/de/featjar/feature/model/io/AttributeIO.java +++ b/src/main/java/de/featjar/feature/model/io/AttributeIO.java @@ -1,118 +1,118 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.io; - -import de.featjar.base.data.*; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -/** - * Helpers for parsing and writing attributes and attribute values. - * - * @author Elias Kuiter - */ -public class AttributeIO { - public static Result> getType(String typeString) { - switch (typeString.toLowerCase(Locale.ENGLISH)) { - case "string": - return Result.of(String.class); - case "bool": - case "boolean": - return Result.of(Boolean.class); - case "int": - case "integer": - return Result.of(Integer.class); - case "long": - return Result.of(Long.class); - case "float": - return Result.of(Float.class); - case "double": - return Result.of(Double.class); - default: - return Result.empty(); - } - } - - public static Result getTypeString(Class type) { - if (String.class.equals(type)) { - return Result.of("string"); - } else if (Boolean.class.equals(type)) { - return Result.of("boolean"); - } else if (Integer.class.equals(type)) { - return Result.of("integer"); - } else if (Long.class.equals(type)) { - return Result.of("long"); - } else if (Float.class.equals(type)) { - return Result.of("float"); - } else if (Double.class.equals(type)) { - return Result.of("double"); - } - return Result.empty(); - } - - public static Result> parseAttribute(String name, String typeString) { - return getType(typeString).map(type -> new Attribute<>(name, type)); - } - - public static Result> parseAttribute(String namespace, String name, String typeString) { - return getType(typeString).map(type -> new Attribute<>(namespace, name, type)); - } - - public static Result parseAttributeValue(Class type, String valueString) { - if (String.class.equals(type)) { - return Result.of(valueString); - } else if (Boolean.class.equals(type)) { - return Result.of(Boolean.valueOf(valueString)); - } else if (Integer.class.equals(type)) { - return Result.of(Integer.valueOf(valueString)); - } else if (Long.class.equals(type)) { - return Result.of(Long.valueOf(valueString)); - } else if (Float.class.equals(type)) { - return Result.of(Float.valueOf(valueString)); - } else if (Double.class.equals(type)) { - return Result.of(Double.valueOf(valueString)); - } - return Result.empty(); - } - - public static Result parseAttributeValue(String typeString, String valueString) { - return getType(typeString).flatMap(type -> parseAttributeValue(type, valueString)); - } - - @SuppressWarnings("unchecked") - public static List parseAndSetAttributeValue( - IAttributable attributable, String namespace, String name, String typeString, String valueString) { - List problems = new ArrayList<>(); - Result> attribute = AttributeIO.parseAttribute(namespace, name, typeString); - Result value = parseAttributeValue(typeString, valueString); - if (attribute.isEmpty()) { - problems.add(new Problem("invalid type for attribute " + name, Problem.Severity.WARNING)); - } else if (value.isEmpty()) { - problems.add(new Problem("invalid value for attribute " + name, Problem.Severity.WARNING)); - } else if (attributable.hasAttributeValue(attribute.get())) { - problems.add(new Problem("already has value for attribute " + name, Problem.Severity.WARNING)); - } else { - attributable.mutate().setAttributeValue((Attribute) attribute.get(), value.get()); - } - return problems; - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.io; + +import de.featjar.base.data.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * Helpers for parsing and writing attributes and attribute values. + * + * @author Elias Kuiter + */ +public class AttributeIO { + public static Result> getType(String typeString) { + switch (typeString.toLowerCase(Locale.ENGLISH)) { + case "string": + return Result.of(String.class); + case "bool": + case "boolean": + return Result.of(Boolean.class); + case "int": + case "integer": + return Result.of(Integer.class); + case "long": + return Result.of(Long.class); + case "float": + return Result.of(Float.class); + case "double": + return Result.of(Double.class); + default: + return Result.empty(); + } + } + + public static Result getTypeString(Class type) { + if (String.class.equals(type)) { + return Result.of("string"); + } else if (Boolean.class.equals(type)) { + return Result.of("boolean"); + } else if (Integer.class.equals(type)) { + return Result.of("integer"); + } else if (Long.class.equals(type)) { + return Result.of("long"); + } else if (Float.class.equals(type)) { + return Result.of("float"); + } else if (Double.class.equals(type)) { + return Result.of("double"); + } + return Result.empty(); + } + + public static Result> parseAttribute(String name, String typeString) { + return getType(typeString).map(type -> new Attribute<>(name, type)); + } + + public static Result> parseAttribute(String namespace, String name, String typeString) { + return getType(typeString).map(type -> new Attribute<>(namespace, name, type)); + } + + public static Result parseAttributeValue(Class type, String valueString) { + if (String.class.equals(type)) { + return Result.of(valueString); + } else if (Boolean.class.equals(type)) { + return Result.of(Boolean.valueOf(valueString)); + } else if (Integer.class.equals(type)) { + return Result.of(Integer.valueOf(valueString)); + } else if (Long.class.equals(type)) { + return Result.of(Long.valueOf(valueString)); + } else if (Float.class.equals(type)) { + return Result.of(Float.valueOf(valueString)); + } else if (Double.class.equals(type)) { + return Result.of(Double.valueOf(valueString)); + } + return Result.empty(); + } + + public static Result parseAttributeValue(String typeString, String valueString) { + return getType(typeString).flatMap(type -> parseAttributeValue(type, valueString)); + } + + @SuppressWarnings("unchecked") + public static List parseAndSetAttributeValue( + IAttributable attributable, String namespace, String name, String typeString, String valueString) { + List problems = new ArrayList<>(); + Result> attribute = AttributeIO.parseAttribute(namespace, name, typeString); + Result value = parseAttributeValue(typeString, valueString); + if (attribute.isEmpty()) { + problems.add(new Problem("invalid type for attribute " + name, Problem.Severity.WARNING)); + } else if (value.isEmpty()) { + problems.add(new Problem("invalid value for attribute " + name, Problem.Severity.WARNING)); + } else if (attributable.hasAttributeValue(attribute.get())) { + problems.add(new Problem("already has value for attribute " + name, Problem.Severity.WARNING)); + } else { + attributable.mutate().setAttributeValue((Attribute) attribute.get(), value.get()); + } + return problems; + } +} diff --git a/src/main/java/de/featjar/feature/model/io/FeatureModelFormats.java b/src/main/java/de/featjar/feature/model/io/FeatureModelFormats.java index 57088528..90f651d5 100644 --- a/src/main/java/de/featjar/feature/model/io/FeatureModelFormats.java +++ b/src/main/java/de/featjar/feature/model/io/FeatureModelFormats.java @@ -1,37 +1,37 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.io; - -import de.featjar.base.FeatJAR; -import de.featjar.base.io.format.AFormats; -import de.featjar.feature.model.IFeatureModel; - -/** - * Manages all formats for {@link IFeatureModel feature models}. - * - * @author Sebastian Krieter - */ -public class FeatureModelFormats extends AFormats { - - public static FeatureModelFormats getInstance() { - return FeatJAR.extensionPoint(FeatureModelFormats.class); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.io; + +import de.featjar.base.FeatJAR; +import de.featjar.base.io.format.AFormats; +import de.featjar.feature.model.IFeatureModel; + +/** + * Manages all formats for {@link IFeatureModel feature models}. + * + * @author Sebastian Krieter + */ +public class FeatureModelFormats extends AFormats { + + public static FeatureModelFormats getInstance() { + return FeatJAR.extensionPoint(FeatureModelFormats.class); + } +} diff --git a/src/main/java/de/featjar/feature/model/io/xml/GraphVizFeatureModelFormat.java b/src/main/java/de/featjar/feature/model/io/xml/GraphVizFeatureModelFormat.java index 8314748a..8675aa1c 100644 --- a/src/main/java/de/featjar/feature/model/io/xml/GraphVizFeatureModelFormat.java +++ b/src/main/java/de/featjar/feature/model/io/xml/GraphVizFeatureModelFormat.java @@ -1,145 +1,145 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.io.xml; - -import de.featjar.base.data.Result; -import de.featjar.base.io.format.IFormat; -import de.featjar.feature.model.IFeatureModel; -import de.featjar.feature.model.IFeatureTree; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -/** - * Writes feature models to GraphViz DOT files. - * - * @author Elias Kuiter - */ -public class GraphVizFeatureModelFormat implements IFormat { - @Override - public String getFileExtension() { - return "dot"; - } - - @Override - public String getName() { - return "GraphViz"; - } - - @Override - public boolean supportsWrite() { - return true; - } - - @Override - public Result serialize(IFeatureModel featureModel) { - // TODO take multiple roots into account - List features = featureModel.getFeatureTreeStream().collect(Collectors.toList()); - return Result.of(String.format( - "digraph {%n graph%s;%n node%s;%n edge%s;%n%s%n%s%n}", - options(option("splines", "false"), option("ranksep", "0.2")), - options( - option("fontname", "Arial"), - option("style", "filled"), - option("fillcolor", "#ccccff"), - option("shape", "box")), - options(option("arrowhead", "none")), - features.stream().map(this::getNode).collect(Collectors.joining("\n")), - features.stream().map(this::getEdge).filter(s -> !s.isEmpty()).collect(Collectors.joining("\n")))); - } - - public String getNode(IFeatureTree feature) { - String nodeString = ""; - nodeString += String.format( - " %s%s;", - quote(feature.getFeature().getIdentifier().toString()), - options( - option("label", feature.getFeature().getName().orElse("")), - option("fillcolor", feature.getFeature().isAbstract() ? "#f2f2ff" : null))); - if (feature.hasParent()) { - nodeString += String.format( - "%n %s%s;", - quote(feature.getFeature().getIdentifier().toString() + "_group"), - options( - option("shape", "diamond"), - option( - "style", - !feature.getParentGroup().get().isAnd() - ? "invis" - : feature.getParentGroup().get().isAlternative() ? "" : null), - option("fillcolor", feature.getParentGroup().get().isOr() ? "#000000" : null), - option("label", ""), - option("width", ".15"), - option("height", ".15"))); - } - return nodeString; - } - - public String getEdge(IFeatureTree feature) { - String edgeString = ""; - if (feature.hasParent()) { - String parentNode = - feature.getParent().get().getFeature().getIdentifier().toString(); - edgeString += getEdge( - parentNode + "_group", - feature, - option("style", feature.getParentGroup().get().isAnd() ? null : "invis")); - if (!feature.getParentGroup().get().isAnd()) - edgeString += - getEdge(parentNode + (feature.getParentGroup().get().isAnd() ? "_group" : ""), feature, ""); - edgeString += String.format( - " %s:s -> %s:n%s;", - quote(feature.getFeature().getIdentifier().toString()), - quote(feature.getFeature().getIdentifier().toString() + "_group"), - options(option("style", feature.getParentGroup().get().isAnd() ? null : "invis"))); - } - return edgeString; - } - - public String getEdge(String parentNode, IFeatureTree childFeature, String option) { - return String.format( - " %s:s -> %s:n%s;%n", - quote(parentNode), - quote(childFeature.getFeature().getIdentifier().toString()), - options( - option( - "arrowhead", - childFeature.getParentGroup().get().isAnd() - ? null - : childFeature.isMandatory() ? "dot" : "odot"), - option)); - } - - protected String quote(String str) { - return String.format("\"%s\"", str.replace("\"", "\\\"")); - } - - protected String options(String... options) { - List optionsList = - Arrays.stream(options).filter(o -> !o.isEmpty()).collect(Collectors.toList()); - if (String.join("", optionsList).trim().isEmpty()) return ""; - return String.format(" [%s]", String.join(" ", optionsList)); - } - - protected String option(String name, String value) { - return value != null ? String.format("%s=%s", name, quote(value)) : ""; - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.io.xml; + +import de.featjar.base.data.Result; +import de.featjar.base.io.format.IFormat; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.IFeatureTree; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Writes feature models to GraphViz DOT files. + * + * @author Elias Kuiter + */ +public class GraphVizFeatureModelFormat implements IFormat { + @Override + public String getFileExtension() { + return "dot"; + } + + @Override + public String getName() { + return "GraphViz"; + } + + @Override + public boolean supportsWrite() { + return true; + } + + @Override + public Result serialize(IFeatureModel featureModel) { + // TODO take multiple roots into account + List features = featureModel.getFeatureTreeStream().collect(Collectors.toList()); + return Result.of(String.format( + "digraph {%n graph%s;%n node%s;%n edge%s;%n%s%n%s%n}", + options(option("splines", "false"), option("ranksep", "0.2")), + options( + option("fontname", "Arial"), + option("style", "filled"), + option("fillcolor", "#ccccff"), + option("shape", "box")), + options(option("arrowhead", "none")), + features.stream().map(this::getNode).collect(Collectors.joining("\n")), + features.stream().map(this::getEdge).filter(s -> !s.isEmpty()).collect(Collectors.joining("\n")))); + } + + public String getNode(IFeatureTree feature) { + String nodeString = ""; + nodeString += String.format( + " %s%s;", + quote(feature.getFeature().getIdentifier().toString()), + options( + option("label", feature.getFeature().getName().orElse("")), + option("fillcolor", feature.getFeature().isAbstract() ? "#f2f2ff" : null))); + if (feature.hasParent()) { + nodeString += String.format( + "%n %s%s;", + quote(feature.getFeature().getIdentifier().toString() + "_group"), + options( + option("shape", "diamond"), + option( + "style", + !feature.getParentGroup().get().isAnd() + ? "invis" + : feature.getParentGroup().get().isAlternative() ? "" : null), + option("fillcolor", feature.getParentGroup().get().isOr() ? "#000000" : null), + option("label", ""), + option("width", ".15"), + option("height", ".15"))); + } + return nodeString; + } + + public String getEdge(IFeatureTree feature) { + String edgeString = ""; + if (feature.hasParent()) { + String parentNode = + feature.getParent().get().getFeature().getIdentifier().toString(); + edgeString += getEdge( + parentNode + "_group", + feature, + option("style", feature.getParentGroup().get().isAnd() ? null : "invis")); + if (!feature.getParentGroup().get().isAnd()) + edgeString += + getEdge(parentNode + (feature.getParentGroup().get().isAnd() ? "_group" : ""), feature, ""); + edgeString += String.format( + " %s:s -> %s:n%s;", + quote(feature.getFeature().getIdentifier().toString()), + quote(feature.getFeature().getIdentifier().toString() + "_group"), + options(option("style", feature.getParentGroup().get().isAnd() ? null : "invis"))); + } + return edgeString; + } + + public String getEdge(String parentNode, IFeatureTree childFeature, String option) { + return String.format( + " %s:s -> %s:n%s;%n", + quote(parentNode), + quote(childFeature.getFeature().getIdentifier().toString()), + options( + option( + "arrowhead", + childFeature.getParentGroup().get().isAnd() + ? null + : childFeature.isMandatory() ? "dot" : "odot"), + option)); + } + + protected String quote(String str) { + return String.format("\"%s\"", str.replace("\"", "\\\"")); + } + + protected String options(String... options) { + List optionsList = + Arrays.stream(options).filter(o -> !o.isEmpty()).collect(Collectors.toList()); + if (String.join("", optionsList).trim().isEmpty()) return ""; + return String.format(" [%s]", String.join(" ", optionsList)); + } + + protected String option(String name, String value) { + return value != null ? String.format("%s=%s", name, quote(value)) : ""; + } +} diff --git a/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelFormat.java b/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelFormat.java index e858cdf1..acf220b9 100644 --- a/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelFormat.java +++ b/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelFormat.java @@ -1,75 +1,75 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.io.xml; - -import de.featjar.base.data.Result; -import de.featjar.base.io.format.IFormat; -import de.featjar.base.io.input.AInputMapper; -import de.featjar.base.io.input.InputHeader; -import de.featjar.feature.model.IFeatureModel; -import de.featjar.formula.io.xml.AXMLFeatureModelParser; - -/** - * Parses and writes feature models from and to FeatureIDE XML files. - * - * @author Sebastian Krieter - * @author Elias Kuiter - */ -public class XMLFeatureModelFormat implements IFormat { - - @Override - public String getName() { - return "FeatureIDE"; - } - - @Override - public String getFileExtension() { - return "xml"; - } - - @Override - public boolean supportsParse() { - return true; - } - - @Override - public boolean supportsWrite() { - return true; - } - - @Override - public boolean supportsContent(InputHeader inputHeader) { - return supportsParse() - && AXMLFeatureModelParser.inputHeaderPattern - .matcher(inputHeader.get()) - .find(); - } - - @Override - public Result parse(AInputMapper inputMapper) { - return new XMLFeatureModelParser().parse(inputMapper); - } - - @Override - public Result serialize(IFeatureModel object) { - return new XMLFeatureModelWriter().serialize(object); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.io.xml; + +import de.featjar.base.data.Result; +import de.featjar.base.io.format.IFormat; +import de.featjar.base.io.input.AInputMapper; +import de.featjar.base.io.input.InputHeader; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.formula.io.xml.AXMLFeatureModelParser; + +/** + * Parses and writes feature models from and to FeatureIDE XML files. + * + * @author Sebastian Krieter + * @author Elias Kuiter + */ +public class XMLFeatureModelFormat implements IFormat { + + @Override + public String getName() { + return "FeatureIDE"; + } + + @Override + public String getFileExtension() { + return "xml"; + } + + @Override + public boolean supportsParse() { + return true; + } + + @Override + public boolean supportsWrite() { + return true; + } + + @Override + public boolean supportsContent(InputHeader inputHeader) { + return supportsParse() + && AXMLFeatureModelParser.inputHeaderPattern + .matcher(inputHeader.get()) + .find(); + } + + @Override + public Result parse(AInputMapper inputMapper) { + return new XMLFeatureModelParser().parse(inputMapper); + } + + @Override + public Result serialize(IFeatureModel object) { + return new XMLFeatureModelWriter().serialize(object); + } +} diff --git a/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelParser.java b/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelParser.java index 621271bc..363732cd 100644 --- a/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelParser.java +++ b/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelParser.java @@ -1,229 +1,229 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.io.xml; - -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.COMMENT; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.COMMENTS; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.CONSTRAINTS; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.DATA_TYPE; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.DESCRIPTION; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.EXT_FEATURE_MODEL; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.FEATURE_MODEL; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.KEY; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.NAMESPACE_TAG; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.PROPERTIES; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.PROPERTY; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.STRUCT; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.TAGS; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.VALUE; - -import de.featjar.base.FeatJAR; -import de.featjar.base.data.Attribute; -import de.featjar.base.data.Problem; -import de.featjar.base.data.Result; -import de.featjar.base.data.Sets; -import de.featjar.base.data.identifier.Identifiers; -import de.featjar.base.io.format.ParseException; -import de.featjar.feature.model.FeatureModel; -import de.featjar.feature.model.IConstraint; -import de.featjar.feature.model.IFeature; -import de.featjar.feature.model.IFeatureModel; -import de.featjar.feature.model.IFeatureModelElement; -import de.featjar.feature.model.IFeatureTree; -import de.featjar.feature.model.io.AttributeIO; -import de.featjar.formula.io.xml.AXMLFeatureModelParser; -import de.featjar.formula.structure.IFormula; -import java.util.Arrays; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -/** - * Parses and writes feature models from and to FeatureIDE XML files. - * - * @author Sebastian Krieter - * @author Elias Kuiter - */ -public class XMLFeatureModelParser extends AXMLFeatureModelParser { - - private IFeatureModel featureModel; - private HashSet featureNames; - - @Override - public IFeatureModel parseDocument(Document document) throws ParseException { - featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); - featureNames = Sets.empty(); - final Element featureModelElement = getDocumentElement(document, FEATURE_MODEL, EXT_FEATURE_MODEL); - - parseFeatureTree(getElement(featureModelElement, STRUCT)); - - Result element = getElementResult(featureModelElement, CONSTRAINTS); - if (element.isPresent()) { - parseConstraints(element.get()); - } - - element = getElementResult(featureModelElement, COMMENTS); - if (element.isPresent()) { - parseComments(element.get()); - } - - element = getElementResult(featureModelElement, PROPERTIES); - if (element.isPresent()) { - parseFeatureModelProperties(element.get()); - } - - return featureModel; - } - - @Override - protected IFeatureTree newFeature( - String name, IFeatureTree parentFeature, boolean mandatory, boolean _abstract, boolean hidden) - throws ParseException { - if (!featureNames.add(name)) { - throw new ParseException("Duplicate feature name!"); - } - IFeature feature = featureModel.mutate().addFeature(name); - IFeatureTree featureTree; - if (parentFeature == null) { - featureTree = featureModel.mutate().addFeatureTreeRoot(feature); - } else { - featureTree = parentFeature.mutate().addFeatureBelow(feature); - } - feature.mutate().setAbstract(_abstract); - feature.mutate().setHidden(hidden); - if (mandatory || parentFeature == null) { - featureTree.mutate().makeMandatory(); - } else { - featureTree.mutate().makeOptional(); - } - return featureTree; - } - - @Override - protected void addAndGroup(IFeatureTree featureTree, List children) { - featureTree.mutate().toAndGroup(); - } - - @Override - protected void addOrGroup(IFeatureTree featureTree, List children) { - featureTree.mutate().toOrGroup(); - } - - @Override - protected void addAlternativeGroup(IFeatureTree featureTree, List children) { - featureTree.mutate().toAlternativeGroup(); - } - - @Override - protected void addFeatureMetadata(IFeatureTree feature, Element e) throws ParseException { - String nodeName = e.getNodeName(); - switch (nodeName) { - case DESCRIPTION: - feature.getFeature().mutate().setDescription(getDescription(e)); - break; - case PROPERTY: - parseProperty(feature.getFeature(), e); - break; - default: - FeatJAR.log().warning("Unkown node name %s", nodeName); - } - } - - @Override - protected IConstraint newConstraint(IFormula formula) { - return featureModel.mutate().addConstraint(formula); - } - - @Override - protected void addConstraintMetadata(IConstraint constraint, Element e) throws ParseException { - String nodeName = e.getNodeName(); - switch (nodeName) { - case DESCRIPTION: - constraint.mutate().setDescription(getDescription(e)); - break; - case PROPERTY: - parseProperty(constraint, e); - break; - case TAGS: - constraint.mutate().setTags(getTags(e)); - break; - default: - FeatJAR.log().warning("Unkown node name %s", nodeName); - } - } - - protected void parseComments(Element element) throws ParseException { - for (final Element e1 : getElements(element.getChildNodes())) { - if (e1.getNodeName().equals(COMMENT)) { - featureModel - .mutate() - .setDescription(featureModel.getDescription().orElse("") + "\n" + e1.getTextContent()); - } else { - addParseProblem("Unknown comment attribute: " + e1.getNodeName(), e1, Problem.Severity.WARNING); - } - } - } - - protected static String getDescription(Node e) { - String description = e.getTextContent(); - // NOTE: The following code is used for backwards compatibility. It replaces - // spaces and tabs that were added to the XML for indentation, but don't - // belong to the actual description. - return description == null - ? null - : description.replaceAll("(\r\n|\r|\n)\\s*", "\n").replaceAll("\\A\n|\n\\Z", ""); - } - - protected static LinkedHashSet getTags(final Node e) { - return new LinkedHashSet<>(Arrays.asList(e.getTextContent().split(","))); - } - - protected void parseProperty(IFeatureModelElement featureModelElement, Element e) throws ParseException { - if (!e.hasAttribute(KEY) || !e.hasAttribute(VALUE)) { - addParseProblem( - "Missing one of the required attributes: " + KEY + " or " + VALUE, e, Problem.Severity.WARNING); - } else { - String typeString = e.hasAttribute(DATA_TYPE) ? e.getAttribute(DATA_TYPE) : "string"; - final String namespace = - e.hasAttribute(NAMESPACE_TAG) ? e.getAttribute(NAMESPACE_TAG) : Attribute.DEFAULT_NAMESPACE; - final String name = e.getAttribute(KEY); - final String valueString = e.getAttribute(VALUE); - parseProblems.addAll(AttributeIO.parseAndSetAttributeValue( - featureModelElement, namespace, name, typeString, valueString)); - } - } - - protected void parseFeatureModelProperties(Element e) throws ParseException { - for (final Element propertyElement : getElements(e.getChildNodes())) { - final String nodeName = propertyElement.getNodeName(); - switch (nodeName) { - case PROPERTY: - parseProperty(featureModel, propertyElement); - break; - default: - FeatJAR.log().warning("Unkown node name %s", nodeName); - } - } - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.io.xml; + +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.COMMENT; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.COMMENTS; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.CONSTRAINTS; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.DATA_TYPE; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.DESCRIPTION; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.EXT_FEATURE_MODEL; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.FEATURE_MODEL; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.KEY; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.NAMESPACE_TAG; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.PROPERTIES; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.PROPERTY; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.STRUCT; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.TAGS; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.VALUE; + +import de.featjar.base.FeatJAR; +import de.featjar.base.data.Attribute; +import de.featjar.base.data.Problem; +import de.featjar.base.data.Result; +import de.featjar.base.data.Sets; +import de.featjar.base.data.identifier.Identifiers; +import de.featjar.base.io.format.ParseException; +import de.featjar.feature.model.FeatureModel; +import de.featjar.feature.model.IConstraint; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.IFeatureModelElement; +import de.featjar.feature.model.IFeatureTree; +import de.featjar.feature.model.io.AttributeIO; +import de.featjar.formula.io.xml.AXMLFeatureModelParser; +import de.featjar.formula.structure.IFormula; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * Parses and writes feature models from and to FeatureIDE XML files. + * + * @author Sebastian Krieter + * @author Elias Kuiter + */ +public class XMLFeatureModelParser extends AXMLFeatureModelParser { + + private IFeatureModel featureModel; + private HashSet featureNames; + + @Override + public IFeatureModel parseDocument(Document document) throws ParseException { + featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + featureNames = Sets.empty(); + final Element featureModelElement = getDocumentElement(document, FEATURE_MODEL, EXT_FEATURE_MODEL); + + parseFeatureTree(getElement(featureModelElement, STRUCT)); + + Result element = getElementResult(featureModelElement, CONSTRAINTS); + if (element.isPresent()) { + parseConstraints(element.get()); + } + + element = getElementResult(featureModelElement, COMMENTS); + if (element.isPresent()) { + parseComments(element.get()); + } + + element = getElementResult(featureModelElement, PROPERTIES); + if (element.isPresent()) { + parseFeatureModelProperties(element.get()); + } + + return featureModel; + } + + @Override + protected IFeatureTree newFeature( + String name, IFeatureTree parentFeature, boolean mandatory, boolean _abstract, boolean hidden) + throws ParseException { + if (!featureNames.add(name)) { + throw new ParseException("Duplicate feature name!"); + } + IFeature feature = featureModel.mutate().addFeature(name); + IFeatureTree featureTree; + if (parentFeature == null) { + featureTree = featureModel.mutate().addFeatureTreeRoot(feature); + } else { + featureTree = parentFeature.mutate().addFeatureBelow(feature); + } + feature.mutate().setAbstract(_abstract); + feature.mutate().setHidden(hidden); + if (mandatory || parentFeature == null) { + featureTree.mutate().makeMandatory(); + } else { + featureTree.mutate().makeOptional(); + } + return featureTree; + } + + @Override + protected void addAndGroup(IFeatureTree featureTree, List children) { + featureTree.mutate().toAndGroup(); + } + + @Override + protected void addOrGroup(IFeatureTree featureTree, List children) { + featureTree.mutate().toOrGroup(); + } + + @Override + protected void addAlternativeGroup(IFeatureTree featureTree, List children) { + featureTree.mutate().toAlternativeGroup(); + } + + @Override + protected void addFeatureMetadata(IFeatureTree feature, Element e) throws ParseException { + String nodeName = e.getNodeName(); + switch (nodeName) { + case DESCRIPTION: + feature.getFeature().mutate().setDescription(getDescription(e)); + break; + case PROPERTY: + parseProperty(feature.getFeature(), e); + break; + default: + FeatJAR.log().warning("Unkown node name %s", nodeName); + } + } + + @Override + protected IConstraint newConstraint(IFormula formula) { + return featureModel.mutate().addConstraint(formula); + } + + @Override + protected void addConstraintMetadata(IConstraint constraint, Element e) throws ParseException { + String nodeName = e.getNodeName(); + switch (nodeName) { + case DESCRIPTION: + constraint.mutate().setDescription(getDescription(e)); + break; + case PROPERTY: + parseProperty(constraint, e); + break; + case TAGS: + constraint.mutate().setTags(getTags(e)); + break; + default: + FeatJAR.log().warning("Unkown node name %s", nodeName); + } + } + + protected void parseComments(Element element) throws ParseException { + for (final Element e1 : getElements(element.getChildNodes())) { + if (e1.getNodeName().equals(COMMENT)) { + featureModel + .mutate() + .setDescription(featureModel.getDescription().orElse("") + "\n" + e1.getTextContent()); + } else { + addParseProblem("Unknown comment attribute: " + e1.getNodeName(), e1, Problem.Severity.WARNING); + } + } + } + + protected static String getDescription(Node e) { + String description = e.getTextContent(); + // NOTE: The following code is used for backwards compatibility. It replaces + // spaces and tabs that were added to the XML for indentation, but don't + // belong to the actual description. + return description == null + ? null + : description.replaceAll("(\r\n|\r|\n)\\s*", "\n").replaceAll("\\A\n|\n\\Z", ""); + } + + protected static LinkedHashSet getTags(final Node e) { + return new LinkedHashSet<>(Arrays.asList(e.getTextContent().split(","))); + } + + protected void parseProperty(IFeatureModelElement featureModelElement, Element e) throws ParseException { + if (!e.hasAttribute(KEY) || !e.hasAttribute(VALUE)) { + addParseProblem( + "Missing one of the required attributes: " + KEY + " or " + VALUE, e, Problem.Severity.WARNING); + } else { + String typeString = e.hasAttribute(DATA_TYPE) ? e.getAttribute(DATA_TYPE) : "string"; + final String namespace = + e.hasAttribute(NAMESPACE_TAG) ? e.getAttribute(NAMESPACE_TAG) : Attribute.DEFAULT_NAMESPACE; + final String name = e.getAttribute(KEY); + final String valueString = e.getAttribute(VALUE); + parseProblems.addAll(AttributeIO.parseAndSetAttributeValue( + featureModelElement, namespace, name, typeString, valueString)); + } + } + + protected void parseFeatureModelProperties(Element e) throws ParseException { + for (final Element propertyElement : getElements(e.getChildNodes())) { + final String nodeName = propertyElement.getNodeName(); + switch (nodeName) { + case PROPERTY: + parseProperty(featureModel, propertyElement); + break; + default: + FeatJAR.log().warning("Unkown node name %s", nodeName); + } + } + } +} diff --git a/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelWriter.java b/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelWriter.java index 62c5d2fb..0ac1e9d7 100644 --- a/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelWriter.java +++ b/src/main/java/de/featjar/feature/model/io/xml/XMLFeatureModelWriter.java @@ -1,267 +1,267 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.io.xml; - -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.ABSTRACT; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.ALT; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.AND; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.ATMOST1; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.CONJ; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.CONSTRAINTS; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.DATA_TYPE; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.DESCRIPTION; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.DISJ; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.EQ; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.FEATURE; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.FEATURE_MODEL; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.HIDDEN; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.IMP; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.KEY; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.MANDATORY; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.NAME; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.NAMESPACE_TAG; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.NOT; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.OR; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.PROPERTY; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.RULE; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.STRUCT; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.TAGS; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.TRUE; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.VALUE; -import static de.featjar.formula.io.xml.XMLFeatureModelConstants.VAR; - -import de.featjar.base.FeatJAR; -import de.featjar.base.data.IAttribute; -import de.featjar.base.io.xml.AXMLWriter; -import de.featjar.feature.model.IConstraint; -import de.featjar.feature.model.IFeatureModel; -import de.featjar.feature.model.IFeatureTree; -import de.featjar.feature.model.io.AttributeIO; -import de.featjar.formula.structure.IExpression; -import de.featjar.formula.structure.IFormula; -import de.featjar.formula.structure.connective.And; -import de.featjar.formula.structure.connective.AtMost; -import de.featjar.formula.structure.connective.BiImplies; -import de.featjar.formula.structure.connective.Implies; -import de.featjar.formula.structure.connective.Not; -import de.featjar.formula.structure.connective.Or; -import de.featjar.formula.structure.predicate.Literal; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -/** - * Parses and writes feature models from and to FeatureIDE XML files. - * - * @author Sebastian Krieter - * @author Elias Kuiter - */ -public class XMLFeatureModelWriter extends AXMLWriter { - - private IFeatureModel featureModel; - - @Override - public void writeDocument(IFeatureModel featureModel, Document doc) { - this.featureModel = featureModel; - final Element root = doc.createElement(FEATURE_MODEL); - doc.appendChild(root); - - writeFeatures(doc, root); - writeConstraints(doc, root); - } - - protected void writeFeatures(Document doc, final Element root) { - final Element struct = doc.createElement(STRUCT); - root.appendChild(struct); - writeFeatureTreeRec(doc, struct, featureModel.getRoots().get(0)); - } - - protected void writeConstraints(Document doc, final Element root) { - if (!featureModel.getConstraints().isEmpty()) { - final Element constraints = doc.createElement(CONSTRAINTS); - root.appendChild(constraints); - for (final IConstraint constraint : featureModel.getConstraints()) { - Element rule; - rule = doc.createElement(RULE); - - constraints.appendChild(rule); - addDescription(doc, constraint.getDescription().orElse(null), rule); - addProperties(doc, constraint.getAttributes().get(), rule); - addTags(doc, constraint.getTags(), rule); - createPropositionalConstraints(doc, rule, constraint.getFormula()); - } - } - } - - /** - * Inserts the tags concerning propositional constraints into the DOM document representation - * - * @param doc - * @param node Parent node for the propositional nodes - */ - protected void createPropositionalConstraints(Document doc, Element xmlNode, IFormula node) { - if (node == null) { - return; - } - - final Element op; - if (node instanceof Literal) { - final Literal literal = (Literal) node; - if (!literal.isPositive()) { - final Element opNot = doc.createElement(NOT); - xmlNode.appendChild(opNot); - xmlNode = opNot; - } - op = doc.createElement(VAR); - op.appendChild(doc.createTextNode(literal.getFirstChild().get().getName())); - xmlNode.appendChild(op); - return; - } else if (node instanceof Or) { - op = doc.createElement(DISJ); - } else if (node instanceof BiImplies) { - op = doc.createElement(EQ); - } else if (node instanceof Implies) { - op = doc.createElement(IMP); - } else if (node instanceof And) { - op = doc.createElement(CONJ); - } else if (node instanceof Not) { - op = doc.createElement(NOT); - } else if (node instanceof AtMost) { - op = doc.createElement(ATMOST1); - } else { - FeatJAR.log().error("Unsupported element %s", node); - return; - } - xmlNode.appendChild(op); - - for (final IExpression child : node.getChildren()) { - createPropositionalConstraints(doc, op, (IFormula) child); - } - } - - /** - * Creates document based on feature model step by step - * - * @param doc document to write - * @param node parent node - * @param feat current feature - */ - protected void writeFeatureTreeRec(Document doc, Element node, IFeatureTree feat) { - if (feat == null) { - return; - } - - final List children = feat.getChildren(); - - final Element fnod; - if (children.isEmpty()) { - fnod = doc.createElement(FEATURE); - writeFeatureProperties(doc, node, feat, fnod); - } else { - if (feat.getChildrenGroups().get(0).isAnd()) { - fnod = doc.createElement(AND); - } else if (feat.getChildrenGroups().get(0).isOr()) { - fnod = doc.createElement(OR); - } else if (feat.getChildrenGroups().get(0).isAlternative()) { - fnod = doc.createElement(ALT); - } else { - FeatJAR.log().error("Unkown group %s", feat.getParentGroup()); - return; - } - - writeFeatureProperties(doc, node, feat, fnod); - - for (final IFeatureTree feature : children) { - writeFeatureTreeRec(doc, fnod, feature); - } - } - } - - protected void writeFeatureProperties(Document doc, Element node, IFeatureTree feat, final Element fnod) { - addDescription(doc, feat.getFeature().getDescription().orElse(null), fnod); - if (feat.getAttributes().isPresent()) { - addProperties(doc, feat.getAttributes().get(), fnod); - } - writeAttributes(node, fnod, feat); - } - - protected void addDescription(Document doc, String description, Element fnod) { - if ((description != null) && !description.trim().isEmpty()) { - final Element descr = doc.createElement(DESCRIPTION); - descr.setTextContent(description); - fnod.appendChild(descr); - } - } - - protected void addProperties(Document doc, Map, Object> attributes, Element fnod) { - for (final Entry, Object> property : attributes.entrySet()) { - final Element propNode; - propNode = doc.createElement(PROPERTY); - propNode.setAttribute(NAMESPACE_TAG, property.getKey().getNamespace()); - propNode.setAttribute( - DATA_TYPE, - AttributeIO.getTypeString(property.getKey().getType()) - .orElseThrow(p -> new IllegalArgumentException())); - propNode.setAttribute(KEY, property.getKey().getName()); - propNode.setAttribute(VALUE, property.getValue().toString()); // TODO - fnod.appendChild(propNode); - } - } - - private void addTags(Document doc, Set tags, Element fnod) { - if ((tags != null) && !tags.isEmpty()) { - StringBuilder tagStrings = new StringBuilder(); - for (final String tagString : tags) { - tagStrings.append(tagString); - tagStrings.append(','); - } - if (tagStrings.length() > 0) { - tagStrings.deleteCharAt(tagStrings.length() - 1); - } - final Element tag = doc.createElement(TAGS); - tag.setTextContent(tagStrings.toString()); - fnod.appendChild(tag); - } - } - - protected void writeAttributes(Element node, Element fnod, IFeatureTree feat) { - fnod.setAttribute(NAME, feat.getFeature().getName().get()); - if (feat.getFeature().isHidden()) { - fnod.setAttribute(HIDDEN, TRUE); - } - if (feat.isMandatory() || feat.getParent().isEmpty()) { - if ((feat.getParent().isPresent()) - && feat.getParent().get().getChildrenGroups().get(0).isAnd()) { - fnod.setAttribute(MANDATORY, TRUE); - } else if (feat.getParent().isEmpty()) { - fnod.setAttribute(MANDATORY, TRUE); - } - } - if (feat.getFeature().isAbstract()) { - fnod.setAttribute(ABSTRACT, TRUE); - } - - node.appendChild(fnod); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.io.xml; + +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.ABSTRACT; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.ALT; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.AND; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.ATMOST1; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.CONJ; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.CONSTRAINTS; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.DATA_TYPE; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.DESCRIPTION; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.DISJ; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.EQ; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.FEATURE; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.FEATURE_MODEL; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.HIDDEN; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.IMP; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.KEY; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.MANDATORY; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.NAME; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.NAMESPACE_TAG; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.NOT; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.OR; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.PROPERTY; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.RULE; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.STRUCT; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.TAGS; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.TRUE; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.VALUE; +import static de.featjar.formula.io.xml.XMLFeatureModelConstants.VAR; + +import de.featjar.base.FeatJAR; +import de.featjar.base.data.IAttribute; +import de.featjar.base.io.xml.AXMLWriter; +import de.featjar.feature.model.IConstraint; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.IFeatureTree; +import de.featjar.feature.model.io.AttributeIO; +import de.featjar.formula.structure.IExpression; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.connective.And; +import de.featjar.formula.structure.connective.AtMost; +import de.featjar.formula.structure.connective.BiImplies; +import de.featjar.formula.structure.connective.Implies; +import de.featjar.formula.structure.connective.Not; +import de.featjar.formula.structure.connective.Or; +import de.featjar.formula.structure.predicate.Literal; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +/** + * Parses and writes feature models from and to FeatureIDE XML files. + * + * @author Sebastian Krieter + * @author Elias Kuiter + */ +public class XMLFeatureModelWriter extends AXMLWriter { + + private IFeatureModel featureModel; + + @Override + public void writeDocument(IFeatureModel featureModel, Document doc) { + this.featureModel = featureModel; + final Element root = doc.createElement(FEATURE_MODEL); + doc.appendChild(root); + + writeFeatures(doc, root); + writeConstraints(doc, root); + } + + protected void writeFeatures(Document doc, final Element root) { + final Element struct = doc.createElement(STRUCT); + root.appendChild(struct); + writeFeatureTreeRec(doc, struct, featureModel.getRoots().get(0)); + } + + protected void writeConstraints(Document doc, final Element root) { + if (!featureModel.getConstraints().isEmpty()) { + final Element constraints = doc.createElement(CONSTRAINTS); + root.appendChild(constraints); + for (final IConstraint constraint : featureModel.getConstraints()) { + Element rule; + rule = doc.createElement(RULE); + + constraints.appendChild(rule); + addDescription(doc, constraint.getDescription().orElse(null), rule); + addProperties(doc, constraint.getAttributes().get(), rule); + addTags(doc, constraint.getTags(), rule); + createPropositionalConstraints(doc, rule, constraint.getFormula()); + } + } + } + + /** + * Inserts the tags concerning propositional constraints into the DOM document representation + * + * @param doc + * @param node Parent node for the propositional nodes + */ + protected void createPropositionalConstraints(Document doc, Element xmlNode, IFormula node) { + if (node == null) { + return; + } + + final Element op; + if (node instanceof Literal) { + final Literal literal = (Literal) node; + if (!literal.isPositive()) { + final Element opNot = doc.createElement(NOT); + xmlNode.appendChild(opNot); + xmlNode = opNot; + } + op = doc.createElement(VAR); + op.appendChild(doc.createTextNode(literal.getFirstChild().get().getName())); + xmlNode.appendChild(op); + return; + } else if (node instanceof Or) { + op = doc.createElement(DISJ); + } else if (node instanceof BiImplies) { + op = doc.createElement(EQ); + } else if (node instanceof Implies) { + op = doc.createElement(IMP); + } else if (node instanceof And) { + op = doc.createElement(CONJ); + } else if (node instanceof Not) { + op = doc.createElement(NOT); + } else if (node instanceof AtMost) { + op = doc.createElement(ATMOST1); + } else { + FeatJAR.log().error("Unsupported element %s", node); + return; + } + xmlNode.appendChild(op); + + for (final IExpression child : node.getChildren()) { + createPropositionalConstraints(doc, op, (IFormula) child); + } + } + + /** + * Creates document based on feature model step by step + * + * @param doc document to write + * @param node parent node + * @param feat current feature + */ + protected void writeFeatureTreeRec(Document doc, Element node, IFeatureTree feat) { + if (feat == null) { + return; + } + + final List children = feat.getChildren(); + + final Element fnod; + if (children.isEmpty()) { + fnod = doc.createElement(FEATURE); + writeFeatureProperties(doc, node, feat, fnod); + } else { + if (feat.getChildrenGroups().get(0).isAnd()) { + fnod = doc.createElement(AND); + } else if (feat.getChildrenGroups().get(0).isOr()) { + fnod = doc.createElement(OR); + } else if (feat.getChildrenGroups().get(0).isAlternative()) { + fnod = doc.createElement(ALT); + } else { + FeatJAR.log().error("Unkown group %s", feat.getParentGroup()); + return; + } + + writeFeatureProperties(doc, node, feat, fnod); + + for (final IFeatureTree feature : children) { + writeFeatureTreeRec(doc, fnod, feature); + } + } + } + + protected void writeFeatureProperties(Document doc, Element node, IFeatureTree feat, final Element fnod) { + addDescription(doc, feat.getFeature().getDescription().orElse(null), fnod); + if (feat.getAttributes().isPresent()) { + addProperties(doc, feat.getAttributes().get(), fnod); + } + writeAttributes(node, fnod, feat); + } + + protected void addDescription(Document doc, String description, Element fnod) { + if ((description != null) && !description.trim().isEmpty()) { + final Element descr = doc.createElement(DESCRIPTION); + descr.setTextContent(description); + fnod.appendChild(descr); + } + } + + protected void addProperties(Document doc, Map, Object> attributes, Element fnod) { + for (final Entry, Object> property : attributes.entrySet()) { + final Element propNode; + propNode = doc.createElement(PROPERTY); + propNode.setAttribute(NAMESPACE_TAG, property.getKey().getNamespace()); + propNode.setAttribute( + DATA_TYPE, + AttributeIO.getTypeString(property.getKey().getType()) + .orElseThrow(p -> new IllegalArgumentException())); + propNode.setAttribute(KEY, property.getKey().getName()); + propNode.setAttribute(VALUE, property.getValue().toString()); // TODO + fnod.appendChild(propNode); + } + } + + private void addTags(Document doc, Set tags, Element fnod) { + if ((tags != null) && !tags.isEmpty()) { + StringBuilder tagStrings = new StringBuilder(); + for (final String tagString : tags) { + tagStrings.append(tagString); + tagStrings.append(','); + } + if (tagStrings.length() > 0) { + tagStrings.deleteCharAt(tagStrings.length() - 1); + } + final Element tag = doc.createElement(TAGS); + tag.setTextContent(tagStrings.toString()); + fnod.appendChild(tag); + } + } + + protected void writeAttributes(Element node, Element fnod, IFeatureTree feat) { + fnod.setAttribute(NAME, feat.getFeature().getName().get()); + if (feat.getFeature().isHidden()) { + fnod.setAttribute(HIDDEN, TRUE); + } + if (feat.isMandatory() || feat.getParent().isEmpty()) { + if ((feat.getParent().isPresent()) + && feat.getParent().get().getChildrenGroups().get(0).isAnd()) { + fnod.setAttribute(MANDATORY, TRUE); + } else if (feat.getParent().isEmpty()) { + fnod.setAttribute(MANDATORY, TRUE); + } + } + if (feat.getFeature().isAbstract()) { + fnod.setAttribute(ABSTRACT, TRUE); + } + + node.appendChild(fnod); + } +} diff --git a/src/main/java/de/featjar/feature/model/mixins/IHasCommonAttributes.java b/src/main/java/de/featjar/feature/model/mixins/IHasCommonAttributes.java index 8d99183f..7e847d92 100644 --- a/src/main/java/de/featjar/feature/model/mixins/IHasCommonAttributes.java +++ b/src/main/java/de/featjar/feature/model/mixins/IHasCommonAttributes.java @@ -1,49 +1,49 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.mixins; - -import de.featjar.base.data.Attribute; -import de.featjar.base.data.IAttributable; -import de.featjar.base.data.Result; -import de.featjar.feature.model.*; - -/** - * Implements accessors for commonly used {@link Attribute attributes}. - * That is, all {@link IFeatureModel feature models}, {@link IFeature features}, - * and {@link IConstraint constraints} can have names and descriptions. - * - * @author Elias Kuiter - */ -public interface IHasCommonAttributes extends IAttributable { - default Result getName() { - return getAttributeValue(Attributes.NAME); - } - - default Result getDescription() { - return getAttributeValue(Attributes.DESCRIPTION); - } - - public static interface IHasMutableCommonAttributes extends IMutatableAttributable { - void setName(String name); - - void setDescription(String description); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.mixins; + +import de.featjar.base.data.Attribute; +import de.featjar.base.data.IAttributable; +import de.featjar.base.data.Result; +import de.featjar.feature.model.*; + +/** + * Implements accessors for commonly used {@link Attribute attributes}. + * That is, all {@link IFeatureModel feature models}, {@link IFeature features}, + * and {@link IConstraint constraints} can have names and descriptions. + * + * @author Elias Kuiter + */ +public interface IHasCommonAttributes extends IAttributable { + default Result getName() { + return getAttributeValue(Attributes.NAME); + } + + default Result getDescription() { + return getAttributeValue(Attributes.DESCRIPTION); + } + + public static interface IHasMutableCommonAttributes extends IMutatableAttributable { + void setName(String name); + + void setDescription(String description); + } +} diff --git a/src/main/java/de/featjar/feature/model/mixins/IHasConstraints.java b/src/main/java/de/featjar/feature/model/mixins/IHasConstraints.java index d7365190..8703769d 100644 --- a/src/main/java/de/featjar/feature/model/mixins/IHasConstraints.java +++ b/src/main/java/de/featjar/feature/model/mixins/IHasConstraints.java @@ -1,56 +1,56 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.mixins; - -import de.featjar.base.data.Result; -import de.featjar.base.data.identifier.IIdentifier; -import de.featjar.feature.model.IConstraint; -import de.featjar.feature.model.IFeatureModel; -import java.util.Collection; -import java.util.Objects; - -/** - * Implements a {@link IFeatureModel} mixin for common operations on {@link IConstraint constraints}. - * - * @author Elias Kuiter - */ -public interface IHasConstraints { - Collection getConstraints(); - - default Result getConstraint(IIdentifier identifier) { - Objects.requireNonNull(identifier); - return Result.ofOptional(getConstraints().stream() - .filter(constraint -> constraint.getIdentifier().equals(identifier)) - .findFirst()); - } - - default boolean hasConstraint(IIdentifier identifier) { - return getConstraint(identifier).isPresent(); - } - - default boolean hasConstraint(IConstraint constraint) { - return hasConstraint(constraint.getIdentifier()); - } - - default int getNumberOfConstraints() { - return getConstraints().size(); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.mixins; + +import de.featjar.base.data.Result; +import de.featjar.base.data.identifier.IIdentifier; +import de.featjar.feature.model.IConstraint; +import de.featjar.feature.model.IFeatureModel; +import java.util.Collection; +import java.util.Objects; + +/** + * Implements a {@link IFeatureModel} mixin for common operations on {@link IConstraint constraints}. + * + * @author Elias Kuiter + */ +public interface IHasConstraints { + Collection getConstraints(); + + default Result getConstraint(IIdentifier identifier) { + Objects.requireNonNull(identifier); + return Result.ofOptional(getConstraints().stream() + .filter(constraint -> constraint.getIdentifier().equals(identifier)) + .findFirst()); + } + + default boolean hasConstraint(IIdentifier identifier) { + return getConstraint(identifier).isPresent(); + } + + default boolean hasConstraint(IConstraint constraint) { + return hasConstraint(constraint.getIdentifier()); + } + + default int getNumberOfConstraints() { + return getConstraints().size(); + } +} diff --git a/src/main/java/de/featjar/feature/model/mixins/IHasFeatureTree.java b/src/main/java/de/featjar/feature/model/mixins/IHasFeatureTree.java index 105abfb9..268687b8 100644 --- a/src/main/java/de/featjar/feature/model/mixins/IHasFeatureTree.java +++ b/src/main/java/de/featjar/feature/model/mixins/IHasFeatureTree.java @@ -1,96 +1,96 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.mixins; - -import de.featjar.base.data.*; -import de.featjar.base.data.identifier.IIdentifier; -import de.featjar.base.tree.Trees; -import de.featjar.feature.model.*; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -/** - * Implements a {@link IFeatureModel} mixin for common operations on the {@link IFeatureTree}. - * - * @author Elias Kuiter - */ -public interface IHasFeatureTree { - List getRoots(); - - default Stream getFeatureTreeStream() { - return getRoots().stream().flatMap(Trees::preOrderStream); - } - - default LinkedHashSet getTreeFeatures() { - LinkedHashSet featureSet = new LinkedHashSet<>(); - getFeatureTreeStream().map(IFeatureTree::getFeature).forEach(featureSet::add); - return featureSet; - } - - default int getNumberOfTreeFeatures() { - return getTreeFeatures().size(); - } - - default List getRootFeatures() { - return getRoots().stream().map(IFeatureTree::getFeature).collect(Collectors.toList()); - } - - default Result getTreeFeature(IIdentifier identifier) { - Objects.requireNonNull(identifier); - return Result.ofOptional(getFeatureTreeStream() - .map(IFeatureTree::getFeature) - .filter(feature -> feature.getIdentifier().equals(identifier)) - .findFirst()); - } - - default Result getTreeFeature(String name) { - Objects.requireNonNull(name); - return Result.ofOptional(getFeatureTreeStream() - .map(IFeatureTree::getFeature) - .filter(feature -> feature.getName().valueEquals(name)) - .findFirst()); - } - - default Result getFeatureTree(String name) { - Objects.requireNonNull(name); - return Result.ofOptional(getFeatureTreeStream() - .filter(tree -> tree.getFeature().getName().valueEquals(name)) - .findFirst()); - } - - default Result getFeatureTree(IFeature feature) { - Objects.requireNonNull(feature); - return Result.ofOptional(getFeatureTreeStream() - .filter(tree -> tree.getFeature().equals(feature)) - .findFirst()); - } - - default boolean hasTreeFeature(IIdentifier identifier) { - return getTreeFeature(identifier).isPresent(); - } - - default boolean hasTreeFeature(IFeature feature) { - return hasTreeFeature(feature.getIdentifier()); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.mixins; + +import de.featjar.base.data.*; +import de.featjar.base.data.identifier.IIdentifier; +import de.featjar.base.tree.Trees; +import de.featjar.feature.model.*; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Implements a {@link IFeatureModel} mixin for common operations on the {@link IFeatureTree}. + * + * @author Elias Kuiter + */ +public interface IHasFeatureTree { + List getRoots(); + + default Stream getFeatureTreeStream() { + return getRoots().stream().flatMap(Trees::preOrderStream); + } + + default LinkedHashSet getTreeFeatures() { + LinkedHashSet featureSet = new LinkedHashSet<>(); + getFeatureTreeStream().map(IFeatureTree::getFeature).forEach(featureSet::add); + return featureSet; + } + + default int getNumberOfTreeFeatures() { + return getTreeFeatures().size(); + } + + default List getRootFeatures() { + return getRoots().stream().map(IFeatureTree::getFeature).collect(Collectors.toList()); + } + + default Result getTreeFeature(IIdentifier identifier) { + Objects.requireNonNull(identifier); + return Result.ofOptional(getFeatureTreeStream() + .map(IFeatureTree::getFeature) + .filter(feature -> feature.getIdentifier().equals(identifier)) + .findFirst()); + } + + default Result getTreeFeature(String name) { + Objects.requireNonNull(name); + return Result.ofOptional(getFeatureTreeStream() + .map(IFeatureTree::getFeature) + .filter(feature -> feature.getName().valueEquals(name)) + .findFirst()); + } + + default Result getFeatureTree(String name) { + Objects.requireNonNull(name); + return Result.ofOptional(getFeatureTreeStream() + .filter(tree -> tree.getFeature().getName().valueEquals(name)) + .findFirst()); + } + + default Result getFeatureTree(IFeature feature) { + Objects.requireNonNull(feature); + return Result.ofOptional(getFeatureTreeStream() + .filter(tree -> tree.getFeature().equals(feature)) + .findFirst()); + } + + default boolean hasTreeFeature(IIdentifier identifier) { + return getTreeFeature(identifier).isPresent(); + } + + default boolean hasTreeFeature(IFeature feature) { + return hasTreeFeature(feature.getIdentifier()); + } +} diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java index 8a0fe13f..35e98bed 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java @@ -1,257 +1,254 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.transformer; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; - -import de.featjar.base.computation.AComputation; -import de.featjar.base.computation.Computations; -import de.featjar.base.computation.Dependency; -import de.featjar.base.computation.IComputation; -import de.featjar.base.computation.Progress; -import de.featjar.base.data.Attribute; -import de.featjar.base.data.Range; -import de.featjar.base.data.Result; -import de.featjar.base.tree.Trees; -import de.featjar.feature.model.FeatureTree.Group; -import de.featjar.feature.model.IFeatureModel; -import de.featjar.feature.model.IFeatureTree; -import de.featjar.formula.structure.IFormula; -import de.featjar.formula.structure.connective.And; -import de.featjar.formula.structure.connective.AtLeast; -import de.featjar.formula.structure.connective.AtMost; -import de.featjar.formula.structure.connective.Between; -import de.featjar.formula.structure.connective.Choose; -import de.featjar.formula.structure.connective.Implies; -import de.featjar.formula.structure.connective.Or; -import de.featjar.formula.structure.connective.Reference; -import de.featjar.formula.structure.predicate.Literal; -import de.featjar.formula.structure.term.value.Variable; - -/** - * Transforms a feature model into a boolean formula. - * - * @author Sebastian Krieter - */ -public class ComputeFormula extends AComputation { - protected static final Dependency FEATURE_MODEL = Dependency.newDependency(IFeatureModel.class); - protected static final Dependency SIMPLE_TRANSLATION = Dependency.newDependency(Boolean.class); - - static Attribute literalNameAttribute = new Attribute<>("literalName", String.class); - - public ComputeFormula(IComputation formula) { - super(formula, Computations.of(Boolean.FALSE)); - } - - protected ComputeFormula(ComputeFormula other) { - super(other); - } - - @Override - public Result compute(List dependencyList, Progress progress) { - IFeatureModel featureModel = FEATURE_MODEL.get(dependencyList); - ArrayList constraints = new ArrayList<>(); - HashSet variables = new HashSet<>(); - - IFeatureTree iFeatureTree = featureModel.getRoots().get(0); - - if (SIMPLE_TRANSLATION.get(dependencyList)) { - Trees.traverse(iFeatureTree, new ComputeSimpleFormulaVisitor(constraints, variables)); - } else { - traverseFeatureModel(featureModel, constraints, variables); - } - - Reference reference = new Reference(new And(constraints)); - reference.setFreeVariables(variables); - return Result.of(reference); - } - - private void traverseFeatureModel(IFeatureModel featureModel, ArrayList constraints, - HashSet variables) { - - for (IFeatureTree root : featureModel.getRoots()) { - - Literal rootLiteral = new Literal(root.getFeature().getName().orElse("")); - if(root.isMandatory()) { - constraints.add(rootLiteral); - } - handleGroups(rootLiteral, root, constraints); - - addChildConstraints(root, constraints); - } - } - - private void addChildConstraints(IFeatureTree node, ArrayList constraints) { - - Literal parentLiteral = new Literal(getLiteralName(node)); - - for (IFeatureTree child : node.getChildren()) { - - if (isCardinalityFeature(child)) { - - int upperBound = child.getFeatureCardinalityUpperBound(); - int lowerBound = child.getFeatureCardinalityLowerBound(); - - LinkedList constraintGroupLiterals = new LinkedList(); - - for (int i = 1; i <= upperBound; i++) { - - String literalName = getLiteralName(child) + "_" + i; - if (cardinalityFeatureAbove(child)) { - literalName += "." + getLiteralName(node); - } - - // clone only tree for traversal, not its children - IFeatureTree cardinalityClone = child.cloneTree(); - cardinalityClone.mutate().setAttributeValue(literalNameAttribute, literalName); - - Literal currentLiteral = new Literal(literalName); - - // add all the constraints - // imply parent - constraints.add(new Implies(currentLiteral, parentLiteral)); - // implication chain part - if (i > 1) { - Literal previousLiteral = constraintGroupLiterals.getLast(); - constraints.add(new Implies(currentLiteral, previousLiteral)); - } - // group constraints - handleGroups(currentLiteral, cardinalityClone, constraints); - - constraintGroupLiterals.add(currentLiteral); - - addChildConstraints(cardinalityClone, constraints); - } - // check if 0 and do not add implication - if(lowerBound != 0) - constraints.add(new Implies(parentLiteral, new AtLeast(lowerBound, constraintGroupLiterals))); - - return; - } else { - - String literalName = getLiteralName(child); - if (cardinalityFeatureAbove(child)) { - literalName += "." + getLiteralName(node); - } - - Literal childFeatureLiteral = new Literal(literalName); - child.mutate().setAttributeValue(literalNameAttribute, literalName); - - // add constraints - // always add parent implications (child implies parent) - constraints.add(new Implies(childFeatureLiteral, parentLiteral)); - - // handle group - handleGroups(childFeatureLiteral, child, constraints); - - addChildConstraints(child, constraints); - } - } - } - - private String getLiteralName(IFeatureTree node) { - String literalName = ""; - if (node.getAttributeValue(literalNameAttribute).isEmpty()) { - literalName = node.getFeature().getName().orElse(""); - } else { - literalName = node.getAttributeValue(literalNameAttribute).orElse(""); - } - return literalName; - } - - private boolean cardinalityFeatureAbove(IFeatureTree child) { - - if (!child.getParent().isPresent()) - return false; - - if (isCardinalityFeature(child.getParent().get())) { - return true; - } else { - return cardinalityFeatureAbove(child.getParent().get()); - } - } - - private boolean isCardinalityFeature(IFeatureTree node) { - - if (node.getFeatureCardinalityUpperBound() > 1) { - return true; - } - return false; - } - - private void handleGroups(Literal featureLiteral, IFeatureTree node, ArrayList constraints) { - List childrenGroups = node.getChildrenGroups(); - int groupCount = childrenGroups.size(); - ArrayList> groupLiterals = new ArrayList<>(groupCount); - for (int i = 0; i < groupCount; i++) { - groupLiterals.add(null); - } - List children = node.getChildren(); - for (IFeatureTree childNode : children) { - - String childLiteralName = getLiteralName(childNode); - if (childNode.getAttributeValue(literalNameAttribute).isEmpty() && cardinalityFeatureAbove(childNode)) - childLiteralName += "." + getLiteralName(node); - - Literal childLiteral = new Literal(childLiteralName); - - if (childNode.isMandatory()) { - constraints.add(new Implies(featureLiteral, childLiteral)); - } - - int groupID = childNode.getParentGroupID(); - List list = groupLiterals.get(groupID); - if (list == null) { - groupLiterals.set(groupID, list = new ArrayList<>()); - } - list.add(childLiteral); - } - for (int i = 0; i < groupCount; i++) { - Group group = childrenGroups.get(i); - if (group != null) { - if (group.isOr()) { - constraints.add(new Implies(featureLiteral, new Or(groupLiterals.get(i)))); - } else if (group.isAlternative()) { - constraints.add(new Implies(featureLiteral, new Choose(1, groupLiterals.get(i)))); - } else { - int lowerBound = group.getLowerBound(); - int upperBound = group.getUpperBound(); - if (lowerBound > 0) { - if (upperBound != Range.OPEN) { - constraints.add(new Implies(featureLiteral, - new Between(lowerBound, upperBound, groupLiterals.get(i)))); - } else { - constraints.add(new Implies(featureLiteral, new AtMost(upperBound, groupLiterals.get(i)))); - } - } else { - if (upperBound != Range.OPEN) { - constraints.add(new Implies(featureLiteral, new AtLeast(lowerBound, groupLiterals.get(i)))); - } - } - } - } - } - } - -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.transformer; + +import de.featjar.base.computation.AComputation; +import de.featjar.base.computation.Computations; +import de.featjar.base.computation.Dependency; +import de.featjar.base.computation.IComputation; +import de.featjar.base.computation.Progress; +import de.featjar.base.data.Attribute; +import de.featjar.base.data.Range; +import de.featjar.base.data.Result; +import de.featjar.base.tree.Trees; +import de.featjar.feature.model.FeatureTree.Group; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.IFeatureTree; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.connective.And; +import de.featjar.formula.structure.connective.AtLeast; +import de.featjar.formula.structure.connective.AtMost; +import de.featjar.formula.structure.connective.Between; +import de.featjar.formula.structure.connective.Choose; +import de.featjar.formula.structure.connective.Implies; +import de.featjar.formula.structure.connective.Or; +import de.featjar.formula.structure.connective.Reference; +import de.featjar.formula.structure.predicate.Literal; +import de.featjar.formula.structure.term.value.Variable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; + +/** + * Transforms a feature model into a boolean formula. + * + * @author Sebastian Krieter + */ +public class ComputeFormula extends AComputation { + protected static final Dependency FEATURE_MODEL = Dependency.newDependency(IFeatureModel.class); + protected static final Dependency SIMPLE_TRANSLATION = Dependency.newDependency(Boolean.class); + + static Attribute literalNameAttribute = new Attribute<>("literalName", String.class); + + public ComputeFormula(IComputation formula) { + super(formula, Computations.of(Boolean.FALSE)); + } + + protected ComputeFormula(ComputeFormula other) { + super(other); + } + + @Override + public Result compute(List dependencyList, Progress progress) { + IFeatureModel featureModel = FEATURE_MODEL.get(dependencyList); + ArrayList constraints = new ArrayList<>(); + HashSet variables = new HashSet<>(); + + IFeatureTree iFeatureTree = featureModel.getRoots().get(0); + + if (SIMPLE_TRANSLATION.get(dependencyList)) { + Trees.traverse(iFeatureTree, new ComputeSimpleFormulaVisitor(constraints, variables)); + } else { + traverseFeatureModel(featureModel, constraints, variables); + } + + Reference reference = new Reference(new And(constraints)); + reference.setFreeVariables(variables); + return Result.of(reference); + } + + private void traverseFeatureModel( + IFeatureModel featureModel, ArrayList constraints, HashSet variables) { + + for (IFeatureTree root : featureModel.getRoots()) { + + Literal rootLiteral = new Literal(root.getFeature().getName().orElse("")); + if (root.isMandatory()) { + constraints.add(rootLiteral); + } + handleGroups(rootLiteral, root, constraints); + + addChildConstraints(root, constraints); + } + } + + private void addChildConstraints(IFeatureTree node, ArrayList constraints) { + + Literal parentLiteral = new Literal(getLiteralName(node)); + + for (IFeatureTree child : node.getChildren()) { + + if (isCardinalityFeature(child)) { + + int upperBound = child.getFeatureCardinalityUpperBound(); + int lowerBound = child.getFeatureCardinalityLowerBound(); + + LinkedList constraintGroupLiterals = new LinkedList(); + + for (int i = 1; i <= upperBound; i++) { + + String literalName = getLiteralName(child) + "_" + i; + if (cardinalityFeatureAbove(child)) { + literalName += "." + getLiteralName(node); + } + + // clone only tree for traversal, not its children + IFeatureTree cardinalityClone = child.cloneTree(); + cardinalityClone.mutate().setAttributeValue(literalNameAttribute, literalName); + + Literal currentLiteral = new Literal(literalName); + + // add all the constraints + // imply parent + constraints.add(new Implies(currentLiteral, parentLiteral)); + // implication chain part + if (i > 1) { + Literal previousLiteral = constraintGroupLiterals.getLast(); + constraints.add(new Implies(currentLiteral, previousLiteral)); + } + // group constraints + handleGroups(currentLiteral, cardinalityClone, constraints); + + constraintGroupLiterals.add(currentLiteral); + + addChildConstraints(cardinalityClone, constraints); + } + // check if 0 and do not add implication + if (lowerBound != 0) + constraints.add(new Implies(parentLiteral, new AtLeast(lowerBound, constraintGroupLiterals))); + + return; + } else { + + String literalName = getLiteralName(child); + if (cardinalityFeatureAbove(child)) { + literalName += "." + getLiteralName(node); + } + + Literal childFeatureLiteral = new Literal(literalName); + child.mutate().setAttributeValue(literalNameAttribute, literalName); + + // add constraints + // always add parent implications (child implies parent) + constraints.add(new Implies(childFeatureLiteral, parentLiteral)); + + // handle group + handleGroups(childFeatureLiteral, child, constraints); + + addChildConstraints(child, constraints); + } + } + } + + private String getLiteralName(IFeatureTree node) { + String literalName = ""; + if (node.getAttributeValue(literalNameAttribute).isEmpty()) { + literalName = node.getFeature().getName().orElse(""); + } else { + literalName = node.getAttributeValue(literalNameAttribute).orElse(""); + } + return literalName; + } + + private boolean cardinalityFeatureAbove(IFeatureTree child) { + + if (!child.getParent().isPresent()) return false; + + if (isCardinalityFeature(child.getParent().get())) { + return true; + } else { + return cardinalityFeatureAbove(child.getParent().get()); + } + } + + private boolean isCardinalityFeature(IFeatureTree node) { + + if (node.getFeatureCardinalityUpperBound() > 1) { + return true; + } + return false; + } + + private void handleGroups(Literal featureLiteral, IFeatureTree node, ArrayList constraints) { + List childrenGroups = node.getChildrenGroups(); + int groupCount = childrenGroups.size(); + ArrayList> groupLiterals = new ArrayList<>(groupCount); + for (int i = 0; i < groupCount; i++) { + groupLiterals.add(null); + } + List children = node.getChildren(); + for (IFeatureTree childNode : children) { + + String childLiteralName = getLiteralName(childNode); + if (childNode.getAttributeValue(literalNameAttribute).isEmpty() && cardinalityFeatureAbove(childNode)) + childLiteralName += "." + getLiteralName(node); + + Literal childLiteral = new Literal(childLiteralName); + + if (childNode.isMandatory()) { + constraints.add(new Implies(featureLiteral, childLiteral)); + } + + int groupID = childNode.getParentGroupID(); + List list = groupLiterals.get(groupID); + if (list == null) { + groupLiterals.set(groupID, list = new ArrayList<>()); + } + list.add(childLiteral); + } + for (int i = 0; i < groupCount; i++) { + Group group = childrenGroups.get(i); + if (group != null) { + if (group.isOr()) { + constraints.add(new Implies(featureLiteral, new Or(groupLiterals.get(i)))); + } else if (group.isAlternative()) { + constraints.add(new Implies(featureLiteral, new Choose(1, groupLiterals.get(i)))); + } else { + int lowerBound = group.getLowerBound(); + int upperBound = group.getUpperBound(); + if (lowerBound > 0) { + if (upperBound != Range.OPEN) { + constraints.add(new Implies( + featureLiteral, new Between(lowerBound, upperBound, groupLiterals.get(i)))); + } else { + constraints.add(new Implies(featureLiteral, new AtMost(upperBound, groupLiterals.get(i)))); + } + } else { + if (upperBound != Range.OPEN) { + constraints.add(new Implies(featureLiteral, new AtLeast(lowerBound, groupLiterals.get(i)))); + } + } + } + } + } + } +} diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java b/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java index 90f1f105..b6b9afd8 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java @@ -1,9 +1,25 @@ +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ package de.featjar.feature.model.transformer; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; - import de.featjar.base.data.Range; import de.featjar.base.data.Result; import de.featjar.base.tree.visitor.ITreeVisitor; @@ -20,6 +36,9 @@ import de.featjar.formula.structure.connective.Or; import de.featjar.formula.structure.predicate.Literal; import de.featjar.formula.structure.term.value.Variable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; /** * This visitor implements a simple translation of IFeatureModel to boolean @@ -29,153 +48,152 @@ */ public class ComputeSimpleFormulaVisitor implements ITreeVisitor { - protected ArrayList constraints = new ArrayList<>(); - protected HashSet variables = new HashSet<>(); - - public ComputeSimpleFormulaVisitor(ArrayList constraints, HashSet variables) { - - this.constraints = constraints; - this.variables = variables; - } - - @Override - public TraversalAction firstVisit(List path) { - IFeatureTree node = ITreeVisitor.getCurrentNode(path); - - // TODO use better error value - IFeature feature = node.getFeature(); - String featureName = feature.getName().orElse(""); - // TODO: do not add variable if its a cardinality var. Add duplicates instead - Variable variable = new Variable(featureName, feature.getType()); - variables.add(variable); - - // TODO take featureRanges into Account - Result potentialParentTree = node.getParent(); - Literal featureLiteral = Expressions.literal(featureName); - if (potentialParentTree.isEmpty()) { - handleRoot(featureLiteral, node); - } else if (node.getFeatureCardinalityUpperBound() > 1) { - handleCardinalityFeature(featureLiteral, node); - } else { - handleParent(featureLiteral, node); - } - - handleGroups(featureLiteral, node); - - return ITreeVisitor.super.firstVisit(path); - } - - private void handleParent(Literal featureLiteral, IFeatureTree node) { - // cardinal features must not be a parent - Literal parentLiteral = getNextNonCardinalityParent(node); - constraints.add(new Implies(featureLiteral, parentLiteral)); - } - - private void handleRoot(Literal featureLiteral, IFeatureTree node) { - if (node.isMandatory()) { - constraints.add(featureLiteral); - } - } - - private void handleCardinalityFeature(Literal featureLiteral, IFeatureTree node) { - - int lowerBound = node.getFeatureCardinalityLowerBound(); - int upperBound = node.getFeatureCardinalityUpperBound(); - - ArrayList featureList = new ArrayList(); - - // add literals and implication to parent - String literalName = ""; - Literal parentLiteral = getNextNonCardinalityParent(node); - for (int i = 1; i <= upperBound; i++) { - - literalName = node.getFeature().getName().get() + "_" + i; - featureLiteral = new Literal(literalName); - handleParent(featureLiteral, node); - - if (i > 1) { - // add to implication chain - IFormula previousLiteral = featureList.get(featureList.size() - 1); - constraints.add(new Implies(featureLiteral, previousLiteral)); - } - - featureList.add(featureLiteral); - } - - // add cardinality constraint - // check if 0 and do not add implication - if (lowerBound != 0) - constraints.add(new Implies(parentLiteral, new AtLeast(lowerBound, featureList))); - - } - - private Literal getNextNonCardinalityParent(IFeatureTree node) { - - // if it is possible that root can be as well a cardinality feature - there must - // be an alternative - node = node.getParent().get(); - - if (node.getFeatureCardinalityUpperBound() > 1) { - return getNextNonCardinalityParent(node); - } - - return Expressions.literal(node.getFeature().getName().orElse("")); - } - - private void handleGroups(Literal featureLiteral, IFeatureTree node) { - List childrenGroups = node.getChildrenGroups(); - int groupCount = childrenGroups.size(); - ArrayList> groupLiterals = new ArrayList<>(groupCount); - for (int i = 0; i < groupCount; i++) { - groupLiterals.add(null); - } - - // if node is cardinality feature, set feature literal to parent with no - // cardinality - if (node.getFeatureCardinalityUpperBound() > 1) { - featureLiteral = getNextNonCardinalityParent(node); - } - - List children = node.getChildren(); - for (IFeatureTree childNode : children) { - Literal childLiteral = Expressions.literal(childNode.getFeature().getName().orElse("")); - - if (childNode.isMandatory()) { - constraints.add(new Implies(featureLiteral, childLiteral)); - } - - int groupID = childNode.getParentGroupID(); - List list = groupLiterals.get(groupID); - if (list == null) { - groupLiterals.set(groupID, list = new ArrayList<>()); - } - list.add(childLiteral); - } - - for (int i = 0; i < groupCount; i++) { - Group group = childrenGroups.get(i); - if (group != null) { - if (group.isOr()) { - constraints.add(new Implies(featureLiteral, new Or(groupLiterals.get(i)))); - } else if (group.isAlternative()) { - constraints.add(new Implies(featureLiteral, new Choose(1, groupLiterals.get(i)))); - } else { - int lowerBound = group.getLowerBound(); - int upperBound = group.getUpperBound(); - if (lowerBound > 0) { - if (upperBound != Range.OPEN) { - constraints.add(new Implies(featureLiteral, - new Between(lowerBound, upperBound, groupLiterals.get(i)))); - } else { - constraints.add(new Implies(featureLiteral, new AtMost(upperBound, groupLiterals.get(i)))); - } - } else { - if (upperBound != Range.OPEN) { - constraints.add(new Implies(featureLiteral, new AtLeast(lowerBound, groupLiterals.get(i)))); - } - } - } - } - } - } + protected ArrayList constraints = new ArrayList<>(); + protected HashSet variables = new HashSet<>(); + + public ComputeSimpleFormulaVisitor(ArrayList constraints, HashSet variables) { + + this.constraints = constraints; + this.variables = variables; + } + + @Override + public TraversalAction firstVisit(List path) { + IFeatureTree node = ITreeVisitor.getCurrentNode(path); + + // TODO use better error value + IFeature feature = node.getFeature(); + String featureName = feature.getName().orElse(""); + // TODO: do not add variable if its a cardinality var. Add duplicates instead + Variable variable = new Variable(featureName, feature.getType()); + variables.add(variable); + + // TODO take featureRanges into Account + Result potentialParentTree = node.getParent(); + Literal featureLiteral = Expressions.literal(featureName); + if (potentialParentTree.isEmpty()) { + handleRoot(featureLiteral, node); + } else if (node.getFeatureCardinalityUpperBound() > 1) { + handleCardinalityFeature(featureLiteral, node); + } else { + handleParent(featureLiteral, node); + } + + handleGroups(featureLiteral, node); + + return ITreeVisitor.super.firstVisit(path); + } + + private void handleParent(Literal featureLiteral, IFeatureTree node) { + // cardinal features must not be a parent + Literal parentLiteral = getNextNonCardinalityParent(node); + constraints.add(new Implies(featureLiteral, parentLiteral)); + } + + private void handleRoot(Literal featureLiteral, IFeatureTree node) { + if (node.isMandatory()) { + constraints.add(featureLiteral); + } + } + + private void handleCardinalityFeature(Literal featureLiteral, IFeatureTree node) { + + int lowerBound = node.getFeatureCardinalityLowerBound(); + int upperBound = node.getFeatureCardinalityUpperBound(); + + ArrayList featureList = new ArrayList(); + + // add literals and implication to parent + String literalName = ""; + Literal parentLiteral = getNextNonCardinalityParent(node); + for (int i = 1; i <= upperBound; i++) { + + literalName = node.getFeature().getName().get() + "_" + i; + featureLiteral = new Literal(literalName); + handleParent(featureLiteral, node); + + if (i > 1) { + // add to implication chain + IFormula previousLiteral = featureList.get(featureList.size() - 1); + constraints.add(new Implies(featureLiteral, previousLiteral)); + } + + featureList.add(featureLiteral); + } + + // add cardinality constraint + // check if 0 and do not add implication + if (lowerBound != 0) constraints.add(new Implies(parentLiteral, new AtLeast(lowerBound, featureList))); + } + + private Literal getNextNonCardinalityParent(IFeatureTree node) { + + // if it is possible that root can be as well a cardinality feature - there must + // be an alternative + node = node.getParent().get(); + + if (node.getFeatureCardinalityUpperBound() > 1) { + return getNextNonCardinalityParent(node); + } + + return Expressions.literal(node.getFeature().getName().orElse("")); + } + + private void handleGroups(Literal featureLiteral, IFeatureTree node) { + List childrenGroups = node.getChildrenGroups(); + int groupCount = childrenGroups.size(); + ArrayList> groupLiterals = new ArrayList<>(groupCount); + for (int i = 0; i < groupCount; i++) { + groupLiterals.add(null); + } + + // if node is cardinality feature, set feature literal to parent with no + // cardinality + if (node.getFeatureCardinalityUpperBound() > 1) { + featureLiteral = getNextNonCardinalityParent(node); + } + + List children = node.getChildren(); + for (IFeatureTree childNode : children) { + Literal childLiteral = + Expressions.literal(childNode.getFeature().getName().orElse("")); + + if (childNode.isMandatory()) { + constraints.add(new Implies(featureLiteral, childLiteral)); + } + + int groupID = childNode.getParentGroupID(); + List list = groupLiterals.get(groupID); + if (list == null) { + groupLiterals.set(groupID, list = new ArrayList<>()); + } + list.add(childLiteral); + } + + for (int i = 0; i < groupCount; i++) { + Group group = childrenGroups.get(i); + if (group != null) { + if (group.isOr()) { + constraints.add(new Implies(featureLiteral, new Or(groupLiterals.get(i)))); + } else if (group.isAlternative()) { + constraints.add(new Implies(featureLiteral, new Choose(1, groupLiterals.get(i)))); + } else { + int lowerBound = group.getLowerBound(); + int upperBound = group.getUpperBound(); + if (lowerBound > 0) { + if (upperBound != Range.OPEN) { + constraints.add(new Implies( + featureLiteral, new Between(lowerBound, upperBound, groupLiterals.get(i)))); + } else { + constraints.add(new Implies(featureLiteral, new AtMost(upperBound, groupLiterals.get(i)))); + } + } else { + if (upperBound != Range.OPEN) { + constraints.add(new Implies(featureLiteral, new AtLeast(lowerBound, groupLiterals.get(i)))); + } + } + } + } + } + } } diff --git a/src/test/java/de/featjar/feature/model/AttributeTest.java b/src/test/java/de/featjar/feature/model/AttributeTest.java index 95d08858..26d50c10 100644 --- a/src/test/java/de/featjar/feature/model/AttributeTest.java +++ b/src/test/java/de/featjar/feature/model/AttributeTest.java @@ -1,117 +1,117 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import de.featjar.base.data.Attribute; -import de.featjar.base.data.IAttributable; -import de.featjar.base.data.IAttribute; -import de.featjar.base.data.Result; -import de.featjar.base.data.identifier.Identifiers; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Optional; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** - * Tests for {@link Attribute}, {@link Attributes}, and {@link IAttributable}. - * - * @author Elias Kuiter - */ -public class AttributeTest { - FeatureModel featureModel; - Attribute attribute = new Attribute<>("any", "test", String.class); - - @BeforeEach - public void createFeatureModel() { - featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); - } - - @Test - public void attribute() { - assertEquals("any", attribute.getNamespace()); - assertEquals("test", attribute.getName()); - assertEquals(String.class, attribute.getType()); - } - - @Test - public void attributableGetSet() { - LinkedHashMap, Object> attributeToValueMap = new LinkedHashMap<>(); - Attribute attributeWithDefaultValue = new Attribute<>("test", String.class).setDefaultValue("default"); - Assertions.assertTrue(featureModel.getAttributeValue(attribute).isEmpty()); - Assertions.assertEquals(Result.of("default"), featureModel.getAttributeValue(attributeWithDefaultValue)); - assertEquals(attributeToValueMap, featureModel.getAttributes().get()); - featureModel.mutate().setAttributeValue(attribute, "value"); - attributeToValueMap.put(attribute, "value"); - IAttributable attributable = new IAttributable() { - @Override - public Optional, Object>> getAttributes() { - return Optional.of(attributeToValueMap); - } - }; - Assertions.assertEquals(Result.of("value"), featureModel.getAttributeValue(attribute)); - assertEquals(Result.of("value"), attribute.apply(attributable)); - assertTrue(featureModel.getAttributes().isPresent()); - assertEquals(attributeToValueMap, featureModel.getAttributes().get()); - featureModel.mutate().removeAttributeValue(attribute); - attributeToValueMap.clear(); - Assertions.assertEquals(Result.empty(), featureModel.getAttributeValue(attribute)); - assertEquals(attributeToValueMap, featureModel.getAttributes().get()); - } - - @Test - public void attributableToggle() { - Attribute booleanAttribute = new Attribute<>("test", Boolean.class).setDefaultValue(false); - Assertions.assertEquals(Result.of(false), featureModel.getAttributeValue(booleanAttribute)); - featureModel.mutate().toggleAttributeValue(booleanAttribute); - Assertions.assertEquals(Result.of(true), featureModel.getAttributeValue(booleanAttribute)); - } - - @Test - public void attributesName() { - Assertions.assertEquals(featureModel.getName(), featureModel.getAttributeValue(Attributes.NAME)); - Assertions.assertEquals( - "@" + featureModel.getIdentifier(), featureModel.getName().get()); - Assertions.assertEquals(Result.empty(), featureModel.getDescription()); - } - - @Test - public void attributesDescription() { - featureModel.mutate().setDescription("desc"); - Assertions.assertEquals(Result.of("desc"), featureModel.getDescription()); - featureModel.mutate().setDescription(null); - Assertions.assertEquals(Result.empty(), featureModel.getDescription()); - } - - @Test - public void attributesHidden() { - Assertions.assertTrue(featureModel.getRootFeatures().isEmpty()); - IFeature addFeature = featureModel.addFeature("hiddenFeature"); - Assertions.assertFalse(addFeature.isHidden()); - addFeature.mutate().setHidden(true); - Assertions.assertTrue(addFeature.isHidden()); - Assertions.assertFalse(addFeature.mutate().toggleHidden()); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import de.featjar.base.data.Attribute; +import de.featjar.base.data.IAttributable; +import de.featjar.base.data.IAttribute; +import de.featjar.base.data.Result; +import de.featjar.base.data.identifier.Identifiers; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link Attribute}, {@link Attributes}, and {@link IAttributable}. + * + * @author Elias Kuiter + */ +public class AttributeTest { + FeatureModel featureModel; + Attribute attribute = new Attribute<>("any", "test", String.class); + + @BeforeEach + public void createFeatureModel() { + featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + } + + @Test + public void attribute() { + assertEquals("any", attribute.getNamespace()); + assertEquals("test", attribute.getName()); + assertEquals(String.class, attribute.getType()); + } + + @Test + public void attributableGetSet() { + LinkedHashMap, Object> attributeToValueMap = new LinkedHashMap<>(); + Attribute attributeWithDefaultValue = new Attribute<>("test", String.class).setDefaultValue("default"); + Assertions.assertTrue(featureModel.getAttributeValue(attribute).isEmpty()); + Assertions.assertEquals(Result.of("default"), featureModel.getAttributeValue(attributeWithDefaultValue)); + assertEquals(attributeToValueMap, featureModel.getAttributes().get()); + featureModel.mutate().setAttributeValue(attribute, "value"); + attributeToValueMap.put(attribute, "value"); + IAttributable attributable = new IAttributable() { + @Override + public Optional, Object>> getAttributes() { + return Optional.of(attributeToValueMap); + } + }; + Assertions.assertEquals(Result.of("value"), featureModel.getAttributeValue(attribute)); + assertEquals(Result.of("value"), attribute.apply(attributable)); + assertTrue(featureModel.getAttributes().isPresent()); + assertEquals(attributeToValueMap, featureModel.getAttributes().get()); + featureModel.mutate().removeAttributeValue(attribute); + attributeToValueMap.clear(); + Assertions.assertEquals(Result.empty(), featureModel.getAttributeValue(attribute)); + assertEquals(attributeToValueMap, featureModel.getAttributes().get()); + } + + @Test + public void attributableToggle() { + Attribute booleanAttribute = new Attribute<>("test", Boolean.class).setDefaultValue(false); + Assertions.assertEquals(Result.of(false), featureModel.getAttributeValue(booleanAttribute)); + featureModel.mutate().toggleAttributeValue(booleanAttribute); + Assertions.assertEquals(Result.of(true), featureModel.getAttributeValue(booleanAttribute)); + } + + @Test + public void attributesName() { + Assertions.assertEquals(featureModel.getName(), featureModel.getAttributeValue(Attributes.NAME)); + Assertions.assertEquals( + "@" + featureModel.getIdentifier(), featureModel.getName().get()); + Assertions.assertEquals(Result.empty(), featureModel.getDescription()); + } + + @Test + public void attributesDescription() { + featureModel.mutate().setDescription("desc"); + Assertions.assertEquals(Result.of("desc"), featureModel.getDescription()); + featureModel.mutate().setDescription(null); + Assertions.assertEquals(Result.empty(), featureModel.getDescription()); + } + + @Test + public void attributesHidden() { + Assertions.assertTrue(featureModel.getRootFeatures().isEmpty()); + IFeature addFeature = featureModel.addFeature("hiddenFeature"); + Assertions.assertFalse(addFeature.isHidden()); + addFeature.mutate().setHidden(true); + Assertions.assertTrue(addFeature.isHidden()); + Assertions.assertFalse(addFeature.mutate().toggleHidden()); + } +} diff --git a/src/test/java/de/featjar/feature/model/FeatureModelTest.java b/src/test/java/de/featjar/feature/model/FeatureModelTest.java index e1566fff..73cd4739 100644 --- a/src/test/java/de/featjar/feature/model/FeatureModelTest.java +++ b/src/test/java/de/featjar/feature/model/FeatureModelTest.java @@ -1,102 +1,102 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import static org.junit.jupiter.api.Assertions.*; - -import de.featjar.base.data.Result; -import de.featjar.base.data.identifier.Identifiers; -import de.featjar.formula.structure.Expressions; -import java.util.*; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** - * Tests for {@link FeatureModel}, its elements, and its mixins. - * - * @author Elias Kuiter - */ -public class FeatureModelTest { - IFeatureModel featureModel; - - @BeforeEach - public void createFeatureModel() { - featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); - } - - @Test - public void featureModel() { - Assertions.assertEquals("1", featureModel.getIdentifier().toString()); - assertTrue(featureModel.getRoots().isEmpty()); - assertTrue(featureModel.getFeatures().isEmpty()); - assertTrue(featureModel.getConstraints().isEmpty()); - } - - @Test - public void commonAttributesMixin() { - Assertions.assertEquals("@1", featureModel.getName().get()); - Assertions.assertEquals(Result.empty(), featureModel.getDescription()); - featureModel.mutate().setName("My Model"); - featureModel.mutate().setDescription("awesome description"); - Assertions.assertEquals(Result.of("My Model"), featureModel.getName()); - Assertions.assertEquals(Result.of("awesome description"), featureModel.getDescription()); - } - - @Test - public void featureModelConstraintMixin() { - Assertions.assertEquals(0, featureModel.getNumberOfConstraints()); - IConstraint constraint1 = featureModel.mutate().addConstraint(Expressions.True); - IConstraint constraint2 = featureModel.mutate().addConstraint(Expressions.True); - IConstraint constraint3 = featureModel.mutate().addConstraint(Expressions.False); - Assertions.assertEquals(3, featureModel.getNumberOfConstraints()); - Assertions.assertEquals(Result.of(constraint1), featureModel.getConstraint(constraint1.getIdentifier())); - Assertions.assertTrue(featureModel.hasConstraint(constraint2.getIdentifier())); - constraint2.mutate().remove(); - Assertions.assertFalse(featureModel.hasConstraint(constraint2.getIdentifier())); - Assertions.assertTrue(featureModel.hasConstraint(constraint3)); - } - - @Test - public void featureModelFeatureTreeMixin() { - IFeature rootFeature = featureModel.mutate().addFeature("root"); - Assertions.assertEquals(1, featureModel.getNumberOfFeatures()); - IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(rootFeature); - IFeature childFeature = featureModel.mutate().addFeature("child1"); - final IFeatureTree childTree = rootTree.mutate().addFeatureBelow(childFeature); - assertSame(childFeature, childTree.getFeature()); - assertSame(childTree, childFeature.getFeatureTree().get()); - assertSame(childFeature, childTree.getFeature()); - assertSame(rootFeature, childTree.getParent().get().getFeature()); - assertSame(childTree.getParent().get(), rootFeature.getFeatureTree().get()); - assertSame(featureModel.getFeature(childFeature.getIdentifier()).get(), childFeature); - Assertions.assertEquals(2, featureModel.getNumberOfFeatures()); - Assertions.assertEquals(Result.of(childFeature), featureModel.getFeature(childFeature.getIdentifier())); - Assertions.assertTrue(featureModel.hasFeature(childFeature.getIdentifier())); - Assertions.assertTrue(featureModel.getFeature("root2").isEmpty()); - rootFeature.mutate().setName("root2"); - Assertions.assertEquals(Result.of(rootFeature), featureModel.getFeature("root2")); - assertEquals(List.of(childTree), rootFeature.getFeatureTree().get().getChildren()); - assertEquals(rootFeature.getFeatureTree(), childTree.getParent()); - childTree.mutate().removeFromTree(); - assertEquals(List.of(), rootFeature.getFeatureTree().get().getChildren()); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import static org.junit.jupiter.api.Assertions.*; + +import de.featjar.base.data.Result; +import de.featjar.base.data.identifier.Identifiers; +import de.featjar.formula.structure.Expressions; +import java.util.*; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link FeatureModel}, its elements, and its mixins. + * + * @author Elias Kuiter + */ +public class FeatureModelTest { + IFeatureModel featureModel; + + @BeforeEach + public void createFeatureModel() { + featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + } + + @Test + public void featureModel() { + Assertions.assertEquals("1", featureModel.getIdentifier().toString()); + assertTrue(featureModel.getRoots().isEmpty()); + assertTrue(featureModel.getFeatures().isEmpty()); + assertTrue(featureModel.getConstraints().isEmpty()); + } + + @Test + public void commonAttributesMixin() { + Assertions.assertEquals("@1", featureModel.getName().get()); + Assertions.assertEquals(Result.empty(), featureModel.getDescription()); + featureModel.mutate().setName("My Model"); + featureModel.mutate().setDescription("awesome description"); + Assertions.assertEquals(Result.of("My Model"), featureModel.getName()); + Assertions.assertEquals(Result.of("awesome description"), featureModel.getDescription()); + } + + @Test + public void featureModelConstraintMixin() { + Assertions.assertEquals(0, featureModel.getNumberOfConstraints()); + IConstraint constraint1 = featureModel.mutate().addConstraint(Expressions.True); + IConstraint constraint2 = featureModel.mutate().addConstraint(Expressions.True); + IConstraint constraint3 = featureModel.mutate().addConstraint(Expressions.False); + Assertions.assertEquals(3, featureModel.getNumberOfConstraints()); + Assertions.assertEquals(Result.of(constraint1), featureModel.getConstraint(constraint1.getIdentifier())); + Assertions.assertTrue(featureModel.hasConstraint(constraint2.getIdentifier())); + constraint2.mutate().remove(); + Assertions.assertFalse(featureModel.hasConstraint(constraint2.getIdentifier())); + Assertions.assertTrue(featureModel.hasConstraint(constraint3)); + } + + @Test + public void featureModelFeatureTreeMixin() { + IFeature rootFeature = featureModel.mutate().addFeature("root"); + Assertions.assertEquals(1, featureModel.getNumberOfFeatures()); + IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(rootFeature); + IFeature childFeature = featureModel.mutate().addFeature("child1"); + final IFeatureTree childTree = rootTree.mutate().addFeatureBelow(childFeature); + assertSame(childFeature, childTree.getFeature()); + assertSame(childTree, childFeature.getFeatureTree().get()); + assertSame(childFeature, childTree.getFeature()); + assertSame(rootFeature, childTree.getParent().get().getFeature()); + assertSame(childTree.getParent().get(), rootFeature.getFeatureTree().get()); + assertSame(featureModel.getFeature(childFeature.getIdentifier()).get(), childFeature); + Assertions.assertEquals(2, featureModel.getNumberOfFeatures()); + Assertions.assertEquals(Result.of(childFeature), featureModel.getFeature(childFeature.getIdentifier())); + Assertions.assertTrue(featureModel.hasFeature(childFeature.getIdentifier())); + Assertions.assertTrue(featureModel.getFeature("root2").isEmpty()); + rootFeature.mutate().setName("root2"); + Assertions.assertEquals(Result.of(rootFeature), featureModel.getFeature("root2")); + assertEquals(List.of(childTree), rootFeature.getFeatureTree().get().getChildren()); + assertEquals(rootFeature.getFeatureTree(), childTree.getParent()); + childTree.mutate().removeFromTree(); + assertEquals(List.of(), rootFeature.getFeatureTree().get().getChildren()); + } +} diff --git a/src/test/java/de/featjar/feature/model/FeatureTest.java b/src/test/java/de/featjar/feature/model/FeatureTest.java index 8758ad35..58b9bb0c 100644 --- a/src/test/java/de/featjar/feature/model/FeatureTest.java +++ b/src/test/java/de/featjar/feature/model/FeatureTest.java @@ -1,117 +1,117 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import de.featjar.base.data.identifier.Identifiers; -import de.featjar.formula.structure.IFormula; -import de.featjar.formula.structure.connective.And; -import de.featjar.formula.structure.connective.BiImplies; -import de.featjar.formula.structure.connective.Implies; -import de.featjar.formula.structure.connective.Not; -import de.featjar.formula.structure.connective.Or; -import de.featjar.formula.structure.predicate.Literal; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -public class FeatureTest { - private static FeatureModel featureModel; - - @BeforeAll - public static void setup() { - FeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); - - // features - IFeatureTree rootTree = - featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - rootTree.mutate().toAndGroup(); - - IFeature childFeature1 = featureModel.mutate().addFeature("Test1"); - IFeatureTree childTree1 = rootTree.mutate().addFeatureBelow(childFeature1); - - IFeature childFeature2 = featureModel.mutate().addFeature("Test2"); - IFeatureTree childTree2 = rootTree.mutate().addFeatureBelow(childFeature2); - - IFeature childFeature3 = featureModel.mutate().addFeature("Test3"); - IFeatureTree childTree3 = childTree1.mutate().addFeatureBelow(childFeature3); - childTree1.mutate().toAlternativeGroup(); - - IFeature childFeature4 = featureModel.mutate().addFeature("Test4"); - childTree1.mutate().addFeatureBelow(childFeature4); - - IFeature childFeature5 = featureModel.mutate().addFeature("Test5"); - IFeatureTree childTree5 = childTree2.mutate().addFeatureBelow(childFeature5); - childTree2.mutate().toOrGroup(); - - IFeature childFeature6 = featureModel.mutate().addFeature("Test6"); - childTree2.mutate().addFeatureBelow(childFeature6); - - IFeature childFeature7 = featureModel.mutate().addFeature("Test7"); - IFeatureTree childTree7 = rootTree.mutate().addFeatureBelow(childFeature7); - childTree7.mutate().makeMandatory(); - - IFeature childFeature8 = featureModel.mutate().addFeature("Test8"); - childTree3.mutate().addFeatureBelow(childFeature8); - - IFormula formula1 = new Or( - new And(new Literal("Test1"), new Literal("Test2")), - new BiImplies(new Literal("Test3"), new Literal("Test4")), - new Implies(new Literal("Test5"), new Literal("Test6")), - new Not(new Literal("Test7"))); - - // constraints - featureModel.mutate().addConstraint(formula1); - - featureModel.getFeature("Test1").get().mutate().setHidden(true); - - FeatureTest.featureModel = featureModel; - } - - @Test - public void hasHiddenParentMethodCorrectlyTraversesTreeToCheckIfThereIsAParentThatIsHidden() { - IFeature root = featureModel.getFeature("root").get(); - IFeature test1 = featureModel.getFeature("Test1").get(); - IFeature test2 = featureModel.getFeature("Test2").get(); - IFeature test3 = featureModel.getFeature("Test3").get(); - IFeature test5 = featureModel.getFeature("Test5").get(); - IFeature test8 = featureModel.getFeature("Test8").get(); - - // Tests the visible root. - assertFalse(root.hasHiddenParent()); - - // Tests the hidden feature. - assertFalse(test1.hasHiddenParent()); - - // Tests a visible feature at the same depth of the hidden feature (test1). - assertFalse(test2.hasHiddenParent()); - - // Tests child of hidden feature (test1). - assertTrue(test3.hasHiddenParent()); - - // Tests child of visible feature (test2). - assertFalse(test5.hasHiddenParent()); - - // Tests grandchild for hidden feature (test1). - assertTrue(test8.hasHiddenParent()); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import de.featjar.base.data.identifier.Identifiers; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.connective.And; +import de.featjar.formula.structure.connective.BiImplies; +import de.featjar.formula.structure.connective.Implies; +import de.featjar.formula.structure.connective.Not; +import de.featjar.formula.structure.connective.Or; +import de.featjar.formula.structure.predicate.Literal; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class FeatureTest { + private static FeatureModel featureModel; + + @BeforeAll + public static void setup() { + FeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + + // features + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAndGroup(); + + IFeature childFeature1 = featureModel.mutate().addFeature("Test1"); + IFeatureTree childTree1 = rootTree.mutate().addFeatureBelow(childFeature1); + + IFeature childFeature2 = featureModel.mutate().addFeature("Test2"); + IFeatureTree childTree2 = rootTree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("Test3"); + IFeatureTree childTree3 = childTree1.mutate().addFeatureBelow(childFeature3); + childTree1.mutate().toAlternativeGroup(); + + IFeature childFeature4 = featureModel.mutate().addFeature("Test4"); + childTree1.mutate().addFeatureBelow(childFeature4); + + IFeature childFeature5 = featureModel.mutate().addFeature("Test5"); + IFeatureTree childTree5 = childTree2.mutate().addFeatureBelow(childFeature5); + childTree2.mutate().toOrGroup(); + + IFeature childFeature6 = featureModel.mutate().addFeature("Test6"); + childTree2.mutate().addFeatureBelow(childFeature6); + + IFeature childFeature7 = featureModel.mutate().addFeature("Test7"); + IFeatureTree childTree7 = rootTree.mutate().addFeatureBelow(childFeature7); + childTree7.mutate().makeMandatory(); + + IFeature childFeature8 = featureModel.mutate().addFeature("Test8"); + childTree3.mutate().addFeatureBelow(childFeature8); + + IFormula formula1 = new Or( + new And(new Literal("Test1"), new Literal("Test2")), + new BiImplies(new Literal("Test3"), new Literal("Test4")), + new Implies(new Literal("Test5"), new Literal("Test6")), + new Not(new Literal("Test7"))); + + // constraints + featureModel.mutate().addConstraint(formula1); + + featureModel.getFeature("Test1").get().mutate().setHidden(true); + + FeatureTest.featureModel = featureModel; + } + + @Test + public void hasHiddenParentMethodCorrectlyTraversesTreeToCheckIfThereIsAParentThatIsHidden() { + IFeature root = featureModel.getFeature("root").get(); + IFeature test1 = featureModel.getFeature("Test1").get(); + IFeature test2 = featureModel.getFeature("Test2").get(); + IFeature test3 = featureModel.getFeature("Test3").get(); + IFeature test5 = featureModel.getFeature("Test5").get(); + IFeature test8 = featureModel.getFeature("Test8").get(); + + // Tests the visible root. + assertFalse(root.hasHiddenParent()); + + // Tests the hidden feature. + assertFalse(test1.hasHiddenParent()); + + // Tests a visible feature at the same depth of the hidden feature (test1). + assertFalse(test2.hasHiddenParent()); + + // Tests child of hidden feature (test1). + assertTrue(test3.hasHiddenParent()); + + // Tests child of visible feature (test2). + assertFalse(test5.hasHiddenParent()); + + // Tests grandchild for hidden feature (test1). + assertTrue(test8.hasHiddenParent()); + } +} diff --git a/src/test/java/de/featjar/feature/model/IdentifierTest.java b/src/test/java/de/featjar/feature/model/IdentifierTest.java index 1a221d82..d72eabe1 100644 --- a/src/test/java/de/featjar/feature/model/IdentifierTest.java +++ b/src/test/java/de/featjar/feature/model/IdentifierTest.java @@ -1,87 +1,87 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; - -import de.featjar.base.data.identifier.AIdentifier; -import de.featjar.base.data.identifier.IIdentifiable; -import de.featjar.base.data.identifier.IIdentifier; -import de.featjar.base.data.identifier.Identifiers; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -/** - * Tests for {@link AIdentifier} and {@link IIdentifiable}. - * - * @author Elias Kuiter - */ -public class IdentifierTest { - IFeatureModel featureModel; - - @BeforeEach - public void createFeatureModel() { - featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); - } - - @Test - void identifierCounter() { - IIdentifier identifier = Identifiers.newCounterIdentifier(); - assertEquals("1", identifier.toString()); - assertEquals("2", identifier.getNewIdentifier().toString()); - assertEquals("3", identifier.getNewIdentifier().toString()); - assertNotEquals(identifier.toString(), identifier.getNewIdentifier().toString()); - assertEquals(identifier, identifier.getFactory().parse(identifier.toString())); - } - - @Test - void identifierUUID() { - IIdentifier identifier = Identifiers.newUUIDIdentifier(); - assertNotEquals(identifier.toString(), identifier.getNewIdentifier().toString()); - assertEquals(identifier, identifier.getFactory().parse(identifier.toString())); - } - - @Test - void identifiable() { - IIdentifier identifier = Identifiers.newCounterIdentifier(); - featureModel = new FeatureModel(identifier); - assertEquals("1", featureModel.getIdentifier().toString()); - featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - Assertions.assertEquals( - "2", featureModel.getRootFeatures().get(0).getIdentifier().toString()); - Assertions.assertEquals("3", identifier.getFactory().get().toString()); - Assertions.assertEquals( - "4", featureModel.getRootFeatures().get(0).getNewIdentifier().toString()); - featureModel = new FeatureModel(identifier.getNewIdentifier()); - featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - assertEquals("5", featureModel.getIdentifier().toString()); - Assertions.assertEquals( - "6", featureModel.getRootFeatures().get(0).getIdentifier().toString()); - assertEquals("7", featureModel.getNewIdentifier().toString()); - assertEquals( - "2", - new FeatureModel(Identifiers.newCounterIdentifier()) - .getNewIdentifier() - .toString()); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import de.featjar.base.data.identifier.AIdentifier; +import de.featjar.base.data.identifier.IIdentifiable; +import de.featjar.base.data.identifier.IIdentifier; +import de.featjar.base.data.identifier.Identifiers; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link AIdentifier} and {@link IIdentifiable}. + * + * @author Elias Kuiter + */ +public class IdentifierTest { + IFeatureModel featureModel; + + @BeforeEach + public void createFeatureModel() { + featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + } + + @Test + void identifierCounter() { + IIdentifier identifier = Identifiers.newCounterIdentifier(); + assertEquals("1", identifier.toString()); + assertEquals("2", identifier.getNewIdentifier().toString()); + assertEquals("3", identifier.getNewIdentifier().toString()); + assertNotEquals(identifier.toString(), identifier.getNewIdentifier().toString()); + assertEquals(identifier, identifier.getFactory().parse(identifier.toString())); + } + + @Test + void identifierUUID() { + IIdentifier identifier = Identifiers.newUUIDIdentifier(); + assertNotEquals(identifier.toString(), identifier.getNewIdentifier().toString()); + assertEquals(identifier, identifier.getFactory().parse(identifier.toString())); + } + + @Test + void identifiable() { + IIdentifier identifier = Identifiers.newCounterIdentifier(); + featureModel = new FeatureModel(identifier); + assertEquals("1", featureModel.getIdentifier().toString()); + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + Assertions.assertEquals( + "2", featureModel.getRootFeatures().get(0).getIdentifier().toString()); + Assertions.assertEquals("3", identifier.getFactory().get().toString()); + Assertions.assertEquals( + "4", featureModel.getRootFeatures().get(0).getNewIdentifier().toString()); + featureModel = new FeatureModel(identifier.getNewIdentifier()); + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + assertEquals("5", featureModel.getIdentifier().toString()); + Assertions.assertEquals( + "6", featureModel.getRootFeatures().get(0).getIdentifier().toString()); + assertEquals("7", featureModel.getNewIdentifier().toString()); + assertEquals( + "2", + new FeatureModel(Identifiers.newCounterIdentifier()) + .getNewIdentifier() + .toString()); + } +} diff --git a/src/test/java/de/featjar/feature/model/configuration/ConfigurationTest.java b/src/test/java/de/featjar/feature/model/configuration/ConfigurationTest.java index b9a4c911..5e40def2 100644 --- a/src/test/java/de/featjar/feature/model/configuration/ConfigurationTest.java +++ b/src/test/java/de/featjar/feature/model/configuration/ConfigurationTest.java @@ -1,356 +1,356 @@ -/* -< * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.configuration; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import de.featjar.base.data.identifier.Identifiers; -import de.featjar.feature.configuration.Configuration; -import de.featjar.feature.configuration.Configuration.Selection; -import de.featjar.feature.configuration.Configuration.SelectionNotPossibleException; -import de.featjar.feature.model.FeatureModel; -import de.featjar.feature.model.IFeature; -import de.featjar.feature.model.IFeatureTree; -import de.featjar.formula.structure.IFormula; -import de.featjar.formula.structure.connective.And; -import de.featjar.formula.structure.connective.BiImplies; -import de.featjar.formula.structure.connective.Implies; -import de.featjar.formula.structure.connective.Not; -import de.featjar.formula.structure.connective.Or; -import de.featjar.formula.structure.predicate.Literal; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -public class ConfigurationTest { - - private static FeatureModel featureModel; - private static Configuration configuration; - - @BeforeAll - public static void setupTestConfiguration() { - - FeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); - - // features - IFeatureTree rootTree = - featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - rootTree.mutate().toAlternativeGroup(); - - IFeature childFeature1 = featureModel.mutate().addFeature("Test1"); - IFeatureTree childTree1 = rootTree.mutate().addFeatureBelow(childFeature1); - childTree1.mutate().toAlternativeGroup(); - - IFeature childFeature2 = featureModel.mutate().addFeature("Test2"); - IFeatureTree childTree2 = rootTree.mutate().addFeatureBelow(childFeature2); - childTree2.mutate().toOrGroup(); - - IFeature childFeature3 = featureModel.mutate().addFeature("Test3"); - IFeatureTree childTree3 = childTree1.mutate().addFeatureBelow(childFeature3); - - IFeature childFeature4 = featureModel.mutate().addFeature("Test4"); - childTree1.mutate().addFeatureBelow(childFeature4); - - IFeature childFeature5 = featureModel.mutate().addFeature("Test5"); - IFeatureTree childTree5 = childTree2.mutate().addFeatureBelow(childFeature5); - - IFeature childFeature6 = featureModel.mutate().addFeature("Test6"); - childTree2.mutate().addFeatureBelow(childFeature6); - - IFeature childFeature7 = featureModel.mutate().addFeature("Test7"); - IFeatureTree childTree7 = rootTree.mutate().addFeatureBelow(childFeature7); - childTree7.mutate().makeMandatory(); - - IFormula formula1 = new Or( - new And(new Literal("Test1"), new Literal("Test2")), - new BiImplies(new Literal("Test3"), new Literal("Test4")), - new Implies(new Literal("Test5"), new Literal("Test6")), - new Not(new Literal("Test7"))); - - // constraints - featureModel.mutate().addConstraint(formula1); - - ConfigurationTest.featureModel = featureModel; - ConfigurationTest.configuration = new Configuration(featureModel); - } - - @Test - public void testfeatureModelToConfigurationAsSet() { - for (IFeature feature : featureModel.getFeatures()) { - assertFalse( - configuration.getSelection(feature.getName().orElseThrow()).isEmpty()); - } - } - - @Test - public void testConfigurationCloning() { - - // configuration setup - FeatureModel featureModelForCloning = new FeatureModel(Identifiers.newCounterIdentifier()); - - // features - IFeatureTree rootTree = featureModelForCloning - .mutate() - .addFeatureTreeRoot(featureModelForCloning.mutate().addFeature("root")); - - IFeature childFeature1 = featureModelForCloning.mutate().addFeature("Test1"); - IFeatureTree childTree1 = rootTree.mutate().addFeatureBelow(childFeature1); - childTree1.mutate().toAlternativeGroup(); - - IFeature childFeature2 = featureModelForCloning.mutate().addFeature("Test2"); - IFeatureTree childTree2 = rootTree.mutate().addFeatureBelow(childFeature2); - childTree2.mutate().toOrGroup(); - - IFeature childFeature3 = featureModelForCloning.mutate().addFeature("Test3"); - IFeatureTree childTree3 = childTree1.mutate().addFeatureBelow(childFeature3); - - IFeature childFeature4 = featureModelForCloning.mutate().addFeature("Test4"); - childTree1.mutate().addFeatureBelow(childFeature4); - - IFeature childFeature5 = featureModelForCloning.mutate().addFeature("Test5"); - IFeatureTree childTree5 = childTree2.mutate().addFeatureBelow(childFeature5); - - IFeature childFeature6 = featureModelForCloning.mutate().addFeature("Test6"); - childTree2.mutate().addFeatureBelow(childFeature6); - - IFeature childFeature7 = featureModelForCloning.mutate().addFeature("Test7"); - IFeatureTree childTree7 = rootTree.mutate().addFeatureBelow(childFeature7); - childTree7.mutate().makeMandatory(); - - IFormula formula1 = new Or( - new And(new Literal("Test1"), new Literal("Test2")), - new BiImplies(new Literal("Test3"), new Literal("Test4")), - new Implies(new Literal("Test5"), new Literal("Test6")), - new Not(new Literal("Test7"))); - - // constraints - featureModelForCloning.mutate().addConstraint(formula1); - - Configuration configurationForCloning = new Configuration(featureModelForCloning); - - // selection setup - configurationForCloning.getSelection("root").orElseThrow().setManual(Boolean.TRUE); - configurationForCloning.getSelection("Test1").orElseThrow().setAutomatic(Boolean.TRUE); - configurationForCloning.getSelection("Test2").orElseThrow().setManual(Boolean.TRUE); - configurationForCloning.getSelection("Test3").orElseThrow().setAutomatic(Boolean.FALSE); - configurationForCloning.getSelection("Test4").orElseThrow().setManual(Boolean.FALSE); - - // execute clone() - Configuration clonedConfiguration = configurationForCloning.clone(); - - // check whether the selections of the clonedConfiguration are the same as for configurationForCloning - assertEquals( - Boolean.TRUE, - clonedConfiguration.getSelection("root").orElseThrow().getManual()); - assertEquals( - Boolean.TRUE, - clonedConfiguration.getSelection("Test1").orElseThrow().getAutomatic()); - assertEquals( - Boolean.TRUE, - clonedConfiguration.getSelection("Test2").orElseThrow().getManual()); - - assertEquals( - Boolean.FALSE, - clonedConfiguration.getSelection("Test3").orElseThrow().getAutomatic()); - assertEquals( - Boolean.FALSE, - clonedConfiguration.getSelection("Test4").orElseThrow().getManual()); - - assertEquals( - null, clonedConfiguration.getSelection("Test5").orElseThrow().getSelection()); - assertEquals( - null, clonedConfiguration.getSelection("Test6").orElseThrow().getSelection()); - assertEquals( - null, clonedConfiguration.getSelection("Test7").orElseThrow().getSelection()); - } - - @Test - public void testSelectionAttributesSetterAndGetterOfAutomaticAndManual() { - Selection testSelection = new Selection<>(Boolean.class); - - // Initial state - assertEquals(null, testSelection.getAutomatic()); - assertEquals(null, testSelection.getManual()); - - // Test manual selection to SELECTED - testSelection.setManual(Boolean.TRUE); - assertEquals(null, testSelection.getAutomatic()); - assertEquals(Boolean.TRUE, testSelection.getManual()); - assertThrows(SelectionNotPossibleException.class, () -> { - testSelection.setAutomatic(Boolean.FALSE); - }); - testSelection.setManual(null); - - // Test automatic selection to SELECTED - testSelection.setAutomatic(Boolean.TRUE); - assertEquals(Boolean.TRUE, testSelection.getAutomatic()); - assertEquals(null, testSelection.getManual()); - assertThrows(SelectionNotPossibleException.class, () -> { - testSelection.setManual(Boolean.FALSE); - }); - testSelection.setAutomatic(null); - - // Test manual selection to UNSELECTED - testSelection.setManual(Boolean.FALSE); - assertEquals(null, testSelection.getAutomatic()); - assertEquals(Boolean.FALSE, testSelection.getManual()); - assertThrows(SelectionNotPossibleException.class, () -> { - testSelection.setAutomatic(Boolean.TRUE); - }); - testSelection.setManual(null); - - // Test automatic selection to UNSELECTED - testSelection.setAutomatic(Boolean.FALSE); - assertEquals(Boolean.FALSE, testSelection.getAutomatic()); - assertEquals(null, testSelection.getManual()); - assertThrows(SelectionNotPossibleException.class, () -> { - testSelection.setManual(Boolean.TRUE); - }); - testSelection.setAutomatic(null); - - // Test setting both manual and automatic to SELECTED - testSelection.setManual(Boolean.TRUE); - testSelection.setAutomatic(Boolean.TRUE); - assertEquals(Boolean.TRUE, testSelection.getManual()); - assertEquals(Boolean.TRUE, testSelection.getAutomatic()); - testSelection.setManual(null); - testSelection.setAutomatic(null); - - // Test setting both manual and automatic to UNSELECTED - testSelection.setManual(Boolean.FALSE); - testSelection.setAutomatic(Boolean.FALSE); - assertEquals(Boolean.FALSE, testSelection.getManual()); - assertEquals(Boolean.FALSE, testSelection.getAutomatic()); - testSelection.setManual(null); - testSelection.setAutomatic(null); - } - - @Test - public void testSelectionAttributesSetterAndGetterOfAutomaticAndManualFromATestConfiguration() { - Configuration testConfiguration = configuration.clone(); - - assertEquals(null, testConfiguration.getSelection("Test1").orElseThrow().getAutomatic()); - assertEquals(null, testConfiguration.getSelection("Test2").orElseThrow().getManual()); - - testConfiguration.get("Test1").setAutomatic(Boolean.TRUE); - testConfiguration.get("Test2").setManual(Boolean.TRUE); - - assertEquals( - Boolean.TRUE, - testConfiguration.getSelection("Test1").orElseThrow().getAutomatic()); - assertEquals( - Boolean.TRUE, - testConfiguration.getSelection("Test2").orElseThrow().getManual()); - assertEquals( - Boolean.TRUE, - testConfiguration.getSelection("Test1").orElseThrow().getSelection()); - assertEquals( - Boolean.TRUE, - testConfiguration.getSelection("Test2").orElseThrow().getSelection()); - - testConfiguration.get("Test1").setAutomatic(Boolean.FALSE); - testConfiguration.get("Test2").setManual(Boolean.FALSE); - - assertEquals( - Boolean.FALSE, - testConfiguration.getSelection("Test1").orElseThrow().getAutomatic()); - assertEquals( - Boolean.FALSE, - testConfiguration.getSelection("Test2").orElseThrow().getManual()); - assertEquals( - Boolean.FALSE, - testConfiguration.getSelection("Test1").orElseThrow().getSelection()); - assertEquals( - Boolean.FALSE, - testConfiguration.getSelection("Test2").orElseThrow().getSelection()); - } - - @Test - public void testResetValuesShouldClearSelectionAttributes() { - - Configuration testConfiguration = configuration.clone(); - - // selection setup - testConfiguration.get("root").setManual(Boolean.TRUE); - testConfiguration.get("Test1").setAutomatic(Boolean.TRUE); - testConfiguration.get("Test2").setManual(Boolean.TRUE); - testConfiguration.get("Test3").setAutomatic(Boolean.FALSE); - testConfiguration.get("Test4").setManual(Boolean.FALSE); - - // check all SELECTED features in LinkedHashMap selectableFeatures - assertEquals(Boolean.TRUE, testConfiguration.get("root").getSelection()); - assertEquals(Boolean.TRUE, testConfiguration.get("Test1").getSelection()); - assertEquals(Boolean.TRUE, testConfiguration.get("Test2").getSelection()); - - // check all UNSELECTED features in LinkedHashMap selectableFeatures - assertEquals(Boolean.FALSE, testConfiguration.get("Test3").getSelection()); - assertEquals(Boolean.FALSE, testConfiguration.get("Test4").getSelection()); - - // check all UNDEFINED features in LinkedHashMap selectableFeatures - assertEquals(null, testConfiguration.get("Test5").getSelection()); - assertEquals(null, testConfiguration.get("Test6").getSelection()); - assertEquals(null, testConfiguration.get("Test7").getSelection()); - - // execute resetValues - testConfiguration.reset(); - - // selected, unselected and automatic features should be empty after resetValues() - assertTrue(testConfiguration.getManualFeatures().isEmpty()); - assertTrue(testConfiguration.getAutomaticFeatures().isEmpty()); - - assertEquals(null, testConfiguration.get("root").getSelection()); - assertEquals(null, testConfiguration.get("Test1").getSelection()); - assertEquals(null, testConfiguration.get("Test2").getSelection()); - assertEquals(null, testConfiguration.get("Test3").getSelection()); - assertEquals(null, testConfiguration.get("Test4").getSelection()); - assertEquals(null, testConfiguration.get("Test5").getSelection()); - assertEquals(null, testConfiguration.get("Test6").getSelection()); - assertEquals(null, testConfiguration.get("Test7").getSelection()); - } - - @Test - public void testResetAutomaticValuesShouldOnlyClearAutomaticAttributes() { - Configuration testConfiguration = configuration.clone(); - - testConfiguration.get("Test1").setAutomatic(Boolean.TRUE); - testConfiguration.get("Test2").setAutomatic(Boolean.FALSE); - testConfiguration.get("Test3").setManual(Boolean.TRUE); - testConfiguration.get("Test4").setManual(Boolean.FALSE); - - // execute resetAutomaticValues() - testConfiguration.resetAutomatic(); - - // there should be no automatic features anymore - assertTrue(testConfiguration.getAutomaticFeatures().isEmpty()); - - assertTrue(testConfiguration.get("Test1").getAutomatic() == null); - assertTrue(testConfiguration.get("Test2").getAutomatic() == null); - assertTrue(testConfiguration.get("Test3").getAutomatic() == null); - assertTrue(testConfiguration.get("Test4").getAutomatic() == null); - - assertTrue(testConfiguration.get("Test1").getManual() == null); - assertTrue(testConfiguration.get("Test2").getManual() == null); - assertTrue(testConfiguration.get("Test3").getManual() == Boolean.TRUE); - assertTrue(testConfiguration.get("Test4").getManual() == Boolean.FALSE); - } -} +/* +< * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.configuration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import de.featjar.base.data.identifier.Identifiers; +import de.featjar.feature.configuration.Configuration; +import de.featjar.feature.configuration.Configuration.Selection; +import de.featjar.feature.configuration.Configuration.SelectionNotPossibleException; +import de.featjar.feature.model.FeatureModel; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureTree; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.connective.And; +import de.featjar.formula.structure.connective.BiImplies; +import de.featjar.formula.structure.connective.Implies; +import de.featjar.formula.structure.connective.Not; +import de.featjar.formula.structure.connective.Or; +import de.featjar.formula.structure.predicate.Literal; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class ConfigurationTest { + + private static FeatureModel featureModel; + private static Configuration configuration; + + @BeforeAll + public static void setupTestConfiguration() { + + FeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + + // features + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAlternativeGroup(); + + IFeature childFeature1 = featureModel.mutate().addFeature("Test1"); + IFeatureTree childTree1 = rootTree.mutate().addFeatureBelow(childFeature1); + childTree1.mutate().toAlternativeGroup(); + + IFeature childFeature2 = featureModel.mutate().addFeature("Test2"); + IFeatureTree childTree2 = rootTree.mutate().addFeatureBelow(childFeature2); + childTree2.mutate().toOrGroup(); + + IFeature childFeature3 = featureModel.mutate().addFeature("Test3"); + IFeatureTree childTree3 = childTree1.mutate().addFeatureBelow(childFeature3); + + IFeature childFeature4 = featureModel.mutate().addFeature("Test4"); + childTree1.mutate().addFeatureBelow(childFeature4); + + IFeature childFeature5 = featureModel.mutate().addFeature("Test5"); + IFeatureTree childTree5 = childTree2.mutate().addFeatureBelow(childFeature5); + + IFeature childFeature6 = featureModel.mutate().addFeature("Test6"); + childTree2.mutate().addFeatureBelow(childFeature6); + + IFeature childFeature7 = featureModel.mutate().addFeature("Test7"); + IFeatureTree childTree7 = rootTree.mutate().addFeatureBelow(childFeature7); + childTree7.mutate().makeMandatory(); + + IFormula formula1 = new Or( + new And(new Literal("Test1"), new Literal("Test2")), + new BiImplies(new Literal("Test3"), new Literal("Test4")), + new Implies(new Literal("Test5"), new Literal("Test6")), + new Not(new Literal("Test7"))); + + // constraints + featureModel.mutate().addConstraint(formula1); + + ConfigurationTest.featureModel = featureModel; + ConfigurationTest.configuration = new Configuration(featureModel); + } + + @Test + public void testfeatureModelToConfigurationAsSet() { + for (IFeature feature : featureModel.getFeatures()) { + assertFalse( + configuration.getSelection(feature.getName().orElseThrow()).isEmpty()); + } + } + + @Test + public void testConfigurationCloning() { + + // configuration setup + FeatureModel featureModelForCloning = new FeatureModel(Identifiers.newCounterIdentifier()); + + // features + IFeatureTree rootTree = featureModelForCloning + .mutate() + .addFeatureTreeRoot(featureModelForCloning.mutate().addFeature("root")); + + IFeature childFeature1 = featureModelForCloning.mutate().addFeature("Test1"); + IFeatureTree childTree1 = rootTree.mutate().addFeatureBelow(childFeature1); + childTree1.mutate().toAlternativeGroup(); + + IFeature childFeature2 = featureModelForCloning.mutate().addFeature("Test2"); + IFeatureTree childTree2 = rootTree.mutate().addFeatureBelow(childFeature2); + childTree2.mutate().toOrGroup(); + + IFeature childFeature3 = featureModelForCloning.mutate().addFeature("Test3"); + IFeatureTree childTree3 = childTree1.mutate().addFeatureBelow(childFeature3); + + IFeature childFeature4 = featureModelForCloning.mutate().addFeature("Test4"); + childTree1.mutate().addFeatureBelow(childFeature4); + + IFeature childFeature5 = featureModelForCloning.mutate().addFeature("Test5"); + IFeatureTree childTree5 = childTree2.mutate().addFeatureBelow(childFeature5); + + IFeature childFeature6 = featureModelForCloning.mutate().addFeature("Test6"); + childTree2.mutate().addFeatureBelow(childFeature6); + + IFeature childFeature7 = featureModelForCloning.mutate().addFeature("Test7"); + IFeatureTree childTree7 = rootTree.mutate().addFeatureBelow(childFeature7); + childTree7.mutate().makeMandatory(); + + IFormula formula1 = new Or( + new And(new Literal("Test1"), new Literal("Test2")), + new BiImplies(new Literal("Test3"), new Literal("Test4")), + new Implies(new Literal("Test5"), new Literal("Test6")), + new Not(new Literal("Test7"))); + + // constraints + featureModelForCloning.mutate().addConstraint(formula1); + + Configuration configurationForCloning = new Configuration(featureModelForCloning); + + // selection setup + configurationForCloning.getSelection("root").orElseThrow().setManual(Boolean.TRUE); + configurationForCloning.getSelection("Test1").orElseThrow().setAutomatic(Boolean.TRUE); + configurationForCloning.getSelection("Test2").orElseThrow().setManual(Boolean.TRUE); + configurationForCloning.getSelection("Test3").orElseThrow().setAutomatic(Boolean.FALSE); + configurationForCloning.getSelection("Test4").orElseThrow().setManual(Boolean.FALSE); + + // execute clone() + Configuration clonedConfiguration = configurationForCloning.clone(); + + // check whether the selections of the clonedConfiguration are the same as for configurationForCloning + assertEquals( + Boolean.TRUE, + clonedConfiguration.getSelection("root").orElseThrow().getManual()); + assertEquals( + Boolean.TRUE, + clonedConfiguration.getSelection("Test1").orElseThrow().getAutomatic()); + assertEquals( + Boolean.TRUE, + clonedConfiguration.getSelection("Test2").orElseThrow().getManual()); + + assertEquals( + Boolean.FALSE, + clonedConfiguration.getSelection("Test3").orElseThrow().getAutomatic()); + assertEquals( + Boolean.FALSE, + clonedConfiguration.getSelection("Test4").orElseThrow().getManual()); + + assertEquals( + null, clonedConfiguration.getSelection("Test5").orElseThrow().getSelection()); + assertEquals( + null, clonedConfiguration.getSelection("Test6").orElseThrow().getSelection()); + assertEquals( + null, clonedConfiguration.getSelection("Test7").orElseThrow().getSelection()); + } + + @Test + public void testSelectionAttributesSetterAndGetterOfAutomaticAndManual() { + Selection testSelection = new Selection<>(Boolean.class); + + // Initial state + assertEquals(null, testSelection.getAutomatic()); + assertEquals(null, testSelection.getManual()); + + // Test manual selection to SELECTED + testSelection.setManual(Boolean.TRUE); + assertEquals(null, testSelection.getAutomatic()); + assertEquals(Boolean.TRUE, testSelection.getManual()); + assertThrows(SelectionNotPossibleException.class, () -> { + testSelection.setAutomatic(Boolean.FALSE); + }); + testSelection.setManual(null); + + // Test automatic selection to SELECTED + testSelection.setAutomatic(Boolean.TRUE); + assertEquals(Boolean.TRUE, testSelection.getAutomatic()); + assertEquals(null, testSelection.getManual()); + assertThrows(SelectionNotPossibleException.class, () -> { + testSelection.setManual(Boolean.FALSE); + }); + testSelection.setAutomatic(null); + + // Test manual selection to UNSELECTED + testSelection.setManual(Boolean.FALSE); + assertEquals(null, testSelection.getAutomatic()); + assertEquals(Boolean.FALSE, testSelection.getManual()); + assertThrows(SelectionNotPossibleException.class, () -> { + testSelection.setAutomatic(Boolean.TRUE); + }); + testSelection.setManual(null); + + // Test automatic selection to UNSELECTED + testSelection.setAutomatic(Boolean.FALSE); + assertEquals(Boolean.FALSE, testSelection.getAutomatic()); + assertEquals(null, testSelection.getManual()); + assertThrows(SelectionNotPossibleException.class, () -> { + testSelection.setManual(Boolean.TRUE); + }); + testSelection.setAutomatic(null); + + // Test setting both manual and automatic to SELECTED + testSelection.setManual(Boolean.TRUE); + testSelection.setAutomatic(Boolean.TRUE); + assertEquals(Boolean.TRUE, testSelection.getManual()); + assertEquals(Boolean.TRUE, testSelection.getAutomatic()); + testSelection.setManual(null); + testSelection.setAutomatic(null); + + // Test setting both manual and automatic to UNSELECTED + testSelection.setManual(Boolean.FALSE); + testSelection.setAutomatic(Boolean.FALSE); + assertEquals(Boolean.FALSE, testSelection.getManual()); + assertEquals(Boolean.FALSE, testSelection.getAutomatic()); + testSelection.setManual(null); + testSelection.setAutomatic(null); + } + + @Test + public void testSelectionAttributesSetterAndGetterOfAutomaticAndManualFromATestConfiguration() { + Configuration testConfiguration = configuration.clone(); + + assertEquals(null, testConfiguration.getSelection("Test1").orElseThrow().getAutomatic()); + assertEquals(null, testConfiguration.getSelection("Test2").orElseThrow().getManual()); + + testConfiguration.get("Test1").setAutomatic(Boolean.TRUE); + testConfiguration.get("Test2").setManual(Boolean.TRUE); + + assertEquals( + Boolean.TRUE, + testConfiguration.getSelection("Test1").orElseThrow().getAutomatic()); + assertEquals( + Boolean.TRUE, + testConfiguration.getSelection("Test2").orElseThrow().getManual()); + assertEquals( + Boolean.TRUE, + testConfiguration.getSelection("Test1").orElseThrow().getSelection()); + assertEquals( + Boolean.TRUE, + testConfiguration.getSelection("Test2").orElseThrow().getSelection()); + + testConfiguration.get("Test1").setAutomatic(Boolean.FALSE); + testConfiguration.get("Test2").setManual(Boolean.FALSE); + + assertEquals( + Boolean.FALSE, + testConfiguration.getSelection("Test1").orElseThrow().getAutomatic()); + assertEquals( + Boolean.FALSE, + testConfiguration.getSelection("Test2").orElseThrow().getManual()); + assertEquals( + Boolean.FALSE, + testConfiguration.getSelection("Test1").orElseThrow().getSelection()); + assertEquals( + Boolean.FALSE, + testConfiguration.getSelection("Test2").orElseThrow().getSelection()); + } + + @Test + public void testResetValuesShouldClearSelectionAttributes() { + + Configuration testConfiguration = configuration.clone(); + + // selection setup + testConfiguration.get("root").setManual(Boolean.TRUE); + testConfiguration.get("Test1").setAutomatic(Boolean.TRUE); + testConfiguration.get("Test2").setManual(Boolean.TRUE); + testConfiguration.get("Test3").setAutomatic(Boolean.FALSE); + testConfiguration.get("Test4").setManual(Boolean.FALSE); + + // check all SELECTED features in LinkedHashMap selectableFeatures + assertEquals(Boolean.TRUE, testConfiguration.get("root").getSelection()); + assertEquals(Boolean.TRUE, testConfiguration.get("Test1").getSelection()); + assertEquals(Boolean.TRUE, testConfiguration.get("Test2").getSelection()); + + // check all UNSELECTED features in LinkedHashMap selectableFeatures + assertEquals(Boolean.FALSE, testConfiguration.get("Test3").getSelection()); + assertEquals(Boolean.FALSE, testConfiguration.get("Test4").getSelection()); + + // check all UNDEFINED features in LinkedHashMap selectableFeatures + assertEquals(null, testConfiguration.get("Test5").getSelection()); + assertEquals(null, testConfiguration.get("Test6").getSelection()); + assertEquals(null, testConfiguration.get("Test7").getSelection()); + + // execute resetValues + testConfiguration.reset(); + + // selected, unselected and automatic features should be empty after resetValues() + assertTrue(testConfiguration.getManualFeatures().isEmpty()); + assertTrue(testConfiguration.getAutomaticFeatures().isEmpty()); + + assertEquals(null, testConfiguration.get("root").getSelection()); + assertEquals(null, testConfiguration.get("Test1").getSelection()); + assertEquals(null, testConfiguration.get("Test2").getSelection()); + assertEquals(null, testConfiguration.get("Test3").getSelection()); + assertEquals(null, testConfiguration.get("Test4").getSelection()); + assertEquals(null, testConfiguration.get("Test5").getSelection()); + assertEquals(null, testConfiguration.get("Test6").getSelection()); + assertEquals(null, testConfiguration.get("Test7").getSelection()); + } + + @Test + public void testResetAutomaticValuesShouldOnlyClearAutomaticAttributes() { + Configuration testConfiguration = configuration.clone(); + + testConfiguration.get("Test1").setAutomatic(Boolean.TRUE); + testConfiguration.get("Test2").setAutomatic(Boolean.FALSE); + testConfiguration.get("Test3").setManual(Boolean.TRUE); + testConfiguration.get("Test4").setManual(Boolean.FALSE); + + // execute resetAutomaticValues() + testConfiguration.resetAutomatic(); + + // there should be no automatic features anymore + assertTrue(testConfiguration.getAutomaticFeatures().isEmpty()); + + assertTrue(testConfiguration.get("Test1").getAutomatic() == null); + assertTrue(testConfiguration.get("Test2").getAutomatic() == null); + assertTrue(testConfiguration.get("Test3").getAutomatic() == null); + assertTrue(testConfiguration.get("Test4").getAutomatic() == null); + + assertTrue(testConfiguration.get("Test1").getManual() == null); + assertTrue(testConfiguration.get("Test2").getManual() == null); + assertTrue(testConfiguration.get("Test3").getManual() == Boolean.TRUE); + assertTrue(testConfiguration.get("Test4").getManual() == Boolean.FALSE); + } +} diff --git a/src/test/java/de/featjar/feature/model/io/GraphVizFeatureModelFormatTest.java b/src/test/java/de/featjar/feature/model/io/GraphVizFeatureModelFormatTest.java index 93094a4d..2e48d6a6 100644 --- a/src/test/java/de/featjar/feature/model/io/GraphVizFeatureModelFormatTest.java +++ b/src/test/java/de/featjar/feature/model/io/GraphVizFeatureModelFormatTest.java @@ -1,54 +1,54 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.io; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import de.featjar.Common; -import de.featjar.base.FeatJAR; -import de.featjar.base.io.IO; -import de.featjar.feature.model.IFeatureModel; -import de.featjar.feature.model.io.xml.GraphVizFeatureModelFormat; -import de.featjar.feature.model.io.xml.XMLFeatureModelFormat; -import java.io.IOException; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -public class GraphVizFeatureModelFormatTest extends Common { - - @BeforeAll - public static void begin() { - FeatJAR.testConfiguration().initialize(); - } - - @AfterAll - public static void end() { - FeatJAR.deinitialize(); - } - - @Test - public void graphVizFeatureModelFormat() throws IOException { - IFeatureModel featureModel = load("testFeatureModels/car.xml", new XMLFeatureModelFormat()); - String print = IO.print(featureModel, new GraphVizFeatureModelFormat()); - assertTrue(print.startsWith("digraph {")); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.io; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import de.featjar.Common; +import de.featjar.base.FeatJAR; +import de.featjar.base.io.IO; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.io.xml.GraphVizFeatureModelFormat; +import de.featjar.feature.model.io.xml.XMLFeatureModelFormat; +import java.io.IOException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class GraphVizFeatureModelFormatTest extends Common { + + @BeforeAll + public static void begin() { + FeatJAR.testConfiguration().initialize(); + } + + @AfterAll + public static void end() { + FeatJAR.deinitialize(); + } + + @Test + public void graphVizFeatureModelFormat() throws IOException { + IFeatureModel featureModel = load("testFeatureModels/car.xml", new XMLFeatureModelFormat()); + String print = IO.print(featureModel, new GraphVizFeatureModelFormat()); + assertTrue(print.startsWith("digraph {")); + } +} diff --git a/src/test/java/de/featjar/feature/model/io/XMLFeatureModelFormulaFormatTest.java b/src/test/java/de/featjar/feature/model/io/XMLFeatureModelFormulaFormatTest.java index 5a837088..44b1437f 100644 --- a/src/test/java/de/featjar/feature/model/io/XMLFeatureModelFormulaFormatTest.java +++ b/src/test/java/de/featjar/feature/model/io/XMLFeatureModelFormulaFormatTest.java @@ -1,159 +1,159 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.io; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import de.featjar.Common; -import de.featjar.base.data.Result; -import de.featjar.base.data.Sets; -import de.featjar.base.data.identifier.Identifiers; -import de.featjar.base.io.format.IFormat; -import de.featjar.base.io.input.StringInputMapper; -import de.featjar.feature.model.FeatureModel; -import de.featjar.feature.model.IFeature; -import de.featjar.feature.model.IFeatureModel; -import de.featjar.feature.model.IFeatureTree; -import de.featjar.feature.model.io.xml.XMLFeatureModelFormat; -import de.featjar.formula.structure.IFormula; -import de.featjar.formula.structure.connective.And; -import de.featjar.formula.structure.connective.BiImplies; -import de.featjar.formula.structure.connective.Implies; -import de.featjar.formula.structure.connective.Not; -import de.featjar.formula.structure.connective.Or; -import de.featjar.formula.structure.predicate.Literal; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.Objects; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -public class XMLFeatureModelFormulaFormatTest extends Common { - - private static FeatureModel featureModel; - - @BeforeAll - public static void setup() { - FeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); - - // features - IFeatureTree rootTree = - featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - rootTree.mutate().toAlternativeGroup(); - - IFeature childFeature1 = featureModel.mutate().addFeature("Test1"); - IFeatureTree childTree1 = rootTree.mutate().addFeatureBelow(childFeature1); - - IFeature childFeature2 = featureModel.mutate().addFeature("Test2"); - IFeatureTree childTree2 = rootTree.mutate().addFeatureBelow(childFeature2); - childTree2.mutate().toOrGroup(); - - IFeature childFeature3 = featureModel.mutate().addFeature("Test3"); - childTree1.mutate().addFeatureBelow(childFeature3); - - IFeature childFeature4 = featureModel.mutate().addFeature("Test4"); - childTree1.mutate().addFeatureBelow(childFeature4); - - IFeature childFeature5 = featureModel.mutate().addFeature("Test5"); - childTree2.mutate().addFeatureBelow(childFeature5); - - IFeature childFeature6 = featureModel.mutate().addFeature("Test6"); - childTree2.mutate().addFeatureBelow(childFeature6); - - IFeature childFeature7 = featureModel.mutate().addFeature("Test7"); - IFeatureTree childTree7 = rootTree.mutate().addFeatureBelow(childFeature7); - childTree7.mutate().makeMandatory(); - - IFormula formula1 = new Or( - new And(new Literal("Test1"), new Literal("Test2")), - new BiImplies(new Literal("Test3"), new Literal("Test4")), - new Implies(new Literal("Test5"), new Literal("Test6")), - new Not(new Literal("Test7"))); - - // constraints - featureModel.mutate().addConstraint(formula1); - - XMLFeatureModelFormulaFormatTest.featureModel = featureModel; - } - - @Test - public void xmlFeatureModelFormat() { - IFeatureModel featureModelResult = load("testFeatureModels/car.xml", new XMLFeatureModelFormat()); - IFeatureModel featureModel = featureModelResult; - String[] featureNames = new String[] { - "Car", - "Carbody", - "Radio", - "Ports", - "USB", - "CD", - "Navigation", - "DigitalCards", - "Europe", - "USA", - "GPSAntenna", - "Bluetooth", - "Gearbox", - "Manual", - "Automatic", - "GearboxTest" - }; - assertEquals( - Sets.of(featureNames), - featureModel.getFeatures().stream() - .map(IFeature::getName) - .map(Result::get) - .collect(Sets.toSet())); - } - - @Test - void testXMLFileToFeatureModelToXMLFile() throws IOException { - parseAndSerialize(new XMLFeatureModelFormat(), "testFeatureModels/car.xml"); - } - - private void parseAndSerialize(IFormat format, String path) { - IFeatureModel fm = Common.load(path, format); - - Result serializeResult = format.serialize(fm); - assertTrue(serializeResult.isPresent(), "Serialization failed"); - } - - // TODO: Need to assert objects.equals for each featuremodel.element instead of for the featuremodel itself. - - // @Test - void testFeatureModelToXMLStringToFeatureModel() throws IOException { - IFormat format = new XMLFeatureModelFormat(); - - Result serializedResult = format.serialize(featureModel); - Assertions.assertTrue(serializedResult.isPresent(), "Serialization of IFeatureModel failed"); - - String serializedFeatureModel = serializedResult.get(); - Result parseResult = - format.parse(new StringInputMapper(serializedFeatureModel, StandardCharsets.UTF_8, "xml")); - - Assertions.assertTrue(parseResult.isPresent(), parseResult.printProblems()); - IFeatureModel parsedFeatureModel = parseResult.get(); - - Assertions.assertTrue(Objects.equals(parsedFeatureModel, featureModel)); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.io; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import de.featjar.Common; +import de.featjar.base.data.Result; +import de.featjar.base.data.Sets; +import de.featjar.base.data.identifier.Identifiers; +import de.featjar.base.io.format.IFormat; +import de.featjar.base.io.input.StringInputMapper; +import de.featjar.feature.model.FeatureModel; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.IFeatureTree; +import de.featjar.feature.model.io.xml.XMLFeatureModelFormat; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.connective.And; +import de.featjar.formula.structure.connective.BiImplies; +import de.featjar.formula.structure.connective.Implies; +import de.featjar.formula.structure.connective.Not; +import de.featjar.formula.structure.connective.Or; +import de.featjar.formula.structure.predicate.Literal; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class XMLFeatureModelFormulaFormatTest extends Common { + + private static FeatureModel featureModel; + + @BeforeAll + public static void setup() { + FeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + + // features + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().toAlternativeGroup(); + + IFeature childFeature1 = featureModel.mutate().addFeature("Test1"); + IFeatureTree childTree1 = rootTree.mutate().addFeatureBelow(childFeature1); + + IFeature childFeature2 = featureModel.mutate().addFeature("Test2"); + IFeatureTree childTree2 = rootTree.mutate().addFeatureBelow(childFeature2); + childTree2.mutate().toOrGroup(); + + IFeature childFeature3 = featureModel.mutate().addFeature("Test3"); + childTree1.mutate().addFeatureBelow(childFeature3); + + IFeature childFeature4 = featureModel.mutate().addFeature("Test4"); + childTree1.mutate().addFeatureBelow(childFeature4); + + IFeature childFeature5 = featureModel.mutate().addFeature("Test5"); + childTree2.mutate().addFeatureBelow(childFeature5); + + IFeature childFeature6 = featureModel.mutate().addFeature("Test6"); + childTree2.mutate().addFeatureBelow(childFeature6); + + IFeature childFeature7 = featureModel.mutate().addFeature("Test7"); + IFeatureTree childTree7 = rootTree.mutate().addFeatureBelow(childFeature7); + childTree7.mutate().makeMandatory(); + + IFormula formula1 = new Or( + new And(new Literal("Test1"), new Literal("Test2")), + new BiImplies(new Literal("Test3"), new Literal("Test4")), + new Implies(new Literal("Test5"), new Literal("Test6")), + new Not(new Literal("Test7"))); + + // constraints + featureModel.mutate().addConstraint(formula1); + + XMLFeatureModelFormulaFormatTest.featureModel = featureModel; + } + + @Test + public void xmlFeatureModelFormat() { + IFeatureModel featureModelResult = load("testFeatureModels/car.xml", new XMLFeatureModelFormat()); + IFeatureModel featureModel = featureModelResult; + String[] featureNames = new String[] { + "Car", + "Carbody", + "Radio", + "Ports", + "USB", + "CD", + "Navigation", + "DigitalCards", + "Europe", + "USA", + "GPSAntenna", + "Bluetooth", + "Gearbox", + "Manual", + "Automatic", + "GearboxTest" + }; + assertEquals( + Sets.of(featureNames), + featureModel.getFeatures().stream() + .map(IFeature::getName) + .map(Result::get) + .collect(Sets.toSet())); + } + + @Test + void testXMLFileToFeatureModelToXMLFile() throws IOException { + parseAndSerialize(new XMLFeatureModelFormat(), "testFeatureModels/car.xml"); + } + + private void parseAndSerialize(IFormat format, String path) { + IFeatureModel fm = Common.load(path, format); + + Result serializeResult = format.serialize(fm); + assertTrue(serializeResult.isPresent(), "Serialization failed"); + } + + // TODO: Need to assert objects.equals for each featuremodel.element instead of for the featuremodel itself. + + // @Test + void testFeatureModelToXMLStringToFeatureModel() throws IOException { + IFormat format = new XMLFeatureModelFormat(); + + Result serializedResult = format.serialize(featureModel); + Assertions.assertTrue(serializedResult.isPresent(), "Serialization of IFeatureModel failed"); + + String serializedFeatureModel = serializedResult.get(); + Result parseResult = + format.parse(new StringInputMapper(serializedFeatureModel, StandardCharsets.UTF_8, "xml")); + + Assertions.assertTrue(parseResult.isPresent(), parseResult.printProblems()); + IFeatureModel parsedFeatureModel = parseResult.get(); + + Assertions.assertTrue(Objects.equals(parsedFeatureModel, featureModel)); + } +} diff --git a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java index 9345e503..0fda73f4 100644 --- a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java +++ b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java @@ -1,400 +1,424 @@ -/* - * Copyright (C) 2025 FeatJAR-Development-Team - * - * This file is part of FeatJAR-feature-model. - * - * feature-model is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3.0 of the License, - * or (at your option) any later version. - * - * feature-model is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - * See the GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with feature-model. If not, see . - * - * See for further information. - */ -package de.featjar.feature.model.transformer; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import java.util.Arrays; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import de.featjar.base.computation.ComputeConstant; -import de.featjar.base.data.Range; -import de.featjar.base.data.identifier.Identifiers; -import de.featjar.feature.model.FeatureModel; -import de.featjar.feature.model.IFeature; -import de.featjar.feature.model.IFeatureModel; -import de.featjar.feature.model.IFeatureTree; -import de.featjar.formula.structure.IFormula; -import de.featjar.formula.structure.connective.And; -import de.featjar.formula.structure.connective.Between; -import de.featjar.formula.structure.connective.Choose; -import de.featjar.formula.structure.connective.Implies; -import de.featjar.formula.structure.connective.Or; -import de.featjar.formula.structure.connective.Reference; -import de.featjar.formula.structure.predicate.Literal; - -class ComputeFormulaTest { - private IFeatureModel featureModel; - private IFormula expected; - - @BeforeEach - public void createFeatureModel() { - featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); - } - - @Test - void simpleWithTwoCardinalies() { - IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - rootTree.mutate().makeMandatory(); - rootTree.mutate().toAndGroup(); - - // create and set cardinality for the child feature - IFeature childFeature1 = featureModel.mutate().addFeature("A"); - IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); - childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); - - IFeature childFeature2 = featureModel.mutate().addFeature("B"); - IFeatureTree childFeature2Tree = childFeature1Tree.mutate().addFeatureBelow(childFeature2); - childFeature2Tree.mutate().setFeatureCardinality(Range.of(0, 2)); - - expected = new Reference(new And(new Literal("root"), new Implies(new Literal("A_1"), new Literal("root")), - new Implies(new Literal("A_2"), new Literal("root")), - new Implies(new Literal("A_2"), new Literal("A_1")), - - new Implies(new Literal("B_1"), new Literal("root")), - new Implies(new Literal("B_2"), new Literal("root")), - new Implies(new Literal("B_2"), new Literal("B_1")))); - - executeSimpleTest(); - } - - @Test - void withTwoCardinalies() { - IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - rootTree.mutate().makeMandatory(); - rootTree.mutate().toAndGroup(); - - // create and set cardinality for the child feature - IFeature childFeature1 = featureModel.mutate().addFeature("A"); - IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); - childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); - - IFeature childFeature2 = featureModel.mutate().addFeature("B"); - IFeatureTree childFeature2Tree = childFeature1Tree.mutate().addFeatureBelow(childFeature2); - childFeature2Tree.mutate().setFeatureCardinality(Range.of(0, 2)); - - expected = new Reference(new And(new Literal("root"), new Implies(new Literal("A_1"), new Literal("root")), - new Implies(new Literal("B_1.A_1"), new Literal("A_1")), - new Implies(new Literal("B_2.A_1"), new Literal("A_1")), - new Implies(new Literal("B_2.A_1"), new Literal("B_1.A_1")), - - new Implies(new Literal("A_2"), new Literal("root")), - new Implies(new Literal("A_2"), new Literal("A_1")), - new Implies(new Literal("B_1.A_2"), new Literal("A_2")), - new Implies(new Literal("B_2.A_2"), new Literal("A_2")), - new Implies(new Literal("B_2.A_2"), new Literal("B_1.A_2")))); - - executeTest(); - } - - @Test - void simpleWithCardinalityAndChildGroup() { - IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - rootTree.mutate().makeMandatory(); - rootTree.mutate().toAndGroup(); - - // create and set cardinality for the child feature - IFeature childFeature1 = featureModel.mutate().addFeature("A"); - IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); - childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); - - childFeature1Tree.mutate().toAlternativeGroup(); - - IFeature childFeature2 = featureModel.mutate().addFeature("B"); - childFeature1Tree.mutate().addFeatureBelow(childFeature2); - - IFeature childFeature3 = featureModel.mutate().addFeature("C"); - childFeature1Tree.mutate().addFeatureBelow(childFeature3); - - expected = new Reference(new And(new Literal("root"), new Implies(new Literal("A_1"), new Literal("root")), - new Implies(new Literal("A_2"), new Literal("root")), - new Implies(new Literal("A_2"), new Literal("A_1")), - - new Implies(new Literal("root"), new Choose(1, Arrays.asList(new Literal("B"), new Literal("C")))), - new Implies(new Literal("B"), new Literal("root")), - new Implies(new Literal("C"), new Literal("root")))); - - executeSimpleTest(); - } - - @Test - void withCardinalityAndChildInbetween() { - IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - rootTree.mutate().makeMandatory(); - rootTree.mutate().toAndGroup(); - - // create and set cardinality for the child feature - IFeature childFeature1 = featureModel.mutate().addFeature("A"); - IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); - childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); - - IFeature childFeature2 = featureModel.mutate().addFeature("B"); - IFeatureTree childFeature1Tree2 = childFeature1Tree.mutate().addFeatureBelow(childFeature2); - - IFeature childFeature3 = featureModel.mutate().addFeature("C"); - IFeatureTree childFeature1Tree3 = childFeature1Tree2.mutate().addFeatureBelow(childFeature3); - childFeature1Tree3.mutate().setFeatureCardinality(Range.of(0, 2)); - - expected = new Reference(new And(new Literal("root"), new Implies(new Literal("A_1"), new Literal("root")), - new Implies(new Literal("B.A_1"), new Literal("A_1")), - new Implies(new Literal("C_1.B.A_1"), new Literal("B.A_1")), - new Implies(new Literal("C_2.B.A_1"), new Literal("B.A_1")), - new Implies(new Literal("C_2.B.A_1"), new Literal("C_1.B.A_1")), - - new Implies(new Literal("A_2"), new Literal("root")), - new Implies(new Literal("A_2"), new Literal("A_1")), - new Implies(new Literal("B.A_2"), new Literal("A_2")), - new Implies(new Literal("C_1.B.A_2"), new Literal("B.A_2")), - new Implies(new Literal("C_2.B.A_2"), new Literal("B.A_2")), - new Implies(new Literal("C_2.B.A_2"), new Literal("C_1.B.A_2")))); - - executeTest(); - - } - - @Test - void withTwoGroups() { - IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - rootTree.mutate().makeMandatory(); - rootTree.mutate().toAndGroup(); - - IFeature childFeature1 = featureModel.mutate().addFeature("A"); - rootTree.mutate().addFeatureBelow(childFeature1); - - IFeature childFeature2 = featureModel.mutate().addFeature("B"); - rootTree.mutate().addFeatureBelow(childFeature2); - - rootTree.mutate().toAlternativeGroup(); - int orGroupId = rootTree.mutate().addOrGroup(); - - IFeature childFeature3 = featureModel.mutate().addFeature("C"); - IFeatureTree childFeatureTree3 = rootTree.mutate().addFeatureBelow(childFeature3); - childFeatureTree3.mutate().setParentGroupID(orGroupId); - - IFeature childFeature4 = featureModel.mutate().addFeature("D"); - IFeatureTree addFeatureBelow4 = rootTree.mutate().addFeatureBelow(childFeature4); - addFeatureBelow4.mutate().setParentGroupID(orGroupId); - - expected = new Reference(new And(new Literal("root"), - new Implies(new Literal("root"), new Choose(1, Arrays.asList(new Literal("A"), new Literal("B")))), - new Implies(new Literal("root"), new Or(Arrays.asList(new Literal("C"), new Literal("D")))), - - new Implies(new Literal("A"), new Literal("root")), new Implies(new Literal("B"), new Literal("root")), - new Implies(new Literal("C"), new Literal("root")), - new Implies(new Literal("D"), new Literal("root")))); - - executeTest(); - - } - - @Test - void withCardinalityAndChildGroup() { - IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - rootTree.mutate().makeMandatory(); - rootTree.mutate().toAndGroup(); - - // create and set cardinality for the child feature - IFeature childFeature1 = featureModel.mutate().addFeature("A"); - IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); - childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); - - childFeature1Tree.mutate().toAlternativeGroup(); - - IFeature childFeature2 = featureModel.mutate().addFeature("B"); - childFeature1Tree.mutate().addFeatureBelow(childFeature2); - - IFeature childFeature3 = featureModel.mutate().addFeature("C"); - childFeature1Tree.mutate().addFeatureBelow(childFeature3); - - expected = new Reference(new And(new Literal("root"), new Implies(new Literal("A_1"), new Literal("root")), - new Implies(new Literal("A_1"), - new Choose(1, Arrays.asList(new Literal("B.A_1"), new Literal("C.A_1")))), - new Implies(new Literal("B.A_1"), new Literal("A_1")), - new Implies(new Literal("C.A_1"), new Literal("A_1")), - - new Implies(new Literal("A_2"), new Literal("root")), - new Implies(new Literal("A_2"), new Literal("A_1")), - new Implies(new Literal("A_2"), - new Choose(1, Arrays.asList(new Literal("B.A_2"), new Literal("C.A_2")))), - new Implies(new Literal("B.A_2"), new Literal("A_2")), - new Implies(new Literal("C.A_2"), new Literal("A_2")))); - - executeTest(); - } - - @Test - void withCardinalityAndChildChildGroup() { - IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - rootTree.mutate().makeMandatory(); - rootTree.mutate().toAndGroup(); - - // create and set cardinality for the child feature - IFeature childFeature1 = featureModel.mutate().addFeature("A"); - IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); - childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); - - childFeature1Tree.mutate().toAlternativeGroup(); - - IFeature childFeature2 = featureModel.mutate().addFeature("B"); - childFeature1Tree.mutate().addFeatureBelow(childFeature2); - - IFeature childFeature3 = featureModel.mutate().addFeature("C"); - IFeatureTree childFeature1Tree2 = childFeature1Tree.mutate().addFeatureBelow(childFeature3); - - childFeature1Tree2.mutate().toOrGroup(); - - IFeature childFeature4 = featureModel.mutate().addFeature("D"); - childFeature1Tree2.mutate().addFeatureBelow(childFeature4); - - IFeature childFeature5 = featureModel.mutate().addFeature("E"); - childFeature1Tree2.mutate().addFeatureBelow(childFeature5); - - expected = new Reference(new And(new Literal("root"), new Implies(new Literal("A_1"), new Literal("root")), - new Implies(new Literal("A_1"), - new Choose(1, Arrays.asList(new Literal("B.A_1"), new Literal("C.A_1")))), - new Implies(new Literal("B.A_1"), new Literal("A_1")), - new Implies(new Literal("C.A_1"), new Literal("A_1")), - - // sub-subtree - new Implies(new Literal("C.A_1"), - new Or(Arrays.asList(new Literal("D.C.A_1"), new Literal("E.C.A_1")))), - new Implies(new Literal("D.C.A_1"), new Literal("C.A_1")), - new Implies(new Literal("E.C.A_1"), new Literal("C.A_1")), - - new Implies(new Literal("A_2"), new Literal("root")), - new Implies(new Literal("A_2"), new Literal("A_1")), - new Implies(new Literal("A_2"), - new Choose(1, Arrays.asList(new Literal("B.A_2"), new Literal("C.A_2")))), - new Implies(new Literal("B.A_2"), new Literal("A_2")), - new Implies(new Literal("C.A_2"), new Literal("A_2")), - - // second sub-subtree - new Implies(new Literal("C.A_2"), - new Or(Arrays.asList(new Literal("D.C.A_2"), new Literal("E.C.A_2")))), - new Implies(new Literal("D.C.A_2"), new Literal("C.A_2")), - new Implies(new Literal("E.C.A_2"), new Literal("C.A_2")))); - - executeTest(); - } - - @Test - void onlyRoot() { - - // root and nothing else - featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")).mutate().makeMandatory(); - - // root must be selected - expected = new Reference(new And(new Literal("root"))); - - executeTest(); - } - - @Test - void oneFeature() { - - // root - IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - rootTree.mutate().makeMandatory(); - rootTree.mutate().toAndGroup(); - - // create and add our only child - IFeature childFeature = featureModel.mutate().addFeature("Test1"); - rootTree.mutate().addFeatureBelow(childFeature); - - expected = new Reference(new And(new Literal("root"), new Implies(new Literal("Test1"), new Literal("root")))); - - executeTest(); - } - - @Test - void withCardinalityGroup() { - IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - rootTree.mutate().makeMandatory(); - rootTree.mutate().toCardinalityGroup(Range.of(2, 3)); - - // create and set cardinality for the child feature - IFeature childFeature1 = featureModel.mutate().addFeature("A"); - rootTree.mutate().addFeatureBelow(childFeature1); - - IFeature childFeature2 = featureModel.mutate().addFeature("B"); - rootTree.mutate().addFeatureBelow(childFeature2); - - IFeature childFeature3 = featureModel.mutate().addFeature("C"); - rootTree.mutate().addFeatureBelow(childFeature3); - - expected = new Reference(new And(new Literal("root"), - new Implies(new Literal("root"), - new Between(2, 3, Arrays.asList(new Literal("A"), new Literal("B"), new Literal("C")))), - new Implies(new Literal("A"), new Literal("root")), new Implies(new Literal("B"), new Literal("root")), - new Implies(new Literal("C"), new Literal("root")))); - - executeTest(); - } - - @Test - void withOneCardinalityFeature() { - IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - rootTree.mutate().makeMandatory(); - rootTree.mutate().toAndGroup(); - - // create and set cardinality for the child feature - IFeature childFeature = featureModel.mutate().addFeature("A"); - IFeatureTree childFeatureTree1 = rootTree.mutate().addFeatureBelow(childFeature); - childFeatureTree1.mutate().setFeatureCardinality(Range.of(0, 2)); - - // add normal feature below - IFeature childFeature2 = featureModel.mutate().addFeature("B"); - childFeatureTree1.mutate().addFeatureBelow(childFeature2); - - expected = new Reference(new And(new Literal("root"), new Implies(new Literal("A_1"), new Literal("root")), - new Implies(new Literal("B.A_1"), new Literal("A_1")), - new Implies(new Literal("A_2"), new Literal("root")), - new Implies(new Literal("A_2"), new Literal("A_1")), - new Implies(new Literal("B.A_2"), new Literal("A_2")))); - - executeTest(); - } - - private void executeTest() { - - ComputeConstant computeConstant = new ComputeConstant(featureModel); - ComputeFormula computeFormula = new ComputeFormula(computeConstant); - - IFormula resultFormula = computeFormula.computeResult().get(); - - // assert - assertEquals(expected, resultFormula); - } - - private void executeSimpleTest() { - - ComputeConstant computeConstant = new ComputeConstant(featureModel); - ComputeFormula computeFormula = new ComputeFormula(computeConstant); - - IFormula resultFormula = computeFormula.set(ComputeFormula.SIMPLE_TRANSLATION, Boolean.TRUE).computeResult() - .get(); - - // assert - assertEquals(expected, resultFormula); - } -} +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.transformer; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import de.featjar.base.computation.ComputeConstant; +import de.featjar.base.data.Range; +import de.featjar.base.data.identifier.Identifiers; +import de.featjar.feature.model.FeatureModel; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.IFeatureTree; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.connective.And; +import de.featjar.formula.structure.connective.Between; +import de.featjar.formula.structure.connective.Choose; +import de.featjar.formula.structure.connective.Implies; +import de.featjar.formula.structure.connective.Or; +import de.featjar.formula.structure.connective.Reference; +import de.featjar.formula.structure.predicate.Literal; +import java.util.Arrays; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ComputeFormulaTest { + private IFeatureModel featureModel; + private IFormula expected; + + @BeforeEach + public void createFeatureModel() { + featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + } + + @Test + void simpleWithTwoCardinalies() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + IFeatureTree childFeature2Tree = childFeature1Tree.mutate().addFeatureBelow(childFeature2); + childFeature2Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("B_1"), new Literal("root")), + new Implies(new Literal("B_2"), new Literal("root")), + new Implies(new Literal("B_2"), new Literal("B_1")))); + + executeSimpleTest(); + } + + @Test + void withTwoCardinalies() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + IFeatureTree childFeature2Tree = childFeature1Tree.mutate().addFeatureBelow(childFeature2); + childFeature2Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("B_1.A_1"), new Literal("A_1")), + new Implies(new Literal("B_2.A_1"), new Literal("A_1")), + new Implies(new Literal("B_2.A_1"), new Literal("B_1.A_1")), + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("B_1.A_2"), new Literal("A_2")), + new Implies(new Literal("B_2.A_2"), new Literal("A_2")), + new Implies(new Literal("B_2.A_2"), new Literal("B_1.A_2")))); + + executeTest(); + } + + @Test + void simpleWithCardinalityAndChildGroup() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + childFeature1Tree.mutate().toAlternativeGroup(); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + childFeature1Tree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + childFeature1Tree.mutate().addFeatureBelow(childFeature3); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("root"), new Choose(1, Arrays.asList(new Literal("B"), new Literal("C")))), + new Implies(new Literal("B"), new Literal("root")), + new Implies(new Literal("C"), new Literal("root")))); + + executeSimpleTest(); + } + + @Test + void withCardinalityAndChildInbetween() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + IFeatureTree childFeature1Tree2 = childFeature1Tree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + IFeatureTree childFeature1Tree3 = childFeature1Tree2.mutate().addFeatureBelow(childFeature3); + childFeature1Tree3.mutate().setFeatureCardinality(Range.of(0, 2)); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("B.A_1"), new Literal("A_1")), + new Implies(new Literal("C_1.B.A_1"), new Literal("B.A_1")), + new Implies(new Literal("C_2.B.A_1"), new Literal("B.A_1")), + new Implies(new Literal("C_2.B.A_1"), new Literal("C_1.B.A_1")), + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("B.A_2"), new Literal("A_2")), + new Implies(new Literal("C_1.B.A_2"), new Literal("B.A_2")), + new Implies(new Literal("C_2.B.A_2"), new Literal("B.A_2")), + new Implies(new Literal("C_2.B.A_2"), new Literal("C_1.B.A_2")))); + + executeTest(); + } + + @Test + void withTwoGroups() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + rootTree.mutate().addFeatureBelow(childFeature1); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + rootTree.mutate().addFeatureBelow(childFeature2); + + rootTree.mutate().toAlternativeGroup(); + int orGroupId = rootTree.mutate().addOrGroup(); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + IFeatureTree childFeatureTree3 = rootTree.mutate().addFeatureBelow(childFeature3); + childFeatureTree3.mutate().setParentGroupID(orGroupId); + + IFeature childFeature4 = featureModel.mutate().addFeature("D"); + IFeatureTree addFeatureBelow4 = rootTree.mutate().addFeatureBelow(childFeature4); + addFeatureBelow4.mutate().setParentGroupID(orGroupId); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("root"), new Choose(1, Arrays.asList(new Literal("A"), new Literal("B")))), + new Implies(new Literal("root"), new Or(Arrays.asList(new Literal("C"), new Literal("D")))), + new Implies(new Literal("A"), new Literal("root")), + new Implies(new Literal("B"), new Literal("root")), + new Implies(new Literal("C"), new Literal("root")), + new Implies(new Literal("D"), new Literal("root")))); + + executeTest(); + } + + @Test + void withCardinalityAndChildGroup() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + childFeature1Tree.mutate().toAlternativeGroup(); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + childFeature1Tree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + childFeature1Tree.mutate().addFeatureBelow(childFeature3); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("A_1"), new Literal("root")), + new Implies( + new Literal("A_1"), new Choose(1, Arrays.asList(new Literal("B.A_1"), new Literal("C.A_1")))), + new Implies(new Literal("B.A_1"), new Literal("A_1")), + new Implies(new Literal("C.A_1"), new Literal("A_1")), + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies( + new Literal("A_2"), new Choose(1, Arrays.asList(new Literal("B.A_2"), new Literal("C.A_2")))), + new Implies(new Literal("B.A_2"), new Literal("A_2")), + new Implies(new Literal("C.A_2"), new Literal("A_2")))); + + executeTest(); + } + + @Test + void withCardinalityAndChildChildGroup() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + childFeature1Tree.mutate().toAlternativeGroup(); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + childFeature1Tree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + IFeatureTree childFeature1Tree2 = childFeature1Tree.mutate().addFeatureBelow(childFeature3); + + childFeature1Tree2.mutate().toOrGroup(); + + IFeature childFeature4 = featureModel.mutate().addFeature("D"); + childFeature1Tree2.mutate().addFeatureBelow(childFeature4); + + IFeature childFeature5 = featureModel.mutate().addFeature("E"); + childFeature1Tree2.mutate().addFeatureBelow(childFeature5); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("A_1"), new Literal("root")), + new Implies( + new Literal("A_1"), new Choose(1, Arrays.asList(new Literal("B.A_1"), new Literal("C.A_1")))), + new Implies(new Literal("B.A_1"), new Literal("A_1")), + new Implies(new Literal("C.A_1"), new Literal("A_1")), + + // sub-subtree + new Implies( + new Literal("C.A_1"), new Or(Arrays.asList(new Literal("D.C.A_1"), new Literal("E.C.A_1")))), + new Implies(new Literal("D.C.A_1"), new Literal("C.A_1")), + new Implies(new Literal("E.C.A_1"), new Literal("C.A_1")), + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies( + new Literal("A_2"), new Choose(1, Arrays.asList(new Literal("B.A_2"), new Literal("C.A_2")))), + new Implies(new Literal("B.A_2"), new Literal("A_2")), + new Implies(new Literal("C.A_2"), new Literal("A_2")), + + // second sub-subtree + new Implies( + new Literal("C.A_2"), new Or(Arrays.asList(new Literal("D.C.A_2"), new Literal("E.C.A_2")))), + new Implies(new Literal("D.C.A_2"), new Literal("C.A_2")), + new Implies(new Literal("E.C.A_2"), new Literal("C.A_2")))); + + executeTest(); + } + + @Test + void onlyRoot() { + + // root and nothing else + featureModel + .mutate() + .addFeatureTreeRoot(featureModel.mutate().addFeature("root")) + .mutate() + .makeMandatory(); + + // root must be selected + expected = new Reference(new And(new Literal("root"))); + + executeTest(); + } + + @Test + void oneFeature() { + + // root + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and add our only child + IFeature childFeature = featureModel.mutate().addFeature("Test1"); + rootTree.mutate().addFeatureBelow(childFeature); + + expected = new Reference(new And(new Literal("root"), new Implies(new Literal("Test1"), new Literal("root")))); + + executeTest(); + } + + @Test + void withCardinalityGroup() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toCardinalityGroup(Range.of(2, 3)); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + rootTree.mutate().addFeatureBelow(childFeature1); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + rootTree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + rootTree.mutate().addFeatureBelow(childFeature3); + + expected = new Reference(new And( + new Literal("root"), + new Implies( + new Literal("root"), + new Between(2, 3, Arrays.asList(new Literal("A"), new Literal("B"), new Literal("C")))), + new Implies(new Literal("A"), new Literal("root")), + new Implies(new Literal("B"), new Literal("root")), + new Implies(new Literal("C"), new Literal("root")))); + + executeTest(); + } + + @Test + void withOneCardinalityFeature() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature = featureModel.mutate().addFeature("A"); + IFeatureTree childFeatureTree1 = rootTree.mutate().addFeatureBelow(childFeature); + childFeatureTree1.mutate().setFeatureCardinality(Range.of(0, 2)); + + // add normal feature below + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + childFeatureTree1.mutate().addFeatureBelow(childFeature2); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("B.A_1"), new Literal("A_1")), + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("B.A_2"), new Literal("A_2")))); + + executeTest(); + } + + private void executeTest() { + + ComputeConstant computeConstant = new ComputeConstant(featureModel); + ComputeFormula computeFormula = new ComputeFormula(computeConstant); + + IFormula resultFormula = computeFormula.computeResult().get(); + + // assert + assertEquals(expected, resultFormula); + } + + private void executeSimpleTest() { + + ComputeConstant computeConstant = new ComputeConstant(featureModel); + ComputeFormula computeFormula = new ComputeFormula(computeConstant); + + IFormula resultFormula = computeFormula + .set(ComputeFormula.SIMPLE_TRANSLATION, Boolean.TRUE) + .computeResult() + .get(); + + // assert + assertEquals(expected, resultFormula); + } +} From 660aad7f8ccef608c4077a92cfa8401d36363e41 Mon Sep 17 00:00:00 2001 From: Lara Date: Thu, 9 Oct 2025 13:12:01 +0200 Subject: [PATCH 28/67] feat: added test --- .../io/tikz/FeatureModelDisplayTikzTest.java | 120 ++++++++++++++++++ .../feature/model/io/tikz/HeadLoaderTest.java | 21 ++- 2 files changed, 130 insertions(+), 11 deletions(-) create mode 100644 src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java diff --git a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java new file mode 100644 index 00000000..f72d6225 --- /dev/null +++ b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java @@ -0,0 +1,120 @@ +package de.featjar.feature.model.io.tikz; + +import de.featjar.base.FeatJAR; +import de.featjar.base.data.Range; +import de.featjar.base.data.identifier.Identifiers; +import de.featjar.feature.model.*; +import de.featjar.formula.structure.connective.*; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ +public class FeatureModelDisplayTikzTest { + + private final StringBuilder stringBuilder = new StringBuilder(); + private static IFeatureModel featureModel; + + @BeforeAll + public static void init() { + FeatJAR.testConfiguration().initialize(); + + FeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + + // First Tree + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("Hello")); + rootTree.mutate().toAndGroup(); + + IFeature feature = featureModel.mutate().addFeature("Feature"); + IFeatureTree firstFeatureTree = rootTree.mutate().addFeatureBelow(feature); + + IFeature world = featureModel.mutate().addFeature("World"); + rootTree.mutate().addFeatureBelow(world); + + IFeature wonderful = featureModel.addFeature("Wonderful"); + firstFeatureTree.mutate().addFeatureBelow(wonderful); + + IFeature beautiful = featureModel.addFeature("Beautiful"); + firstFeatureTree.mutate().addFeatureBelow(beautiful); + + // Second Tree + + IFeatureTree rootTreeSec = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("Hello2")); + rootTree.mutate().toAndGroup(); + + IFeature feature2 = featureModel.mutate().addFeature("Feature2"); + rootTreeSec.mutate().addFeatureBelow(feature2); + + IFeature world2 = featureModel.mutate().addFeature("World2"); + IFeatureTree firstFeatureTree2 = rootTreeSec.mutate().addFeatureBelow(world2); + + IFeature wonderful2 = featureModel.addFeature("Wonderful2"); + firstFeatureTree2.mutate().addFeatureBelow(wonderful2); + + IFeature beautiful2 = featureModel.addFeature("Beautiful2"); + firstFeatureTree2.mutate().addFeatureBelow(beautiful2); + + // Thrid Tree + + IFeatureTree rootTree3 = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree3.mutate().toAndGroup(); + + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree3.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + childFeature1Tree.mutate().toAlternativeGroup(); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + childFeature1Tree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + childFeature1Tree.mutate().addFeatureBelow(childFeature3); + + FeatureModelDisplayTikzTest.featureModel = featureModel; + } + + @Test + public void perform() { + new TikzGraphicalFeatureModelFormat(featureModel, false).serialize().ifPresent(s -> { + FeatJAR.log().info(s); + }); + } + + /** + * Test for the "synatx" mechanic + */ + + public void displayTikz() { + featureModel.getFeatureModel().getRoots().forEach(iFeatureTree -> { + stringBuilder.append("[").append(iFeatureTree.getFeature().getName().get()); + for (IFeatureTree featureTreeChildren : iFeatureTree.getChildren()) { + subTreeRecursiv(featureTreeChildren); + } + }); + + stringBuilder.append("]"); + FeatJAR.log().info(stringBuilder); + + String expectedOutput = "[Hello[Feature[Wonderful][Beautiful]][World]]"; + + Assertions.assertEquals(expectedOutput, + stringBuilder.toString(), "Expected: " + expectedOutput + System.lineSeparator() + + "Output: " + stringBuilder); + } + + private void subTreeRecursiv(IFeatureTree featureTree) { + stringBuilder.append("[").append(featureTree.getFeature().getName().get()); + for (IFeatureTree featureTreeChildren : featureTree.getChildren()) { + subTreeRecursiv(featureTreeChildren); + } + stringBuilder.append("]"); + } + +} diff --git a/src/test/java/de/featjar/feature/model/io/tikz/HeadLoaderTest.java b/src/test/java/de/featjar/feature/model/io/tikz/HeadLoaderTest.java index 0e7d0479..79ea96fe 100644 --- a/src/test/java/de/featjar/feature/model/io/tikz/HeadLoaderTest.java +++ b/src/test/java/de/featjar/feature/model/io/tikz/HeadLoaderTest.java @@ -1,25 +1,24 @@ package de.featjar.feature.model.io.tikz; +import de.featjar.feature.model.io.tikz.format.TikzHeadFormat; import org.junit.jupiter.api.Test; -import java.io.*; +import java.util.Collections; + +/** + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ public class HeadLoaderTest { @Test public void loadHead() { StringBuilder stringBuilder = new StringBuilder(); - InputStream inputStream = getClass().getClassLoader().getResourceAsStream("head.tex"); - try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"))) { - String line; - while (((line = bufferedReader.readLine()) != null)) { - stringBuilder.append(line).append("\n"); - } - } catch (IOException e) { - throw new RuntimeException(e); - } + TikzHeadFormat.header(stringBuilder, Collections.emptyList(), false); - System.out.print(stringBuilder.toString()); + System.out.print(stringBuilder); } } From 3a241d5d94875adf0ba34a80b34eb681e58176de Mon Sep 17 00:00:00 2001 From: Lara Date: Thu, 9 Oct 2025 13:13:43 +0200 Subject: [PATCH 29/67] changes: removed unused mehtods --- .../io/tikz/format/IGraphicalFormat.java | 10 -- .../model/io/tikz/format/TikzBodyFormat.java | 24 ---- .../model/io/tikz/format/TikzHeadFormat.java | 41 +++++-- .../model/io/tikz/format/TikzMainFormat.java | 111 +++++++----------- 4 files changed, 73 insertions(+), 113 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/IGraphicalFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/IGraphicalFormat.java index 66189124..d1812ed0 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/IGraphicalFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/IGraphicalFormat.java @@ -4,14 +4,4 @@ public interface IGraphicalFormat { void write(); - boolean supportWirte(); - - boolean supportRead(); - - String getSuffix(); - - String getName(); - - String getId(); - } diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzBodyFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzBodyFormat.java index 731d0c30..6df82947 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzBodyFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzBodyFormat.java @@ -28,28 +28,4 @@ public void write() { stringBuilder.append("\\end{document}"); } - @Override - public boolean supportWirte() { - return true; - } - - @Override - public boolean supportRead() { - return false; - } - - @Override - public String getSuffix() { - return ".tex"; - } - - @Override - public String getName() { - return "LaTeX-Document with TikZ"; - } - - @Override - public String getId() { - return ""; - } } diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzHeadFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzHeadFormat.java index 6bc80bc1..f6fbc729 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzHeadFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzHeadFormat.java @@ -1,21 +1,42 @@ package de.featjar.feature.model.io.tikz.format; +import de.featjar.base.data.Problem; import java.io.*; -import java.util.Objects; +import java.nio.charset.StandardCharsets; +import java.util.List; public class TikzHeadFormat { - public static void header(StringBuilder stringBuilder) { - try (BufferedReader bufferedReader = new BufferedReader( - new InputStreamReader( - Objects.requireNonNull(TikzHeadFormat.class - .getClassLoader() - .getResourceAsStream("head.tlx"))))) { + public static void header(StringBuilder stringBuilder, List problemList, boolean hasVerticalLayout) { + String replacement = String.format( // + " parent anchor = %s," + System.lineSeparator() // + + " child anchor = %s," + System.lineSeparator() // + + "%s" // + + " l sep = 2em," + System.lineSeparator() // + + " s sep = 1em," // + + "%s", // + hasVerticalLayout ? "east" : "south", // + hasVerticalLayout ? "west" : "north", // + hasVerticalLayout ? " grow' = east," + System.lineSeparator() : "", // + hasVerticalLayout ? " tier/.pgfmath=level()," : ""); - bufferedReader.lines() - .forEach(line -> stringBuilder.append(line).append("\n")); + InputStream inputStream = TikzHeadFormat.class.getClassLoader().getResourceAsStream("head.tex"); + + if (inputStream == null) { + problemList.add(new Problem("InputStream in header is null / not found")); + return; + } + + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + String line; + while (((line = bufferedReader.readLine()) != null)) { + if (line.contains("{replaceWithVerticalSetting}")) { + line = replacement; + } + stringBuilder.append(line).append("\n"); + } } catch (IOException e) { - e.printStackTrace(); + throw new RuntimeException(e); } } } diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java index 4d73817a..1a689c42 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java @@ -1,26 +1,18 @@ package de.featjar.feature.model.io.tikz.format; -import de.featjar.base.data.Problem; -import de.featjar.feature.model.IConstraint; -import de.featjar.feature.model.IFeature; -import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.IFeatureTree; import de.featjar.feature.model.io.tikz.TikzGraphicalFeatureModelFormat; -import java.util.HashSet; -import java.util.List; - public class TikzMainFormat implements IGraphicalFormat{ private final boolean[] LEGEND = new boolean[7]; - private final IFeatureModel featureModel; + private final IFeatureTree featureTree; private final StringBuilder stringBuilder; - private final List problemList; - public TikzMainFormat(IFeatureModel featureModel, StringBuilder stringBuilder, List problemList) { - this.featureModel = featureModel; + public TikzMainFormat(IFeatureTree featureTree, StringBuilder stringBuilder) { + this.featureTree = featureTree; this.stringBuilder = stringBuilder; - this.problemList = problemList; } @Override @@ -28,32 +20,7 @@ public void write() { printForest(); } - @Override - public boolean supportWirte() { - return true; - } - - @Override - public boolean supportRead() { - return false; - } - - @Override - public String getSuffix() { - return ".tex"; - } - - @Override - public String getName() { - return "LaTeX-Document with TikZ"; - } - - @Override - public String getId() { - return ""; - } - - private void insertNodeHead(String featureName) { + /*private void insertNodeHead(String featureName) { stringBuilder.append("[").append(featureName); IFeature feature = featureModel.getFeature(featureName).orElse(null); @@ -82,43 +49,49 @@ private void insertNodeHead(String featureName) { } } } + */ - private boolean isRootFeature(IFeature feature) { - return featureModel.getRootFeatures().contains(feature); - } - - private void insertNodeTail() { + /** + * A Feature is allowed to have a tree. This method checks the children and add them to the StringBuilder + * in LateX (.tex) style. + * + * @param featureTree (the part tree of the feature) + */ + private void printTree(IFeatureTree featureTree) { + stringBuilder.append("[").append(featureTree.getFeature().getName().get()); + for (IFeatureTree featureTreeChildren : featureTree.getChildren()) { + printTree(featureTreeChildren); + } stringBuilder.append("]"); } - private void printTree() { - - } - - private int countFeatures() { - return featureModel.getFeatures().size(); - //return -1; // todo: change later - } - - private String getRoot() { - return "-1"; // todo: change later - } - - public void printForest() { - stringBuilder.append("\\begin{forest}" + TikzGraphicalFeatureModelFormat.LINE_SEPERATOR + "\tfeatureDiagram" + TikzGraphicalFeatureModelFormat.LINE_SEPERATOR + "\t"); - //printTree(getRoot(object), object, treeStringBuilder); - //postProcessing(treeStringBuilder); + /** + * Build the complete tree of the FeatureModel. + */ + private void printForest() { + stringBuilder.append("\\begin{forest}").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append("\tfeatureDiagram").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append("\t"); + stringBuilder.append("[").append(featureTree.getFeature().getName().get()); + for (IFeatureTree featureTreeChildren : featureTree.getChildren()) { + printTree(featureTreeChildren); + } + stringBuilder.append("]"); + postProcessing(); stringBuilder.append("\t").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); /*if (!object.isLegendHidden()) { printLegend(str, object); - } - - */ + }*/ //printConstraints(str, object); - stringBuilder.append("\\end{forest}"); + stringBuilder.append("\\end{forest}").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); } - private void printLegend() { + /** + * Processes a String to make special symbols LaTeX compatible. + */ + private void postProcessing() { + stringBuilder.replace(0, stringBuilder.length(), stringBuilder.toString().replace("_", "\\_")); + } + + /* private void printLegend() { boolean check = false; final StringBuilder sb = new StringBuilder(); if (LEGEND[0] && LEGEND[1]) { @@ -182,7 +155,7 @@ private void printLegend() { } } - */ + if (check) { stringBuilder.append(" \\matrix [anchor=north west] at (current bounding box.north east) {").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\node [placeholder] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" };").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\matrix [draw=drawColor,anchor=north west] at (current bounding box.north east) {").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\node [label=center:\\underline{Legend:}] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); @@ -196,8 +169,9 @@ private void printLegend() { check = false; } } + */ - private void printConstraints() { + /*private void printConstraints() { stringBuilder.append(" \\matrix [below=1mm of current bounding box] {").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); for (IConstraint constraint : featureModel.getConstraints()) { String text = constraint.getFormula().print(); @@ -213,8 +187,7 @@ private void printConstraints() { str.append(" \\node {\\(" + text + "\\)}; \\\\" + lnSep); } str.append(" };" + lnSep); - - */ } + */ } From d6875c0f4fb452156cebe24108bc1134cdf19bb0 Mon Sep 17 00:00:00 2001 From: Lara Date: Thu, 9 Oct 2025 13:14:26 +0200 Subject: [PATCH 30/67] changes: added authors --- .../de/featjar/feature/model/io/tikz/color/FeatureColor.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/de/featjar/feature/model/io/tikz/color/FeatureColor.java b/src/main/java/de/featjar/feature/model/io/tikz/color/FeatureColor.java index 627d9ede..e08c38c3 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/color/FeatureColor.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/color/FeatureColor.java @@ -1,5 +1,10 @@ package de.featjar.feature.model.io.tikz.color; +/** + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ public enum FeatureColor { RED("redColor"), From d422cf69ba51de724014d27fff3f8cf72d2440fe Mon Sep 17 00:00:00 2001 From: Lara Date: Thu, 9 Oct 2025 13:15:40 +0200 Subject: [PATCH 31/67] feat: added working serializer --- .../tikz/TikzGraphicalFeatureModelFormat.java | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java index d20faa0e..ee355140 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java @@ -3,15 +3,22 @@ import de.featjar.base.data.Problem; import de.featjar.base.data.Result; import de.featjar.base.io.format.IFormat; -import de.featjar.base.io.format.ParseException; -import de.featjar.feature.model.IFeature; import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.IFeatureTree; import de.featjar.feature.model.io.tikz.format.TikzHeadFormat; import de.featjar.feature.model.io.tikz.format.TikzMainFormat; import java.util.ArrayList; import java.util.List; +/** + * This class is moved from FeatureIDE to FeatJAR. The former class was written by Simon Wenk and Yang Liu. + * We did some changes, moved in different classes, and we rewrote some code and added new functions. + * + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ public class TikzGraphicalFeatureModelFormat implements IFormat { public static String LINE_SEPERATOR = System.lineSeparator(); @@ -20,33 +27,33 @@ public class TikzGraphicalFeatureModelFormat implements IFormat { private final StringBuilder stringBuilder; private final IFeatureModel featureModel; + private final boolean hasVerticalLayout; private final List problemList; - public TikzGraphicalFeatureModelFormat(IFeatureModel featureModel) { + public TikzGraphicalFeatureModelFormat(IFeatureModel featureModel, boolean hasVerticalLayout) { this.stringBuilder = new StringBuilder(); this.featureModel = featureModel; this.problemList = new ArrayList<>(); + this.hasVerticalLayout = hasVerticalLayout; } public Result serialize() { stringBuilder.append("\\documentclass[border=5pt]{standalone}"); stringBuilder.append(LINE_SEPERATOR); - TikzHeadFormat.header(stringBuilder); - stringBuilder.append("\\begin{document}" + LINE_SEPERATOR + " %---The Feature Diagram-----------------------------------------------------" + LINE_SEPERATOR); - new TikzMainFormat(featureModel, stringBuilder, problemList).printForest(); + TikzHeadFormat.header(stringBuilder, problemList, hasVerticalLayout); + stringBuilder.append("\\begin{document}").append(LINE_SEPERATOR).append(" %---The Feature Diagram-----------------------------------------------------").append(LINE_SEPERATOR); + for (IFeatureTree featureTree : featureModel.getRoots()) { + new TikzMainFormat(featureTree, stringBuilder).write(); + } stringBuilder.append(LINE_SEPERATOR); - stringBuilder.append("\t%---------------------------------------------------------------------------" + LINE_SEPERATOR + "\\end{document}"); - return Result.of(stringBuilder.toString()); + stringBuilder.append("\t%---------------------------------------------------------------------------").append(LINE_SEPERATOR).append("\\end{document}"); + return Result.of(stringBuilder.toString(), problemList); } public IFeatureModel getFeatureModel() { return featureModel; } - public StringBuilder getStringBuilder() { - return stringBuilder; - } - @Override public String getFileExtension() { return ".tex"; From b9e994133417717ff16486edd85ba38da6e1e58f Mon Sep 17 00:00:00 2001 From: Lara Date: Thu, 9 Oct 2025 16:02:16 +0200 Subject: [PATCH 32/67] change: placeholder for replace in the process --- src/main/resources/head.tex | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/resources/head.tex b/src/main/resources/head.tex index dbba6b0e..c7f2613f 100644 --- a/src/main/resources/head.tex +++ b/src/main/resources/head.tex @@ -27,10 +27,7 @@ text depth = 0, draw = drawColor, edge = {draw=drawColor}, - parent anchor = south, - child anchor = north, - l sep = 2em, - s sep = 1em, + {replaceWithVerticalSetting} } }, /tikz/redColor/.style={ From 4a5a14aed986fe4c4f6513ced0189a09244f2886 Mon Sep 17 00:00:00 2001 From: Lara Date: Thu, 9 Oct 2025 16:39:25 +0200 Subject: [PATCH 33/67] change: small changes - start legend --- .../tikz/TikzGraphicalFeatureModelFormat.java | 2 +- .../model/io/tikz/format/TikzMainFormat.java | 143 +++++++----------- .../io/tikz/FeatureModelDisplayTikzTest.java | 6 +- 3 files changed, 59 insertions(+), 92 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java index ee355140..932c9caa 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java @@ -43,7 +43,7 @@ public Result serialize() { TikzHeadFormat.header(stringBuilder, problemList, hasVerticalLayout); stringBuilder.append("\\begin{document}").append(LINE_SEPERATOR).append(" %---The Feature Diagram-----------------------------------------------------").append(LINE_SEPERATOR); for (IFeatureTree featureTree : featureModel.getRoots()) { - new TikzMainFormat(featureTree, stringBuilder).write(); + new TikzMainFormat(featureModel, featureTree, stringBuilder).write(); } stringBuilder.append(LINE_SEPERATOR); stringBuilder.append("\t%---------------------------------------------------------------------------").append(LINE_SEPERATOR).append("\\end{document}"); diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java index 1a689c42..2268a1d1 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java @@ -1,5 +1,7 @@ package de.featjar.feature.model.io.tikz.format; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureModel; import de.featjar.feature.model.IFeatureTree; import de.featjar.feature.model.io.tikz.TikzGraphicalFeatureModelFormat; @@ -7,10 +9,12 @@ public class TikzMainFormat implements IGraphicalFormat{ private final boolean[] LEGEND = new boolean[7]; + private final IFeatureModel featureModel; private final IFeatureTree featureTree; private final StringBuilder stringBuilder; - public TikzMainFormat(IFeatureTree featureTree, StringBuilder stringBuilder) { + public TikzMainFormat(IFeatureModel featureModel ,IFeatureTree featureTree, StringBuilder stringBuilder) { + this.featureModel = featureModel; this.featureTree = featureTree; this.stringBuilder = stringBuilder; } @@ -20,15 +24,7 @@ public void write() { printForest(); } - /*private void insertNodeHead(String featureName) { - stringBuilder.append("[").append(featureName); - IFeature feature = featureModel.getFeature(featureName).orElse(null); - - if (feature == null) { - problemList.add(new Problem("The feature " + featureName + " is null")); - return; - } - + private void insertNodeHead(IFeature feature) { if (feature.isAbstract()) { stringBuilder.append(",abstract"); LEGEND[0] = true; @@ -48,8 +44,24 @@ public void write() { LEGEND[3] = true; } } + + if (!isRootFeature(feature)) { + if (feature.getFeatureTree().get().getParentGroup().get().isOr()) { + stringBuilder.append(",or"); + LEGEND[4] = true; + } + } + if (!isRootFeature(feature)) { + if (feature.getFeatureTree().get().getParentGroup().get().isAlternative()) { + stringBuilder.append(",alternative"); + LEGEND[5] = true; + } + } + } + + private boolean isRootFeature(IFeature feature) { + return featureModel.getRootFeatures().contains(feature); } - */ /** * A Feature is allowed to have a tree. This method checks the children and add them to the StringBuilder @@ -58,7 +70,10 @@ public void write() { * @param featureTree (the part tree of the feature) */ private void printTree(IFeatureTree featureTree) { - stringBuilder.append("[").append(featureTree.getFeature().getName().get()); + IFeature feature = featureTree.getFeature(); + //insertNodeHead(feature.getName().get()); + stringBuilder.append("[").append(feature.getName().get()); + insertNodeHead(feature); for (IFeatureTree featureTreeChildren : featureTree.getChildren()) { printTree(featureTreeChildren); } @@ -69,17 +84,20 @@ private void printTree(IFeatureTree featureTree) { * Build the complete tree of the FeatureModel. */ private void printForest() { + IFeature feature = featureTree.getFeature(); + stringBuilder.append("\\begin{forest}").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append("\tfeatureDiagram").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append("\t"); - stringBuilder.append("[").append(featureTree.getFeature().getName().get()); + stringBuilder.append("[").append(feature.getName().get()); + insertNodeHead(feature); for (IFeatureTree featureTreeChildren : featureTree.getChildren()) { printTree(featureTreeChildren); } stringBuilder.append("]"); postProcessing(); stringBuilder.append("\t").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); - /*if (!object.isLegendHidden()) { - printLegend(str, object); - }*/ + if (!featureTree.getFeature().isHidden()) { // todo: fix error + printLegend(); // todo + } // todo //printConstraints(str, object); stringBuilder.append("\\end{forest}").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); } @@ -91,85 +109,32 @@ private void postProcessing() { stringBuilder.replace(0, stringBuilder.length(), stringBuilder.toString().replace("_", "\\_")); } - /* private void printLegend() { - boolean check = false; - final StringBuilder sb = new StringBuilder(); - if (LEGEND[0] && LEGEND[1]) { - check = true; - sb.append(" \\node [abstract,label=right:Abstract Feature] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); - LEGEND[0] = false; - sb.append(" \\node [concrete,label=right:Concrete Feature] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); - LEGEND[1] = false; - } - if (LEGEND[0]) { - check = true; - sb.append(" \\node [abstract,label=right:Feature] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); - LEGEND[0] = false; - } - if (LEGEND[1]) { - check = true; - sb.append(" \\node [concrete,label=right:Feature] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); - LEGEND[1] = false; - } - if (LEGEND[2]) { - check = true; - sb.append(" \\node [mandatory,label=right:Mandatory] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); - LEGEND[2] = false; - } - if (LEGEND[3]) { - check = true; - sb.append(" \\node [optional,label=right:Optional] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); - LEGEND[3] = false; - } - if (LEGEND[4]) { - check = true; - // myString.append(" \\filldraw[drawColor] (0.45,0.15) ++ (225:0.3) arc[start angle=315,end angle=225,radius=0.2]; " + lnSep - // + " \\node [or,label=right:Or] {}; \\\\" + lnSep); - sb.append(" \\filldraw[drawColor] (0.1,0) - +(-0,-0.2) - +(0.2,-0.2)- +(0.1,0);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\draw[drawColor] (0.1,0) -- +(-0.2, -0.4);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\draw[drawColor] (0.1,0) -- +(0.2,-0.4);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\fill[drawColor] (0,-0.2) arc (240:300:0.2);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\node [or,label=right:Or Group] {}; \\\\"); - LEGEND[4] = false; - } - if (LEGEND[5]) { - check = true; - // myString.append(" \\draw[drawColor] (0.45,0.15) ++ (225:0.3) arc[start angle=315,end angle=225,radius=0.2] -- cycle; " + lnSep - // + " \\node [alternative,label=right:Alternative] {}; \\\\" + lnSep); - sb.append(" \\draw[drawColor] (0.1,0) -- +(-0.2, -0.4);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\draw[drawColor] (0.1,0) -- +(0.2,-0.4);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\draw[drawColor] (0,-0.2) arc (240:300:0.2);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\node [alternative,label=right:Alternative Group] {}; \\\\"); - LEGEND[5] = false; - } - if (LEGEND[6]) { - check = true; - sb.append(" \\node [hiddenNodes,label=center:1,label=right:Collapsed Nodes] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); - LEGEND[6] = false; - } -/*Color - final ColorScheme colorScheme = FeatureColorManager.getCurrentColorScheme(graphicalFeatureModel.getFeatureModelManager().getSnapshot()); - int colorIndex = 1; - - for (final FeatureColor currentColor : new HashSet<>(colorScheme.getColors().values())) { - if (currentColor != FeatureColor.NO_COLOR) { - String meaning = currentColor.getMeaning(); - if (meaning.isEmpty()) { - meaning = "Custom Color " + String.format("%02d", colorIndex); - colorIndex++; - } - sb.append(" \\node [" + featureColorToTikzStyle(currentColor) + ",label=right:" + meaning + "] {}; \\\\" + lnSep); - } - } + private void printLegend() { + + stringBuilder.append(" \\node [abstract,label=right:Abstract Feature] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + + stringBuilder.append(" \\node [concrete,label=right:Concrete Feature] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + + stringBuilder.append(" \\node [abstract,label=right:Feature] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + + stringBuilder.append(" \\node [concrete,label=right:Feature] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + stringBuilder.append(" \\node [mandatory,label=right:Mandatory] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + stringBuilder.append(" \\node [optional,label=right:Optional] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + + stringBuilder.append(" \\filldraw[drawColor] (0.1,0) - +(-0,-0.2) - +(0.2,-0.2)- +(0.1,0);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\draw[drawColor] (0.1,0) -- +(-0.2, -0.4);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\draw[drawColor] (0.1,0) -- +(0.2,-0.4);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\fill[drawColor] (0,-0.2) arc (240:300:0.2);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\node [or,label=right:Or Group] {}; \\\\"); + + stringBuilder.append(" \\draw[drawColor] (0.1,0) -- +(-0.2, -0.4);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\draw[drawColor] (0.1,0) -- +(0.2,-0.4);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\draw[drawColor] (0,-0.2) arc (240:300:0.2);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\node [alternative,label=right:Alternative Group] {}; \\\\"); + + stringBuilder.append(" \\node [hiddenNodes,label=center:1,label=right:Collapsed Nodes] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); - if (check) { stringBuilder.append(" \\matrix [anchor=north west] at (current bounding box.north east) {").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\node [placeholder] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" };").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\matrix [draw=drawColor,anchor=north west] at (current bounding box.north east) {").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\node [label=center:\\underline{Legend:}] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); - stringBuilder.append(sb); stringBuilder.append(" };").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); - check = false; - } else { - for (int i = 0; i < LEGEND.length; ++i) { - LEGEND[i] = false; - } - check = false; - } + + } - */ + /*private void printConstraints() { stringBuilder.append(" \\matrix [below=1mm of current bounding box] {").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); diff --git a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java index f72d6225..93e6ee7b 100644 --- a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java +++ b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java @@ -1,7 +1,6 @@ package de.featjar.feature.model.io.tikz; import de.featjar.base.FeatJAR; -import de.featjar.base.data.Range; import de.featjar.base.data.identifier.Identifiers; import de.featjar.feature.model.*; import de.featjar.formula.structure.connective.*; @@ -32,6 +31,7 @@ public static void init() { IFeature feature = featureModel.mutate().addFeature("Feature"); IFeatureTree firstFeatureTree = rootTree.mutate().addFeatureBelow(feature); + firstFeatureTree.mutate().toOrGroup(); IFeature world = featureModel.mutate().addFeature("World"); rootTree.mutate().addFeatureBelow(world); @@ -44,7 +44,7 @@ public static void init() { // Second Tree - IFeatureTree rootTreeSec = + /*IFeatureTree rootTreeSec = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("Hello2")); rootTree.mutate().toAndGroup(); @@ -77,6 +77,8 @@ public static void init() { IFeature childFeature3 = featureModel.mutate().addFeature("C"); childFeature1Tree.mutate().addFeatureBelow(childFeature3); + */ + FeatureModelDisplayTikzTest.featureModel = featureModel; } From 3ebee6e6eb6c1bcc5b0f4ab6890623d882a462a1 Mon Sep 17 00:00:00 2001 From: Jonas Hanke Date: Fri, 10 Oct 2025 11:33:46 +0200 Subject: [PATCH 34/67] feat: constraints for tikz export --- .../model/io/tikz/format/TikzMainFormat.java | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java index 2268a1d1..a326e55d 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java @@ -1,9 +1,14 @@ package de.featjar.feature.model.io.tikz.format; +import de.featjar.base.tree.Trees; +import de.featjar.feature.model.IConstraint; import de.featjar.feature.model.IFeature; import de.featjar.feature.model.IFeatureModel; import de.featjar.feature.model.IFeatureTree; import de.featjar.feature.model.io.tikz.TikzGraphicalFeatureModelFormat; +import de.featjar.formula.io.textual.ExpressionSerializer; +import de.featjar.formula.io.textual.LaTexSymbols; +import de.featjar.formula.structure.IFormula; public class TikzMainFormat implements IGraphicalFormat{ @@ -135,24 +140,20 @@ private void printLegend() { } + private void printConstraints() { + ExpressionSerializer expressionSerializer = new ExpressionSerializer(); + expressionSerializer.setEnquoteAlways(true); + expressionSerializer.setSymbols(LaTexSymbols.INSTANCE); - /*private void printConstraints() { stringBuilder.append(" \\matrix [below=1mm of current bounding box] {").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); for (IConstraint constraint : featureModel.getConstraints()) { - String text = constraint.getFormula().print(); + String text = constraint.getFormula().traverse(expressionSerializer).get(); text = text.replaceAll("\"([\\w\" ]+)\"", " \\\\text\\{$1\\} "); // wrap all words in \text{} // replace with $2 text = text.replaceAll("\\s+", " "); // remove unnecessary whitespace characters stringBuilder.append(" \\node {\\(").append(text).append("\\)}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + + expressionSerializer.reset(); } stringBuilder.append(" };").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); - /*for (final IGraphicalConstraint constraint : graphicalFeatureModel.getConstraints()) { - String text = constraint.getObject().getNode().toString(NodeWriter.latexSymbols, true); - text = text.replaceAll("\"([\\w\" ]+)\"", " \\\\text\\{$1\\} "); // wrap all words in \text{} // replace with $2 - text = text.replaceAll("\\s+", " "); // remove unnecessary whitespace characters - str.append(" \\node {\\(" + text + "\\)}; \\\\" + lnSep); - } - str.append(" };" + lnSep); } - */ - } From 61ef640fee21df08c4edcab7c74d50a0137a6020 Mon Sep 17 00:00:00 2001 From: Lara Date: Fri, 10 Oct 2025 11:37:47 +0200 Subject: [PATCH 35/67] feat: added authors and the legend print with the matrixhelper --- .../io/tikz/format/IGraphicalFormat.java | 5 ++ .../model/io/tikz/format/TikzBodyFormat.java | 5 ++ .../model/io/tikz/format/TikzHeadFormat.java | 5 ++ .../model/io/tikz/format/TikzMainFormat.java | 83 +++++++++++++++---- .../model/io/tikz/helper/MatrixHelper.java | 4 + .../model/io/tikz/helper/MatrixType.java | 4 + .../io/tikz/helper/MatrixHelperTest.java | 4 + 7 files changed, 92 insertions(+), 18 deletions(-) create mode 100644 src/main/java/de/featjar/feature/model/io/tikz/helper/MatrixHelper.java create mode 100644 src/main/java/de/featjar/feature/model/io/tikz/helper/MatrixType.java create mode 100644 src/test/java/de/featjar/feature/model/io/tikz/helper/MatrixHelperTest.java diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/IGraphicalFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/IGraphicalFormat.java index d1812ed0..0f8e3c6b 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/IGraphicalFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/IGraphicalFormat.java @@ -1,5 +1,10 @@ package de.featjar.feature.model.io.tikz.format; +/** + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ public interface IGraphicalFormat { void write(); diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzBodyFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzBodyFormat.java index 6df82947..102a2517 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzBodyFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzBodyFormat.java @@ -1,5 +1,10 @@ package de.featjar.feature.model.io.tikz.format; +/** + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ public class TikzBodyFormat implements IGraphicalFormat{ private final StringBuilder stringBuilder; diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzHeadFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzHeadFormat.java index f6fbc729..d4025c3f 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzHeadFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzHeadFormat.java @@ -5,6 +5,11 @@ import java.nio.charset.StandardCharsets; import java.util.List; +/** + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ public class TikzHeadFormat { public static void header(StringBuilder stringBuilder, List problemList, boolean hasVerticalLayout) { diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java index 2268a1d1..6e837822 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java @@ -4,10 +4,18 @@ import de.featjar.feature.model.IFeatureModel; import de.featjar.feature.model.IFeatureTree; import de.featjar.feature.model.io.tikz.TikzGraphicalFeatureModelFormat; - +import de.featjar.feature.model.io.tikz.helper.MatrixHelper; +import de.featjar.feature.model.io.tikz.helper.MatrixType; + +/** + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ public class TikzMainFormat implements IGraphicalFormat{ private final boolean[] LEGEND = new boolean[7]; + private boolean check = false; private final IFeatureModel featureModel; private final IFeatureTree featureTree; @@ -28,10 +36,12 @@ private void insertNodeHead(IFeature feature) { if (feature.isAbstract()) { stringBuilder.append(",abstract"); LEGEND[0] = true; + check = true; } if (feature.isConcrete()) { stringBuilder.append(",concrete"); LEGEND[1] = true; + check = true; } if (!isRootFeature(feature) && feature.getFeatureTree().isPresent() @@ -39,9 +49,11 @@ private void insertNodeHead(IFeature feature) { if (feature.getFeatureTree().get().isMandatory()) { stringBuilder.append(",mandatory"); LEGEND[2] = true; + check = true; } else { stringBuilder.append(",optional"); LEGEND[3] = true; + check = true; } } @@ -49,12 +61,14 @@ private void insertNodeHead(IFeature feature) { if (feature.getFeatureTree().get().getParentGroup().get().isOr()) { stringBuilder.append(",or"); LEGEND[4] = true; + check = true; } } if (!isRootFeature(feature)) { if (feature.getFeatureTree().get().getParentGroup().get().isAlternative()) { stringBuilder.append(",alternative"); LEGEND[5] = true; + check = true; } } } @@ -95,9 +109,9 @@ private void printForest() { stringBuilder.append("]"); postProcessing(); stringBuilder.append("\t").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); - if (!featureTree.getFeature().isHidden()) { // todo: fix error - printLegend(); // todo - } // todo + if (!featureTree.getFeature().isHidden()) { + printLegend(); + } //printConstraints(str, object); stringBuilder.append("\\end{forest}").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); } @@ -110,29 +124,62 @@ private void postProcessing() { } private void printLegend() { + if (!check) { + return; + } - stringBuilder.append(" \\node [abstract,label=right:Abstract Feature] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); - - stringBuilder.append(" \\node [concrete,label=right:Concrete Feature] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); - - stringBuilder.append(" \\node [abstract,label=right:Feature] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); - - stringBuilder.append(" \\node [concrete,label=right:Feature] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + MatrixHelper matrixHelper = getMatrixHelper(); + + if (LEGEND[4]) { + matrixHelper + .writeFillDraw("filldraw[drawColor] (0.45,0.15) ++ (225:0.3) arc[start angle=315,end angle=225,radius=0.2]") + .writeNode("[or,label=right:Or] {}") + .writeFillDraw("(0.1,0) - +(-0,-0.2) - +(0.2,-0.2)- +(0.1,0)") + .writeDraw("(0.1,0) -- +(-0.2, -0.4)") + .writeDraw("(0.1,0) -- +(0.2,-0.4)") + .writeFill("(0,-0.2) arc (240:300:0.2)") + .writeNode("[or,label=right:Or Group] {}"); + } - stringBuilder.append(" \\node [mandatory,label=right:Mandatory] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + if (LEGEND[5]) { + matrixHelper + .writeDraw("(0.45,0.15) ++ (225:0.3) arc[start angle=315,end angle=225,radius=0.2] -- cycle") + .writeNode("[alternative,label=right:Alternative] {}") + .writeDraw("(0.1,0) -- +(-0.2, -0.4)") + .writeDraw("(0.1,0) -- +(0.2,-0.4)") + .writeDraw("(0,-0.2) arc (240:300:0.2)") + .writeNode("[alternative,label=right:Alternative Group] {}"); + } - stringBuilder.append(" \\node [optional,label=right:Optional] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + stringBuilder.append(matrixHelper.build()); + } - stringBuilder.append(" \\filldraw[drawColor] (0.1,0) - +(-0,-0.2) - +(0.2,-0.2)- +(0.1,0);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\draw[drawColor] (0.1,0) -- +(-0.2, -0.4);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\draw[drawColor] (0.1,0) -- +(0.2,-0.4);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\fill[drawColor] (0,-0.2) arc (240:300:0.2);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\node [or,label=right:Or Group] {}; \\\\"); + private MatrixHelper getMatrixHelper() { + MatrixHelper matrixHelper = new MatrixHelper(MatrixType.LEGEND); + boolean abstractConcreteExists = false; - stringBuilder.append(" \\draw[drawColor] (0.1,0) -- +(-0.2, -0.4);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\draw[drawColor] (0.1,0) -- +(0.2,-0.4);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\draw[drawColor] (0,-0.2) arc (240:300:0.2);").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\node [alternative,label=right:Alternative Group] {}; \\\\"); + if (LEGEND[0] && LEGEND[1]) { + abstractConcreteExists = true; + matrixHelper.writeNode("[abstract,label=right:Abstract Feature] {}"); + matrixHelper.writeNode("[concrete,label=right:Concrete Feature] {}"); + } - stringBuilder.append(" \\node [hiddenNodes,label=center:1,label=right:Collapsed Nodes] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + if (LEGEND[0] && !abstractConcreteExists) { + matrixHelper.writeNode("[abstract,label=right:Feature] {}"); + } - stringBuilder.append(" \\matrix [anchor=north west] at (current bounding box.north east) {").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\node [placeholder] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" };").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\matrix [draw=drawColor,anchor=north west] at (current bounding box.north east) {").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append(" \\node [label=center:\\underline{Legend:}] {}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); - stringBuilder.append(" };").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + if (LEGEND[1] && !abstractConcreteExists) { + matrixHelper.writeNode("[concrete,label=right:Feature] {}"); + } + if (LEGEND[2]) { + matrixHelper.writeNode("[mandatory,label=right:Mandatory] {}"); + } + if (LEGEND[3]) { + matrixHelper.writeNode("[optional,label=right:Optional] {}"); + } + return matrixHelper; } diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/MatrixHelper.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/MatrixHelper.java new file mode 100644 index 00000000..9a40eb03 --- /dev/null +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/MatrixHelper.java @@ -0,0 +1,4 @@ +package de.featjar.feature.model.io.tikz.helper; + +public class MatrixHelper { +} diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/MatrixType.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/MatrixType.java new file mode 100644 index 00000000..9f4a9f13 --- /dev/null +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/MatrixType.java @@ -0,0 +1,4 @@ +package de.featjar.feature.model.io.tikz.helper; + +public enum MatrixType { +} diff --git a/src/test/java/de/featjar/feature/model/io/tikz/helper/MatrixHelperTest.java b/src/test/java/de/featjar/feature/model/io/tikz/helper/MatrixHelperTest.java new file mode 100644 index 00000000..8b59ec9b --- /dev/null +++ b/src/test/java/de/featjar/feature/model/io/tikz/helper/MatrixHelperTest.java @@ -0,0 +1,4 @@ +package de.featjar.feature.model.io.tikz.helper; + +public class MatrixHelperTest { +} From 1ffcf20a9d3db89bbb4d459e902e9be74147d807 Mon Sep 17 00:00:00 2001 From: Lara Date: Fri, 10 Oct 2025 11:38:21 +0200 Subject: [PATCH 36/67] feat: added matrixhelper for cleaner code --- .../model/io/tikz/helper/MatrixHelper.java | 54 +++++++++++++++++++ .../model/io/tikz/helper/MatrixType.java | 21 ++++++++ 2 files changed, 75 insertions(+) diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/MatrixHelper.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/MatrixHelper.java index 9a40eb03..92420223 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/helper/MatrixHelper.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/MatrixHelper.java @@ -1,4 +1,58 @@ package de.featjar.feature.model.io.tikz.helper; +/** + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ public class MatrixHelper { + + private final static String NODE = " \\node {replace}; \\\\" + System.lineSeparator(); + private final static String DRAW = " \\draw[drawColor] {replace};" + System.lineSeparator(); + private final static String FILL_DRAW = " \\filldraw[drawColor] {replace}; " + System.lineSeparator(); + private final static String FILL = " \\fill[drawColor] {replace};" + System.lineSeparator(); + + private final StringBuilder stringBuilder; + private final String header ; + + public MatrixHelper(MatrixType matrixType) { + this.stringBuilder = new StringBuilder(); + this.header = matrixType.getHeader(); + writeHeader(); + } + + private void writeHeader() { + stringBuilder.append(header); + } + + private void writeFooter() { + stringBuilder.append(" };") + .append(System.lineSeparator()); + } + + public MatrixHelper writeNode(String value) { + stringBuilder.append(NODE.replace("{replace}", value)); + return this; + } + + public MatrixHelper writeDraw(String value) { + stringBuilder.append(DRAW.replace("{replace}", value)); + return this; + } + + public MatrixHelper writeFillDraw(String value) { + stringBuilder.append(FILL_DRAW.replace("{replace}", value)); + return this; + } + + public MatrixHelper writeFill(String value) { + stringBuilder.append(FILL.replace("{replace}", value)); + return this; + } + + public StringBuilder build() { + writeFooter(); + return stringBuilder; + } + } diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/MatrixType.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/MatrixType.java index 9f4a9f13..e9a4de39 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/helper/MatrixType.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/MatrixType.java @@ -1,4 +1,25 @@ package de.featjar.feature.model.io.tikz.helper; +/** + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ public enum MatrixType { + + LEGEND(" \\matrix [anchor=north west] at (current bounding box.north east) {" + System.lineSeparator() + " \\node [placeholder] {}; \\\\" + System.lineSeparator() + + " };" + System.lineSeparator() + " \\matrix [draw=drawColor,anchor=north west] at (current bounding box.north east) {" + System.lineSeparator() + + " \\node [label=center:\\underline{Legend:}] {}; \\\\" + System.lineSeparator()), + CONSTRAINS(""), + ATTRIBUTES(""); + + final String header; + + MatrixType(String header) { + this.header = header; + } + + public String getHeader() { + return header; + } } From b3856d3fed08942d54a5cc4665fe45f737377fb9 Mon Sep 17 00:00:00 2001 From: Lara Date: Fri, 10 Oct 2025 11:38:52 +0200 Subject: [PATCH 37/67] feat: new test for matrixhelper --- .../io/tikz/FeatureModelDisplayTikzTest.java | 26 +++------------ .../io/tikz/helper/MatrixHelperTest.java | 32 +++++++++++++++++++ 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java index 93e6ee7b..b34e962d 100644 --- a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java +++ b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java @@ -1,6 +1,7 @@ package de.featjar.feature.model.io.tikz; import de.featjar.base.FeatJAR; +import de.featjar.base.data.Range; import de.featjar.base.data.identifier.Identifiers; import de.featjar.feature.model.*; import de.featjar.formula.structure.connective.*; @@ -24,9 +25,11 @@ public static void init() { FeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + IFeature featureRootS = featureModel.mutate().addFeature("Hello"); + featureRootS.mutate().setAbstract(); // First Tree IFeatureTree rootTree = - featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("Hello")); + featureModel.mutate().addFeatureTreeRoot(featureRootS); rootTree.mutate().toAndGroup(); IFeature feature = featureModel.mutate().addFeature("Feature"); @@ -44,28 +47,11 @@ public static void init() { // Second Tree - /*IFeatureTree rootTreeSec = - featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("Hello2")); - rootTree.mutate().toAndGroup(); - - IFeature feature2 = featureModel.mutate().addFeature("Feature2"); - rootTreeSec.mutate().addFeatureBelow(feature2); - - IFeature world2 = featureModel.mutate().addFeature("World2"); - IFeatureTree firstFeatureTree2 = rootTreeSec.mutate().addFeatureBelow(world2); - - IFeature wonderful2 = featureModel.addFeature("Wonderful2"); - firstFeatureTree2.mutate().addFeatureBelow(wonderful2); - - IFeature beautiful2 = featureModel.addFeature("Beautiful2"); - firstFeatureTree2.mutate().addFeatureBelow(beautiful2); - - // Thrid Tree - IFeatureTree rootTree3 = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); rootTree3.mutate().toAndGroup(); IFeature childFeature1 = featureModel.mutate().addFeature("A"); + childFeature1.mutate().setAbstract(); IFeatureTree childFeature1Tree = rootTree3.mutate().addFeatureBelow(childFeature1); childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); @@ -77,8 +63,6 @@ public static void init() { IFeature childFeature3 = featureModel.mutate().addFeature("C"); childFeature1Tree.mutate().addFeatureBelow(childFeature3); - */ - FeatureModelDisplayTikzTest.featureModel = featureModel; } diff --git a/src/test/java/de/featjar/feature/model/io/tikz/helper/MatrixHelperTest.java b/src/test/java/de/featjar/feature/model/io/tikz/helper/MatrixHelperTest.java index 8b59ec9b..dc667d12 100644 --- a/src/test/java/de/featjar/feature/model/io/tikz/helper/MatrixHelperTest.java +++ b/src/test/java/de/featjar/feature/model/io/tikz/helper/MatrixHelperTest.java @@ -1,4 +1,36 @@ package de.featjar.feature.model.io.tikz.helper; +import de.featjar.base.FeatJAR; +import org.junit.jupiter.api.Test; + public class MatrixHelperTest { + + @Test + public void matrix() { + FeatJAR.testConfiguration().initialize(); + + MatrixHelper matrixHelper = new MatrixHelper(MatrixType.LEGEND); + matrixHelper + .writeNode("[abstract,label=right:Abstract Feature] {}") + .writeNode("[concrete,label=right:Concrete Feature] {}") + .writeNode("[abstract,label=right:Feature] {}") + .writeNode("[concrete,label=right:Feature] {}") + .writeNode("[mandatory,label=right:Mandatory] {}") + .writeNode("[optional,label=right:Optional] {}") + // Or Group + .writeFillDraw("filldraw[drawColor] (0.1,0) -- +(-0,-0.2) -- +(0.2,-0.2) -- +(0.1,0)") + .writeDraw("draw[drawColor] (0.1,0) -- +(-0.2, -0.4)") + .writeDraw("draw[drawColor] (0.1,0) -- +(0.2,-0.4)") + .writeFill("fill[drawColor] (0,-0.2) arc (240:300:0.2)") + .writeNode("[or,label=right:Or Group] {}"); + + // Alternative Group + //.writeDraw("draw[drawColor] (0.1,0) -- +(-0.2, -0.4)") + //.writeDraw("draw[drawColor] (0.1,0) -- +(0.2,-0.4)") + //.writeDraw("draw[drawColor] (0,-0.2) arc (240:300:0.2)") + //.writeNode("[alternative,label=right:Alternative Group] {}"); + + FeatJAR.log().info(matrixHelper.build()); + } + } From 98f1dc586715fdd8bef2776494594828cd9a6dad Mon Sep 17 00:00:00 2001 From: Lara Date: Fri, 10 Oct 2025 13:23:39 +0200 Subject: [PATCH 38/67] fix: small changes --- .../tikz/TikzGraphicalFeatureModelFormat.java | 8 ++--- .../io/tikz/format/IGraphicalFormat.java | 12 ------- .../model/io/tikz/format/TikzBodyFormat.java | 36 ------------------- .../model/io/tikz/format/TikzMainFormat.java | 17 +++------ .../io/tikz/FeatureModelDisplayTikzTest.java | 27 ++++---------- 5 files changed, 16 insertions(+), 84 deletions(-) delete mode 100644 src/main/java/de/featjar/feature/model/io/tikz/format/IGraphicalFormat.java delete mode 100644 src/main/java/de/featjar/feature/model/io/tikz/format/TikzBodyFormat.java diff --git a/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java index 932c9caa..3bbc8dc8 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java @@ -23,13 +23,12 @@ public class TikzGraphicalFeatureModelFormat implements IFormat { public static String LINE_SEPERATOR = System.lineSeparator(); - private final boolean[] legend = new boolean[7]; - private final StringBuilder stringBuilder; private final IFeatureModel featureModel; private final boolean hasVerticalLayout; private final List problemList; + // todo remove constructer public TikzGraphicalFeatureModelFormat(IFeatureModel featureModel, boolean hasVerticalLayout) { this.stringBuilder = new StringBuilder(); this.featureModel = featureModel; @@ -37,13 +36,14 @@ public TikzGraphicalFeatureModelFormat(IFeatureModel featureModel, boolean hasVe this.hasVerticalLayout = hasVerticalLayout; } - public Result serialize() { + @Override + public Result serialize(IFeatureModel object) { stringBuilder.append("\\documentclass[border=5pt]{standalone}"); stringBuilder.append(LINE_SEPERATOR); TikzHeadFormat.header(stringBuilder, problemList, hasVerticalLayout); stringBuilder.append("\\begin{document}").append(LINE_SEPERATOR).append(" %---The Feature Diagram-----------------------------------------------------").append(LINE_SEPERATOR); for (IFeatureTree featureTree : featureModel.getRoots()) { - new TikzMainFormat(featureModel, featureTree, stringBuilder).write(); + new TikzMainFormat(featureModel, featureTree, stringBuilder).printForest(); } stringBuilder.append(LINE_SEPERATOR); stringBuilder.append("\t%---------------------------------------------------------------------------").append(LINE_SEPERATOR).append("\\end{document}"); diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/IGraphicalFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/IGraphicalFormat.java deleted file mode 100644 index 0f8e3c6b..00000000 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/IGraphicalFormat.java +++ /dev/null @@ -1,12 +0,0 @@ -package de.featjar.feature.model.io.tikz.format; - -/** - * @author Felix Behme - * @author Lara Merza - * @author Jonas Hanke - */ -public interface IGraphicalFormat { - - void write(); - -} diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzBodyFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzBodyFormat.java deleted file mode 100644 index 102a2517..00000000 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzBodyFormat.java +++ /dev/null @@ -1,36 +0,0 @@ -package de.featjar.feature.model.io.tikz.format; - -/** - * @author Felix Behme - * @author Lara Merza - * @author Jonas Hanke - */ -public class TikzBodyFormat implements IGraphicalFormat{ - - private final StringBuilder stringBuilder; - private final String fileName; - - public TikzBodyFormat(String fileName, StringBuilder stringBuilder) { - this.fileName = fileName; - this.stringBuilder = (stringBuilder == null) ? new StringBuilder() : stringBuilder; - } - - @Override - public void write() { - stringBuilder.append("\\documentclass[border=5pt]{standalone}") - .append(System.lineSeparator()); - stringBuilder.append("\\input{head.tex}") - .append(System.lineSeparator()); // Include head - stringBuilder.append("\\begin{document}") - .append(System.lineSeparator()) - .append(" "); - stringBuilder.append("\\sffamily") - .append(System.lineSeparator()); - stringBuilder.append(" \\input{") - .append(fileName) - .append("}") - .append(System.lineSeparator()); // Include main - stringBuilder.append("\\end{document}"); - } - -} diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java index 47255c29..d4537466 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java @@ -10,14 +10,13 @@ import de.featjar.feature.model.io.tikz.helper.MatrixType; import de.featjar.formula.io.textual.ExpressionSerializer; import de.featjar.formula.io.textual.LaTexSymbols; -import de.featjar.formula.structure.IFormula; /** * @author Felix Behme * @author Lara Merza * @author Jonas Hanke */ -public class TikzMainFormat implements IGraphicalFormat{ +public class TikzMainFormat { private final boolean[] LEGEND = new boolean[7]; private boolean check = false; @@ -32,11 +31,6 @@ public TikzMainFormat(IFeatureModel featureModel ,IFeatureTree featureTree, Stri this.stringBuilder = stringBuilder; } - @Override - public void write() { - printForest(); - } - private void insertNodeHead(IFeature feature) { if (feature.isAbstract()) { stringBuilder.append(",abstract"); @@ -49,8 +43,7 @@ private void insertNodeHead(IFeature feature) { check = true; } - if (!isRootFeature(feature) && feature.getFeatureTree().isPresent() - && feature.getFeatureTree().get().getParentGroup().isEmpty()) { + if (!isRootFeature(feature) && feature.getFeatureTree().isPresent()) { if (feature.getFeatureTree().get().isMandatory()) { stringBuilder.append(",mandatory"); LEGEND[2] = true; @@ -90,19 +83,19 @@ private boolean isRootFeature(IFeature feature) { */ private void printTree(IFeatureTree featureTree) { IFeature feature = featureTree.getFeature(); - //insertNodeHead(feature.getName().get()); stringBuilder.append("[").append(feature.getName().get()); insertNodeHead(feature); for (IFeatureTree featureTreeChildren : featureTree.getChildren()) { printTree(featureTreeChildren); } stringBuilder.append("]"); + // Trees.traverse(featureTree, new PrintVisitor()); } /** * Build the complete tree of the FeatureModel. */ - private void printForest() { + public void printForest() { IFeature feature = featureTree.getFeature(); stringBuilder.append("\\begin{forest}").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append("\tfeatureDiagram").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append("\t"); @@ -117,7 +110,7 @@ private void printForest() { if (!featureTree.getFeature().isHidden()) { printLegend(); } - //printConstraints(str, object); + printConstraints(); stringBuilder.append("\\end{forest}").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); } diff --git a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java index b34e962d..eacf81ef 100644 --- a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java +++ b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java @@ -4,7 +4,9 @@ import de.featjar.base.data.Range; import de.featjar.base.data.identifier.Identifiers; import de.featjar.feature.model.*; +import de.featjar.formula.structure.Expressions; import de.featjar.formula.structure.connective.*; +import de.featjar.formula.structure.term.value.Variable; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -30,11 +32,11 @@ public static void init() { // First Tree IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureRootS); - rootTree.mutate().toAndGroup(); + rootTree.mutate().toOrGroup(); IFeature feature = featureModel.mutate().addFeature("Feature"); IFeatureTree firstFeatureTree = rootTree.mutate().addFeatureBelow(feature); - firstFeatureTree.mutate().toOrGroup(); + firstFeatureTree.mutate().makeMandatory(); IFeature world = featureModel.mutate().addFeature("World"); rootTree.mutate().addFeatureBelow(world); @@ -44,31 +46,16 @@ public static void init() { IFeature beautiful = featureModel.addFeature("Beautiful"); firstFeatureTree.mutate().addFeatureBelow(beautiful); + featureModel.addConstraint(new And(Expressions.literal("A"), Expressions.literal("B"))); + featureModel.addConstraint(new Implies(Expressions.literal("C"), new ForAll(new Variable("A"), new BiImplies(Expressions.literal("A"), Expressions.literal("C"))))); - // Second Tree - - IFeatureTree rootTree3 = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); - rootTree3.mutate().toAndGroup(); - - IFeature childFeature1 = featureModel.mutate().addFeature("A"); - childFeature1.mutate().setAbstract(); - IFeatureTree childFeature1Tree = rootTree3.mutate().addFeatureBelow(childFeature1); - childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); - - childFeature1Tree.mutate().toAlternativeGroup(); - - IFeature childFeature2 = featureModel.mutate().addFeature("B"); - childFeature1Tree.mutate().addFeatureBelow(childFeature2); - - IFeature childFeature3 = featureModel.mutate().addFeature("C"); - childFeature1Tree.mutate().addFeatureBelow(childFeature3); FeatureModelDisplayTikzTest.featureModel = featureModel; } @Test public void perform() { - new TikzGraphicalFeatureModelFormat(featureModel, false).serialize().ifPresent(s -> { + new TikzGraphicalFeatureModelFormat(featureModel, false).serialize(featureModel).ifPresent(s -> { FeatJAR.log().info(s); }); } From 5b7d9e36a341a3efd7d4522dda13a96c6be2309e Mon Sep 17 00:00:00 2001 From: Lara Date: Fri, 10 Oct 2025 16:21:18 +0200 Subject: [PATCH 39/67] fix: impl working test --- .../model/io/tikz/helper/PrintVisitor.java | 4 + src/main/resources/test/test-output.tex | 156 ++++++++++++++++++ .../io/tikz/FeatureModelDisplayTikzTest.java | 89 +++++++--- 3 files changed, 227 insertions(+), 22 deletions(-) create mode 100644 src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java create mode 100644 src/main/resources/test/test-output.tex diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java new file mode 100644 index 00000000..c70342ad --- /dev/null +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java @@ -0,0 +1,4 @@ +package de.featjar.feature.model.io.tikz.helper; + +public class PrintVisitor { +} diff --git a/src/main/resources/test/test-output.tex b/src/main/resources/test/test-output.tex new file mode 100644 index 00000000..cf68a19b --- /dev/null +++ b/src/main/resources/test/test-output.tex @@ -0,0 +1,156 @@ +\documentclass[border=5pt]{standalone} +%---required packages & variable definitions------------------------------------ +\usepackage{forest} +\usepackage{amsmath} +\usepackage{xcolor} +\usetikzlibrary{angles} +\usetikzlibrary{positioning} +\definecolor{drawColor}{RGB}{128 128 128} +\newcommand{\circleSize}{0.25em} +\newcommand{\angleSize}{0.8em} +%------------------------------------------------------------------------------- +%---Define the style of the tree------------------------------------------------ +\forestset{ + /tikz/mandatory/.style={ + circle,fill=drawColor, + draw=drawColor, + inner sep=\circleSize + }, + /tikz/optional/.style={ + circle, + fill=white, + draw=drawColor, + inner sep=\circleSize + }, + featureDiagram/.style={ + for tree={ + minimum height = 0.6cm, + text depth = 0, + draw = drawColor, + edge = {draw=drawColor}, + parent anchor = south, + child anchor = north, + l sep = 2em, + s sep = 1em, + } + }, + /tikz/redColor/.style={ + fill = red!60, + draw = drawColor + }, + /tikz/orangeColor/.style={ + fill = orange!50, + draw = drawColor + }, + /tikz/yellowColor/.style={ + fill = yellow!50, + draw = drawColor + }, + /tikz/darkGreenColor/.style={ + fill = black!30!green, + draw = drawColor + }, + /tikz/lightGreenColor/.style={ + fill = green!30, + draw = drawColor + }, + /tikz/cyanColor/.style={ + fill = cyan!30, + draw = drawColor + }, + /tikz/lightGrayColor/.style={ + fill = black!10, + draw = drawColor + }, + /tikz/blueColor/.style={ + fill = blue!50, + draw = drawColor + }, + /tikz/magentaColor/.style={ + fill = magenta, + draw = drawColor + }, + /tikz/pinkColor/.style={ + fill = pink!90, + draw = drawColor + }, + /tikz/abstract/.style={ + fill = blue!85!cyan!5, + draw = drawColor + }, + /tikz/concrete/.style={ + fill = blue!85!cyan!20, + draw = drawColor + }, + mandatory/.style={ + edge label={node [mandatory] {} } + }, + optional/.style={ + edge label={node [optional] {} } + }, + or/.style={ + tikz+={ + \path (.parent) coordinate (A) -- (!u.children) coordinate (B) -- (!ul.parent) coordinate (C) pic[fill=drawColor, angle radius=\angleSize]{angle}; + } + }, + /tikz/or/.style={ + }, + alternative/.style={ + tikz+={ + \path (.parent) coordinate (A) -- (!u.children) coordinate (B) -- (!ul.parent) coordinate (C) pic[draw=drawColor, angle radius=\angleSize]{angle}; + } + }, + /tikz/alternative/.style={ + }, + /tikz/placeholder/.style={ + }, + collapsed/.style={ + rounded corners, + no edge, + for tree={ + fill opacity=0, + draw opacity=0, + l = 0em, + } + }, + /tikz/hiddenNodes/.style={ + midway, + rounded corners, + draw=drawColor, + fill=white, + minimum size = 1.2em, + minimum width = 0.8em, + scale=0.9 + }, +} +%------------------------------------------------------------------------------- +\begin{document} + %---The Feature Diagram----------------------------------------------------- +\begin{forest} + featureDiagram + [Hello,abstract[Feature,concrete,mandatory,or[Wonderful,concrete,optional][Beautiful,concrete,optional]][World,concrete,optional,or]] + \matrix [anchor=north west] at (current bounding box.north east) { + \node [placeholder] {}; \\ + }; + \matrix [draw=drawColor,anchor=north west] at (current bounding box.north east) { + \node [label=center:\underline{Legend:}] {}; \\ + \node [abstract,label=right:Abstract Feature] {}; \\ + \node [concrete,label=right:Concrete Feature] {}; \\ + \node [mandatory,label=right:Mandatory] {}; \\ + \node [optional,label=right:Optional] {}; \\ + \filldraw[drawColor] (0.45,0.15) ++ (225:0.3) arc[start angle=315,end angle=225,radius=0.2]; + \node [or,label=right:Or] {}; \\ + \filldraw[drawColor] (0.1,0) - +(-0,-0.2) - +(0.2,-0.2)- +(0.1,0); + \draw[drawColor] (0.1,0) -- +(-0.2, -0.4); + \draw[drawColor] (0.1,0) -- +(0.2,-0.4); + \fill[drawColor] (0,-0.2) arc (240:300:0.2); + \node [or,label=right:Or Group] {}; \\ + }; + \matrix [below=1mm of current bounding box] { + \node {\( \text{A} \land \text{B} \)}; \\ + \node {\( \text{C} \Rightarrow \forall( \text{A} \Leftrightarrow \text{C} )\)}; \\ + }; +\end{forest} + + %--------------------------------------------------------------------------- +\end{document} \ No newline at end of file diff --git a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java index eacf81ef..768feb1b 100644 --- a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java +++ b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java @@ -1,7 +1,6 @@ package de.featjar.feature.model.io.tikz; import de.featjar.base.FeatJAR; -import de.featjar.base.data.Range; import de.featjar.base.data.identifier.Identifiers; import de.featjar.feature.model.*; import de.featjar.formula.structure.Expressions; @@ -11,6 +10,11 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + /** * @author Felix Behme * @author Lara Merza @@ -55,39 +59,80 @@ public static void init() { @Test public void perform() { - new TikzGraphicalFeatureModelFormat(featureModel, false).serialize(featureModel).ifPresent(s -> { - FeatJAR.log().info(s); + StringBuilder expectedOutput = loadTestFile(); + + if (expectedOutput == null) { + throw new IllegalStateException("File is null"); + } + + String value = expectedOutput.toString(); + + FeatJAR.log().info("Expected Output: " + value); + + new TikzGraphicalFeatureModelFormat().serialize(featureModel).ifPresent(output -> { + FeatJAR.log().info("Expected Output: " + value); + FeatJAR.log().info("Acutally Output: " + output); + + Assertions.assertEquals(value, output); }); } + // Todo: Add @Test here and remove the other @Test on the method perform + //@Test + public void createTestFile() { + new TikzGraphicalFeatureModelFormat().serialize(featureModel).ifPresent(this::writeToFile); + } + /** - * Test for the "synatx" mechanic + * This method can be used to write the output in a file with ignoring tabs or spaces. + * (It will be written correctly) + * + * @param "output" from the "new" file */ + private void writeToFile(String value) { + Path filePath = Paths.get("src", "main", "resources", "test", "test-output.tex"); - public void displayTikz() { - featureModel.getFeatureModel().getRoots().forEach(iFeatureTree -> { - stringBuilder.append("[").append(iFeatureTree.getFeature().getName().get()); - for (IFeatureTree featureTreeChildren : iFeatureTree.getChildren()) { - subTreeRecursiv(featureTreeChildren); + try { + Files.createDirectories(filePath.getParent()); + + try (BufferedWriter writer = Files.newBufferedWriter(filePath)) { + writer.write(value); } - }); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * This method the test-output.tex from the resource (resource/test) file. + * + * @return Output of the file as a StringBuilder + */ + private StringBuilder loadTestFile() { + StringBuilder stringBuilderFile = new StringBuilder(); + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("test/test-output.tex"); - stringBuilder.append("]"); - FeatJAR.log().info(stringBuilder); + if (inputStream == null) { + return null; + } - String expectedOutput = "[Hello[Feature[Wonderful][Beautiful]][World]]"; + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) { + String line; + while ((line = bufferedReader.readLine()) != null) { + stringBuilderFile.append(line).append(System.lineSeparator()); + } - Assertions.assertEquals(expectedOutput, - stringBuilder.toString(), "Expected: " + expectedOutput + System.lineSeparator() - + "Output: " + stringBuilder); - } + } catch (IOException e) { + throw new RuntimeException(e); + } - private void subTreeRecursiv(IFeatureTree featureTree) { - stringBuilder.append("[").append(featureTree.getFeature().getName().get()); - for (IFeatureTree featureTreeChildren : featureTree.getChildren()) { - subTreeRecursiv(featureTreeChildren); + // Remove last add (its empty) + int lastIndex = stringBuilderFile.lastIndexOf("\n"); + if (lastIndex >= 0) { + stringBuilderFile.delete(lastIndex, stringBuilderFile.length()); } - stringBuilder.append("]"); + + return stringBuilderFile; } } From 706c248ec112c1af6aca758c6bfe33758e3d649f Mon Sep 17 00:00:00 2001 From: Lara Date: Fri, 10 Oct 2025 16:21:54 +0200 Subject: [PATCH 40/67] feat: added testfile --- src/main/resources/head.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/head.tex b/src/main/resources/head.tex index c7f2613f..780bd03e 100644 --- a/src/main/resources/head.tex +++ b/src/main/resources/head.tex @@ -94,7 +94,7 @@ alternative/.style={ tikz+={ \path (.parent) coordinate (A) -- (!u.children) coordinate (B) -- (!ul.parent) coordinate (C) pic[draw=drawColor, angle radius=\angleSize]{angle}; - } + } }, /tikz/alternative/.style={ }, From 72d7854ebcb08bce6b74b7550efbd0e44be380f4 Mon Sep 17 00:00:00 2001 From: Lara Date: Fri, 10 Oct 2025 16:23:17 +0200 Subject: [PATCH 41/67] feat: added tree visitor and comments in the class --- .../model/io/tikz/helper/MatrixHelper.java | 2 + .../model/io/tikz/helper/PrintVisitor.java | 80 ++++++++++++++++++- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/MatrixHelper.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/MatrixHelper.java index 92420223..a3d7dafd 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/helper/MatrixHelper.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/MatrixHelper.java @@ -1,6 +1,8 @@ package de.featjar.feature.model.io.tikz.helper; /** + * This class helps to build a matrix in latex. Choose a header from MatrixStyle and write your nodes, draw or whatever. + * * @author Felix Behme * @author Lara Merza * @author Jonas Hanke diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java index c70342ad..91bf2796 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java @@ -1,4 +1,80 @@ package de.featjar.feature.model.io.tikz.helper; -public class PrintVisitor { -} +import de.featjar.base.data.Result; +import de.featjar.base.tree.visitor.ITreeVisitor; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureTree; + +import java.util.List; + +/** + * This class travers a given {@link IFeatureTree} and generates the Tikz representation of the tree. + * + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ +public class PrintVisitor implements ITreeVisitor { + + private final StringBuilder stringBuilder; + + public PrintVisitor() { + this.stringBuilder = new StringBuilder(); + } + + @Override + public TraversalAction firstVisit(List path) { + IFeature feature = ITreeVisitor.getCurrentNode(path).getFeature(); + + stringBuilder.append("[").append(feature.getName().orElse("")); + insertNodeHead(feature); + + return TraversalAction.CONTINUE; + } + + @Override + public TraversalAction lastVisit(List path) { + stringBuilder.append("]"); + return TraversalAction.CONTINUE; + } + + @Override + public Result getResult() { + return Result.of(stringBuilder.toString()); + } + + + private void insertNodeHead(IFeature feature) { + if (feature.isAbstract()) { + stringBuilder.append(",abstract"); + } + + if (feature.isConcrete()) { + stringBuilder.append(",concrete"); + } + + if (!isRootFeature(feature) && feature.getFeatureTree().isPresent()) { + if (feature.getFeatureTree().get().isMandatory()) { + stringBuilder.append(",mandatory"); + } else { + stringBuilder.append(",optional"); + } + } + + if (!isRootFeature(feature)) { + if (feature.getFeatureTree().get().getParentGroup().get().isOr()) { + stringBuilder.append(",or"); + } + } + if (!isRootFeature(feature)) { + if (feature.getFeatureTree().get().getParentGroup().get().isAlternative()) { + stringBuilder.append(",alternative"); + } + } + } + + private boolean isRootFeature(IFeature feature) { + return feature.getFeatureModel().getRootFeatures().contains(feature); + } + +} \ No newline at end of file From 647506e5921fb7af0cd4e95d6791358b15807f44 Mon Sep 17 00:00:00 2001 From: Lara Date: Fri, 10 Oct 2025 16:24:00 +0200 Subject: [PATCH 42/67] fix: impl tree visitor and added comments to the class --- .../tikz/TikzGraphicalFeatureModelFormat.java | 25 ++-- .../model/io/tikz/format/TikzHeadFormat.java | 2 + .../model/io/tikz/format/TikzMainFormat.java | 136 ++++-------------- 3 files changed, 38 insertions(+), 125 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java index 3bbc8dc8..af8c9122 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java @@ -23,24 +23,14 @@ public class TikzGraphicalFeatureModelFormat implements IFormat { public static String LINE_SEPERATOR = System.lineSeparator(); - private final StringBuilder stringBuilder; - private final IFeatureModel featureModel; - private final boolean hasVerticalLayout; - private final List problemList; - - // todo remove constructer - public TikzGraphicalFeatureModelFormat(IFeatureModel featureModel, boolean hasVerticalLayout) { - this.stringBuilder = new StringBuilder(); - this.featureModel = featureModel; - this.problemList = new ArrayList<>(); - this.hasVerticalLayout = hasVerticalLayout; - } - @Override - public Result serialize(IFeatureModel object) { + public Result serialize(IFeatureModel featureModel) { + StringBuilder stringBuilder = new StringBuilder(); + List problemList = new ArrayList<>(); + stringBuilder.append("\\documentclass[border=5pt]{standalone}"); stringBuilder.append(LINE_SEPERATOR); - TikzHeadFormat.header(stringBuilder, problemList, hasVerticalLayout); + TikzHeadFormat.header(stringBuilder, problemList, false); stringBuilder.append("\\begin{document}").append(LINE_SEPERATOR).append(" %---The Feature Diagram-----------------------------------------------------").append(LINE_SEPERATOR); for (IFeatureTree featureTree : featureModel.getRoots()) { new TikzMainFormat(featureModel, featureTree, stringBuilder).printForest(); @@ -50,8 +40,9 @@ public Result serialize(IFeatureModel object) { return Result.of(stringBuilder.toString(), problemList); } - public IFeatureModel getFeatureModel() { - return featureModel; + @Override + public boolean supportsWrite() { + return true; } @Override diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzHeadFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzHeadFormat.java index d4025c3f..02fc8dbf 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzHeadFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzHeadFormat.java @@ -6,6 +6,8 @@ import java.util.List; /** + * This class prints the header of a LaTex document containing the Tikz definitions. + * * @author Felix Behme * @author Lara Merza * @author Jonas Hanke diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java index d4537466..92827c22 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java @@ -2,25 +2,24 @@ import de.featjar.base.tree.Trees; import de.featjar.feature.model.IConstraint; -import de.featjar.feature.model.IFeature; import de.featjar.feature.model.IFeatureModel; import de.featjar.feature.model.IFeatureTree; import de.featjar.feature.model.io.tikz.TikzGraphicalFeatureModelFormat; import de.featjar.feature.model.io.tikz.helper.MatrixHelper; import de.featjar.feature.model.io.tikz.helper.MatrixType; +import de.featjar.feature.model.io.tikz.helper.PrintVisitor; import de.featjar.formula.io.textual.ExpressionSerializer; import de.featjar.formula.io.textual.LaTexSymbols; /** + * This class generates the Tikz representation of a {@link IFeatureModel} including all constraints ({@link IConstraint}). + * * @author Felix Behme * @author Lara Merza * @author Jonas Hanke */ public class TikzMainFormat { - private final boolean[] LEGEND = new boolean[7]; - private boolean check = false; - private final IFeatureModel featureModel; private final IFeatureTree featureTree; private final StringBuilder stringBuilder; @@ -31,80 +30,16 @@ public TikzMainFormat(IFeatureModel featureModel ,IFeatureTree featureTree, Stri this.stringBuilder = stringBuilder; } - private void insertNodeHead(IFeature feature) { - if (feature.isAbstract()) { - stringBuilder.append(",abstract"); - LEGEND[0] = true; - check = true; - } - if (feature.isConcrete()) { - stringBuilder.append(",concrete"); - LEGEND[1] = true; - check = true; - } - - if (!isRootFeature(feature) && feature.getFeatureTree().isPresent()) { - if (feature.getFeatureTree().get().isMandatory()) { - stringBuilder.append(",mandatory"); - LEGEND[2] = true; - check = true; - } else { - stringBuilder.append(",optional"); - LEGEND[3] = true; - check = true; - } - } - - if (!isRootFeature(feature)) { - if (feature.getFeatureTree().get().getParentGroup().get().isOr()) { - stringBuilder.append(",or"); - LEGEND[4] = true; - check = true; - } - } - if (!isRootFeature(feature)) { - if (feature.getFeatureTree().get().getParentGroup().get().isAlternative()) { - stringBuilder.append(",alternative"); - LEGEND[5] = true; - check = true; - } - } - } - - private boolean isRootFeature(IFeature feature) { - return featureModel.getRootFeatures().contains(feature); - } - - /** - * A Feature is allowed to have a tree. This method checks the children and add them to the StringBuilder - * in LateX (.tex) style. - * - * @param featureTree (the part tree of the feature) - */ - private void printTree(IFeatureTree featureTree) { - IFeature feature = featureTree.getFeature(); - stringBuilder.append("[").append(feature.getName().get()); - insertNodeHead(feature); - for (IFeatureTree featureTreeChildren : featureTree.getChildren()) { - printTree(featureTreeChildren); - } - stringBuilder.append("]"); - // Trees.traverse(featureTree, new PrintVisitor()); - } - /** * Build the complete tree of the FeatureModel. */ public void printForest() { - IFeature feature = featureTree.getFeature(); - stringBuilder.append("\\begin{forest}").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append("\tfeatureDiagram").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append("\t"); - stringBuilder.append("[").append(feature.getName().get()); - insertNodeHead(feature); - for (IFeatureTree featureTreeChildren : featureTree.getChildren()) { - printTree(featureTreeChildren); - } - stringBuilder.append("]"); + + PrintVisitor printVisitor = new PrintVisitor(); + Trees.traverse(featureTree, printVisitor); + stringBuilder.append(printVisitor.getResult().get()); + postProcessing(); stringBuilder.append("\t").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); if (!featureTree.getFeature().isHidden()) { @@ -122,15 +57,28 @@ private void postProcessing() { } private void printLegend() { - if (!check) { - return; + MatrixHelper matrixHelper = new MatrixHelper(MatrixType.LEGEND); + + if (stringBuilder.indexOf(",abstract") != -1 && stringBuilder.indexOf(",concrete") != -1) { + matrixHelper.writeNode("[abstract,label=right:Abstract Feature] {}"); + matrixHelper.writeNode("[concrete,label=right:Concrete Feature] {}"); + } else if (stringBuilder.indexOf(",abstract") != -1) { + matrixHelper.writeNode("[abstract,label=right:Feature] {}"); + } else if (stringBuilder.indexOf(",concrete") != -1) { + matrixHelper.writeNode("[concrete,label=right:Feature] {}"); } - MatrixHelper matrixHelper = getMatrixHelper(); + if (stringBuilder.indexOf(",mandatory") != -1) { + matrixHelper.writeNode("[mandatory,label=right:Mandatory] {}"); + } - if (LEGEND[4]) { + if (stringBuilder.indexOf(",optional") != -1) { + matrixHelper.writeNode("[optional,label=right:Optional] {}"); + } + + if (stringBuilder.indexOf(",or") != -1) { matrixHelper - .writeFillDraw("filldraw[drawColor] (0.45,0.15) ++ (225:0.3) arc[start angle=315,end angle=225,radius=0.2]") + .writeFillDraw("(0.45,0.15) ++ (225:0.3) arc[start angle=315,end angle=225,radius=0.2]") .writeNode("[or,label=right:Or] {}") .writeFillDraw("(0.1,0) - +(-0,-0.2) - +(0.2,-0.2)- +(0.1,0)") .writeDraw("(0.1,0) -- +(-0.2, -0.4)") @@ -139,7 +87,7 @@ private void printLegend() { .writeNode("[or,label=right:Or Group] {}"); } - if (LEGEND[5]) { + if (stringBuilder.indexOf(",and") != -1) { matrixHelper .writeDraw("(0.45,0.15) ++ (225:0.3) arc[start angle=315,end angle=225,radius=0.2] -- cycle") .writeNode("[alternative,label=right:Alternative] {}") @@ -152,34 +100,6 @@ private void printLegend() { stringBuilder.append(matrixHelper.build()); } - private MatrixHelper getMatrixHelper() { - MatrixHelper matrixHelper = new MatrixHelper(MatrixType.LEGEND); - boolean abstractConcreteExists = false; - - if (LEGEND[0] && LEGEND[1]) { - abstractConcreteExists = true; - matrixHelper.writeNode("[abstract,label=right:Abstract Feature] {}"); - matrixHelper.writeNode("[concrete,label=right:Concrete Feature] {}"); - } - - if (LEGEND[0] && !abstractConcreteExists) { - matrixHelper.writeNode("[abstract,label=right:Feature] {}"); - } - - if (LEGEND[1] && !abstractConcreteExists) { - matrixHelper.writeNode("[concrete,label=right:Feature] {}"); - } - - if (LEGEND[2]) { - matrixHelper.writeNode("[mandatory,label=right:Mandatory] {}"); - } - - if (LEGEND[3]) { - matrixHelper.writeNode("[optional,label=right:Optional] {}"); - } - return matrixHelper; - } - private void printConstraints() { ExpressionSerializer expressionSerializer = new ExpressionSerializer(); expressionSerializer.setEnquoteAlways(true); @@ -196,4 +116,4 @@ private void printConstraints() { } stringBuilder.append(" };").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); } -} +} \ No newline at end of file From 0378608e4cd2e40d6da83861db9b82ae47607b3f Mon Sep 17 00:00:00 2001 From: Lara Date: Mon, 13 Oct 2025 10:24:13 +0200 Subject: [PATCH 43/67] fix: legend output --- .../model/io/tikz/format/TikzMainFormat.java | 6 +--- .../model/io/tikz/helper/PrintVisitor.java | 17 ++++----- src/main/resources/test/test-output.tex | 8 +++-- .../io/tikz/FeatureModelDisplayTikzTest.java | 3 +- .../feature/model/io/tikz/HeadLoaderTest.java | 24 ------------- .../io/tikz/helper/MatrixHelperTest.java | 36 ------------------- 6 files changed, 16 insertions(+), 78 deletions(-) delete mode 100644 src/test/java/de/featjar/feature/model/io/tikz/HeadLoaderTest.java delete mode 100644 src/test/java/de/featjar/feature/model/io/tikz/helper/MatrixHelperTest.java diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java index 92827c22..522cd757 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java @@ -78,8 +78,6 @@ private void printLegend() { if (stringBuilder.indexOf(",or") != -1) { matrixHelper - .writeFillDraw("(0.45,0.15) ++ (225:0.3) arc[start angle=315,end angle=225,radius=0.2]") - .writeNode("[or,label=right:Or] {}") .writeFillDraw("(0.1,0) - +(-0,-0.2) - +(0.2,-0.2)- +(0.1,0)") .writeDraw("(0.1,0) -- +(-0.2, -0.4)") .writeDraw("(0.1,0) -- +(0.2,-0.4)") @@ -87,10 +85,8 @@ private void printLegend() { .writeNode("[or,label=right:Or Group] {}"); } - if (stringBuilder.indexOf(",and") != -1) { + if (stringBuilder.indexOf(",alternative") != -1) { matrixHelper - .writeDraw("(0.45,0.15) ++ (225:0.3) arc[start angle=315,end angle=225,radius=0.2] -- cycle") - .writeNode("[alternative,label=right:Alternative] {}") .writeDraw("(0.1,0) -- +(-0.2, -0.4)") .writeDraw("(0.1,0) -- +(0.2,-0.4)") .writeDraw("(0,-0.2) arc (240:300:0.2)") diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java index 91bf2796..a710dc22 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java @@ -53,7 +53,7 @@ private void insertNodeHead(IFeature feature) { stringBuilder.append(",concrete"); } - if (!isRootFeature(feature) && feature.getFeatureTree().isPresent()) { + if (isNotRootFeature(feature) && feature.getFeatureTree().isPresent()) { if (feature.getFeatureTree().get().isMandatory()) { stringBuilder.append(",mandatory"); } else { @@ -61,20 +61,21 @@ private void insertNodeHead(IFeature feature) { } } - if (!isRootFeature(feature)) { - if (feature.getFeatureTree().get().getParentGroup().get().isOr()) { + if (isNotRootFeature(feature) && feature.getFeatureTree().isPresent()) { + if (feature.getFeatureTree().get().getParentGroup().isPresent() && + feature.getFeatureTree().get().getParentGroup().get().isOr()) { stringBuilder.append(",or"); } } - if (!isRootFeature(feature)) { - if (feature.getFeatureTree().get().getParentGroup().get().isAlternative()) { + if (isNotRootFeature(feature) && feature.getFeatureTree().isPresent()) { + if (feature.getFeatureTree().get().getParentGroup().isPresent() && + feature.getFeatureTree().get().getParentGroup().get().isAlternative()) { stringBuilder.append(",alternative"); } } } - private boolean isRootFeature(IFeature feature) { - return feature.getFeatureModel().getRootFeatures().contains(feature); + private boolean isNotRootFeature(IFeature feature) { + return !feature.getFeatureModel().getRootFeatures().contains(feature); } - } \ No newline at end of file diff --git a/src/main/resources/test/test-output.tex b/src/main/resources/test/test-output.tex index cf68a19b..c5f95d8e 100644 --- a/src/main/resources/test/test-output.tex +++ b/src/main/resources/test/test-output.tex @@ -128,7 +128,7 @@ %---The Feature Diagram----------------------------------------------------- \begin{forest} featureDiagram - [Hello,abstract[Feature,concrete,mandatory,or[Wonderful,concrete,optional][Beautiful,concrete,optional]][World,concrete,optional,or]] + [Hello,abstract[Feature,concrete,mandatory,or[Wonderful,concrete,optional,alternative][Beautiful,concrete,optional,alternative]][World,concrete,optional,or]] \matrix [anchor=north west] at (current bounding box.north east) { \node [placeholder] {}; \\ }; @@ -138,13 +138,15 @@ \node [concrete,label=right:Concrete Feature] {}; \\ \node [mandatory,label=right:Mandatory] {}; \\ \node [optional,label=right:Optional] {}; \\ - \filldraw[drawColor] (0.45,0.15) ++ (225:0.3) arc[start angle=315,end angle=225,radius=0.2]; - \node [or,label=right:Or] {}; \\ \filldraw[drawColor] (0.1,0) - +(-0,-0.2) - +(0.2,-0.2)- +(0.1,0); \draw[drawColor] (0.1,0) -- +(-0.2, -0.4); \draw[drawColor] (0.1,0) -- +(0.2,-0.4); \fill[drawColor] (0,-0.2) arc (240:300:0.2); \node [or,label=right:Or Group] {}; \\ + \draw[drawColor] (0.1,0) -- +(-0.2, -0.4); + \draw[drawColor] (0.1,0) -- +(0.2,-0.4); + \draw[drawColor] (0,-0.2) arc (240:300:0.2); + \node [alternative,label=right:Alternative Group] {}; \\ }; \matrix [below=1mm of current bounding box] { \node {\( \text{A} \land \text{B} \)}; \\ diff --git a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java index 768feb1b..1068a9ce 100644 --- a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java +++ b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java @@ -41,6 +41,7 @@ public static void init() { IFeature feature = featureModel.mutate().addFeature("Feature"); IFeatureTree firstFeatureTree = rootTree.mutate().addFeatureBelow(feature); firstFeatureTree.mutate().makeMandatory(); + firstFeatureTree.mutate().toAlternativeGroup(); IFeature world = featureModel.mutate().addFeature("World"); rootTree.mutate().addFeatureBelow(world); @@ -67,8 +68,6 @@ public void perform() { String value = expectedOutput.toString(); - FeatJAR.log().info("Expected Output: " + value); - new TikzGraphicalFeatureModelFormat().serialize(featureModel).ifPresent(output -> { FeatJAR.log().info("Expected Output: " + value); FeatJAR.log().info("Acutally Output: " + output); diff --git a/src/test/java/de/featjar/feature/model/io/tikz/HeadLoaderTest.java b/src/test/java/de/featjar/feature/model/io/tikz/HeadLoaderTest.java deleted file mode 100644 index 79ea96fe..00000000 --- a/src/test/java/de/featjar/feature/model/io/tikz/HeadLoaderTest.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.featjar.feature.model.io.tikz; - -import de.featjar.feature.model.io.tikz.format.TikzHeadFormat; -import org.junit.jupiter.api.Test; - -import java.util.Collections; - -/** - * @author Felix Behme - * @author Lara Merza - * @author Jonas Hanke - */ -public class HeadLoaderTest { - - @Test - public void loadHead() { - StringBuilder stringBuilder = new StringBuilder(); - - TikzHeadFormat.header(stringBuilder, Collections.emptyList(), false); - - System.out.print(stringBuilder); - } - -} diff --git a/src/test/java/de/featjar/feature/model/io/tikz/helper/MatrixHelperTest.java b/src/test/java/de/featjar/feature/model/io/tikz/helper/MatrixHelperTest.java deleted file mode 100644 index dc667d12..00000000 --- a/src/test/java/de/featjar/feature/model/io/tikz/helper/MatrixHelperTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package de.featjar.feature.model.io.tikz.helper; - -import de.featjar.base.FeatJAR; -import org.junit.jupiter.api.Test; - -public class MatrixHelperTest { - - @Test - public void matrix() { - FeatJAR.testConfiguration().initialize(); - - MatrixHelper matrixHelper = new MatrixHelper(MatrixType.LEGEND); - matrixHelper - .writeNode("[abstract,label=right:Abstract Feature] {}") - .writeNode("[concrete,label=right:Concrete Feature] {}") - .writeNode("[abstract,label=right:Feature] {}") - .writeNode("[concrete,label=right:Feature] {}") - .writeNode("[mandatory,label=right:Mandatory] {}") - .writeNode("[optional,label=right:Optional] {}") - // Or Group - .writeFillDraw("filldraw[drawColor] (0.1,0) -- +(-0,-0.2) -- +(0.2,-0.2) -- +(0.1,0)") - .writeDraw("draw[drawColor] (0.1,0) -- +(-0.2, -0.4)") - .writeDraw("draw[drawColor] (0.1,0) -- +(0.2,-0.4)") - .writeFill("fill[drawColor] (0,-0.2) arc (240:300:0.2)") - .writeNode("[or,label=right:Or Group] {}"); - - // Alternative Group - //.writeDraw("draw[drawColor] (0.1,0) -- +(-0.2, -0.4)") - //.writeDraw("draw[drawColor] (0.1,0) -- +(0.2,-0.4)") - //.writeDraw("draw[drawColor] (0,-0.2) arc (240:300:0.2)") - //.writeNode("[alternative,label=right:Alternative Group] {}"); - - FeatJAR.log().info(matrixHelper.build()); - } - -} From 125b2db06376ebc88bcd95f95cd9478462beca21 Mon Sep 17 00:00:00 2001 From: Lara Date: Mon, 13 Oct 2025 16:10:23 +0200 Subject: [PATCH 44/67] feat: added attributes to the tikz format --- .../model/io/tikz/helper/AttributeHelper.java | 105 ++++++++++++++++++ .../model/io/tikz/helper/PrintVisitor.java | 6 +- src/main/resources/head.tex | 2 +- .../io/tikz/FeatureModelDisplayTikzTest.java | 6 + 4 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 src/main/java/de/featjar/feature/model/io/tikz/helper/AttributeHelper.java diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/AttributeHelper.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/AttributeHelper.java new file mode 100644 index 00000000..e64fa31b --- /dev/null +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/AttributeHelper.java @@ -0,0 +1,105 @@ +package de.featjar.feature.model.io.tikz.helper; + +import de.featjar.base.FeatJAR; +import de.featjar.base.data.IAttribute; +import de.featjar.feature.model.IFeature; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +public class AttributeHelper { + + private final static String NAME = "\\underline{{replace}}"; + private final static String SHORTSTACK = "[{\\shortstack{{replace}\\\\" + System.lineSeparator(); + private final static String SCRIPTSIZE = "\\scriptsize {replace}\\\\" + System.lineSeparator(); + private final static String SCRIPTSIZE_END = "\\scriptsize {replace} " + System.lineSeparator(); + private final static String END = "}}"; + + private final StringBuilder stringBuilder; + + private List filter; + + private int size = 0; + private int runs = 0; + + private int frontLength = 0; + private int backLength = 0; + + public AttributeHelper(StringBuilder stringBuilder) { + this.stringBuilder = stringBuilder; + this.filter = new ArrayList<>(); + + makeListUpperCase(); + } + + public AttributeHelper(StringBuilder stringBuilder, List filter) { + this.stringBuilder = stringBuilder; + this.filter = filter; + + makeListUpperCase(); + } + + public void writeAttributes(IFeature feature) { + StringBuilder stringBuilderInternal = new StringBuilder(); + feature.getAttributes().ifPresent(iAttributeObjectMap -> { + size = iAttributeObjectMap.size(); + iAttributeObjectMap.forEach((iAttribute, object) -> writeAttributes(iAttribute, object, stringBuilderInternal)); + }); + String featureName = feature.getName().orElse(""); + if (size == 0) { + this.stringBuilder.append("[").append(replace(NAME, featureName)); + } else { + stringBuilder.append(replace(SHORTSTACK, replace(NAME, featureName))); + stringBuilder.append(stringBuilderInternal); + } + } + + private void writeAttributes(IAttribute attribute, Object object, StringBuilder stringBuilder) { + if (filter.contains(attribute.getName().toUpperCase())) { + size-=1; + return; + } + String format = attribute.getName() + " (" + object.getClass().getSimpleName() + ") = " + object; + runs++; + if (isEnd()) { + stringBuilder.append(replace(SCRIPTSIZE_END, format)); + stringBuilder.append(END); + } else { + stringBuilder.append(replace(SCRIPTSIZE, format)); + } + } + + private String modifyFormat(String first, String second) { + String format = ""; + if (first.length() <= frontLength) { + int diff = frontLength = first.length(); + String space = " ".repeat(diff); + format = space + first; + } + format = format + " = "; + if (second.length() <= backLength) { + int diff = backLength = second.length(); + String space = " ".repeat(diff); + format = format + space + second; + } + + return format; + } + private boolean isEnd() { + return runs == size; + } + + private String replace(String key, String value) { + return key.replace("{replace}", value); + } + + private void makeListUpperCase() { + List upperFilter = new ArrayList<>(); + filter.forEach(value -> { + upperFilter.add(value.toUpperCase()); + }); + filter = upperFilter; + } + +} diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java index a710dc22..6b35967f 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java @@ -5,6 +5,7 @@ import de.featjar.feature.model.IFeature; import de.featjar.feature.model.IFeatureTree; +import java.util.Arrays; import java.util.List; /** @@ -26,9 +27,9 @@ public PrintVisitor() { public TraversalAction firstVisit(List path) { IFeature feature = ITreeVisitor.getCurrentNode(path).getFeature(); - stringBuilder.append("[").append(feature.getName().orElse("")); + new AttributeHelper(stringBuilder/*, Arrays.asList("abstract", "name")*/) + .writeAttributes(feature); insertNodeHead(feature); - return TraversalAction.CONTINUE; } @@ -43,7 +44,6 @@ public Result getResult() { return Result.of(stringBuilder.toString()); } - private void insertNodeHead(IFeature feature) { if (feature.isAbstract()) { stringBuilder.append(",abstract"); diff --git a/src/main/resources/head.tex b/src/main/resources/head.tex index 780bd03e..792cc049 100644 --- a/src/main/resources/head.tex +++ b/src/main/resources/head.tex @@ -23,10 +23,10 @@ }, featureDiagram/.style={ for tree={ - minimum height = 0.6cm, text depth = 0, draw = drawColor, edge = {draw=drawColor}, + anchor=north, {replaceWithVerticalSetting} } }, diff --git a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java index 1068a9ce..cf77181c 100644 --- a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java +++ b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java @@ -1,6 +1,7 @@ package de.featjar.feature.model.io.tikz; import de.featjar.base.FeatJAR; +import de.featjar.base.data.Attribute; import de.featjar.base.data.identifier.Identifiers; import de.featjar.feature.model.*; import de.featjar.formula.structure.Expressions; @@ -46,8 +47,13 @@ public static void init() { IFeature world = featureModel.mutate().addFeature("World"); rootTree.mutate().addFeatureBelow(world); + Attribute attribute = new Attribute<>("any", "test", String.class); + Attribute attribute2 = new Attribute<>("anyd", "testd", String.class); + IFeature wonderful = featureModel.addFeature("Wonderful"); firstFeatureTree.mutate().addFeatureBelow(wonderful); + wonderful.mutate().setAttributeValue(attribute, "value"); + wonderful.mutate().setAttributeValue(attribute2, "value2"); IFeature beautiful = featureModel.addFeature("Beautiful"); firstFeatureTree.mutate().addFeatureBelow(beautiful); From a6971affa5220a1f4cf918863941c57ade8cc7d7 Mon Sep 17 00:00:00 2001 From: Lara Date: Mon, 13 Oct 2025 16:13:02 +0200 Subject: [PATCH 45/67] remove: removed unused method --- .../model/io/tikz/helper/AttributeHelper.java | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/AttributeHelper.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/AttributeHelper.java index e64fa31b..687c2eb0 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/helper/AttributeHelper.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/AttributeHelper.java @@ -70,22 +70,6 @@ private void writeAttributes(IAttribute attribute, Object object, StringBuild } } - private String modifyFormat(String first, String second) { - String format = ""; - if (first.length() <= frontLength) { - int diff = frontLength = first.length(); - String space = " ".repeat(diff); - format = space + first; - } - format = format + " = "; - if (second.length() <= backLength) { - int diff = backLength = second.length(); - String space = " ".repeat(diff); - format = format + space + second; - } - - return format; - } private boolean isEnd() { return runs == size; } From dd4818ac3f80797879b7594c8c3f1bd0595b0488 Mon Sep 17 00:00:00 2001 From: Malena Horstmann Date: Mon, 13 Oct 2025 16:26:45 +0200 Subject: [PATCH 46/67] merged attribute aggregate and translation of cardinality features to bool. Unit tests wip --- .../model/transformer/ComputeFormula.java | 95 +++++----- .../ComputeSimpleFormulaVisitor.java | 19 +- .../ReplaceAttributeAggregate.java | 19 +- .../model/transformer/ComputeFormulaTest.java | 55 ++++++ .../ReplaceAttributeAggregateTest.java | 165 ++++++++++-------- 5 files changed, 230 insertions(+), 123 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java index e0b46024..0373e52f 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java @@ -44,10 +44,6 @@ import de.featjar.formula.structure.connective.Reference; import de.featjar.formula.structure.predicate.Literal; import de.featjar.formula.structure.term.value.Variable; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; import java.util.*; /** @@ -60,6 +56,7 @@ public class ComputeFormula extends AComputation { protected static final Dependency SIMPLE_TRANSLATION = Dependency.newDependency(Boolean.class); static Attribute literalNameAttribute = new Attribute<>("literalName", String.class); + private Boolean hasCardinalityFeatures = Boolean.FALSE; public ComputeFormula(IComputation formula) { super(formula, Computations.of(Boolean.FALSE)); @@ -74,46 +71,27 @@ public Result compute(List dependencyList, Progress progress) IFeatureModel featureModel = FEATURE_MODEL.get(dependencyList); ArrayList constraints = new ArrayList<>(); HashSet variables = new HashSet<>(); -//<<<<<<< HEAD - - IFeatureTree iFeatureTree = featureModel.getRoots().get(0); + Map, Object>> attributes = new LinkedHashMap<>(); if (SIMPLE_TRANSLATION.get(dependencyList)) { - Trees.traverse(iFeatureTree, new ComputeSimpleFormulaVisitor(constraints, variables)); + IFeatureTree iFeatureTree = featureModel.getRoots().get(0); + + ComputeSimpleFormulaVisitor simpleVisitor = + new ComputeSimpleFormulaVisitor(constraints, variables, attributes); + Trees.traverse(iFeatureTree, simpleVisitor); + + hasCardinalityFeatures = simpleVisitor.getHasCardinalityFeature(); } else { - traverseFeatureModel(featureModel, constraints, variables); + traverseFeatureModel(featureModel, constraints, variables, attributes); } -//======= -// Map, Object>> attributes = new LinkedHashMap<>(); -// -// featureModel.getFeatureTreeStream().forEach(node -> { -// // TODO use better error value -// IFeature feature = node.getFeature(); -// String featureName = feature.getName().orElse(""); -// Variable variable = new Variable(featureName, feature.getType()); -// variables.add(variable); -// -// if(node.getAttributes().isPresent()) { -// attributes.put(variable, node.getAttributes().get()); -// } -// -// // TODO take featureRanges into Account -// Result potentialParentTree = node.getParent(); -// Literal featureLiteral = Expressions.literal(featureName); -// if (potentialParentTree.isEmpty()) { -// handleRoot(constraints, featureLiteral, node); -// } else { -// handleParent(constraints, featureLiteral, node); -// } -// handleGroups(constraints, featureLiteral, node); -// }); -// -// ReplaceAttributeAggregate replaceAttributeAggregate = new ReplaceAttributeAggregate(attributes); -// featureModel.getConstraints().forEach(constraint -> { -// Trees.traverse(constraint.getFormula(), replaceAttributeAggregate); -// constraints.add(constraint.getFormula()); -// }); -//>>>>>>> main_malenq + + ReplaceAttributeAggregate replaceAttributeAggregate = + new ReplaceAttributeAggregate(attributes, hasCardinalityFeatures); + featureModel.getConstraints().forEach(constraint -> { + Trees.traverse(constraint.getFormula(), replaceAttributeAggregate); + + constraints.add(constraint.getFormula()); + }); Reference reference = new Reference(new And(constraints)); reference.setFreeVariables(variables); @@ -121,27 +99,52 @@ public Result compute(List dependencyList, Progress progress) } private void traverseFeatureModel( - IFeatureModel featureModel, ArrayList constraints, HashSet variables) { + IFeatureModel featureModel, + ArrayList constraints, + HashSet variables, + Map, Object>> attributes) { for (IFeatureTree root : featureModel.getRoots()) { + // collect the attributes of root + Variable variable = new Variable( + root.getFeature().getName().get(), root.getFeature().getType()); + variables.add(variable); + if (root.getAttributes().isPresent()) { + attributes.put(variable, root.getAttributes().get()); + } + Literal rootLiteral = new Literal(root.getFeature().getName().orElse("")); if (root.isMandatory()) { constraints.add(rootLiteral); } handleGroups(rootLiteral, root, constraints); - addChildConstraints(root, constraints); + addChildConstraints(root, constraints, variables, attributes); } } - private void addChildConstraints(IFeatureTree node, ArrayList constraints) { + private void addChildConstraints( + IFeatureTree node, + ArrayList constraints, + HashSet variables, + Map, Object>> attributes) { + + // collect the attributes of all features + // TODO: check if the variables need to be duplicated? + Variable variable = new Variable( + node.getFeature().getName().get(), node.getFeature().getType()); + variables.add(variable); + if (node.getAttributes().isPresent()) { + attributes.put(variable, node.getAttributes().get()); + } Literal parentLiteral = new Literal(getLiteralName(node)); for (IFeatureTree child : node.getChildren()) { if (isCardinalityFeature(child)) { + hasCardinalityFeatures = Boolean.TRUE; int upperBound = child.getFeatureCardinalityUpperBound(); int lowerBound = child.getFeatureCardinalityLowerBound(); @@ -174,7 +177,7 @@ private void addChildConstraints(IFeatureTree node, ArrayList constrai constraintGroupLiterals.add(currentLiteral); - addChildConstraints(cardinalityClone, constraints); + addChildConstraints(cardinalityClone, constraints, variables, attributes); } // check if 0 and do not add implication if (lowerBound != 0) @@ -198,7 +201,7 @@ private void addChildConstraints(IFeatureTree node, ArrayList constrai // handle group handleGroups(childFeatureLiteral, child, constraints); - addChildConstraints(child, constraints); + addChildConstraints(child, constraints, variables, attributes); } } } @@ -285,4 +288,4 @@ private void handleGroups(Literal featureLiteral, IFeatureTree node, ArrayList constraints = new ArrayList<>(); protected HashSet variables = new HashSet<>(); + private Map, Object>> attributes; + private Boolean hasCardinalityFeature = Boolean.FALSE; - public ComputeSimpleFormulaVisitor(ArrayList constraints, HashSet variables) { + public Boolean getHasCardinalityFeature() { + return hasCardinalityFeature; + } + + public ComputeSimpleFormulaVisitor( + ArrayList constraints, + HashSet variables, + Map, Object>> attributes) { this.constraints = constraints; this.variables = variables; + this.attributes = attributes; } @Override @@ -68,6 +80,10 @@ public TraversalAction firstVisit(List path) { Variable variable = new Variable(featureName, feature.getType()); variables.add(variable); + if (node.getAttributes().isPresent()) { + attributes.put(variable, node.getAttributes().get()); + } + // TODO take featureRanges into Account Result potentialParentTree = node.getParent(); Literal featureLiteral = Expressions.literal(featureName); @@ -97,6 +113,7 @@ private void handleRoot(Literal featureLiteral, IFeatureTree node) { } private void handleCardinalityFeature(Literal featureLiteral, IFeatureTree node) { + hasCardinalityFeature = Boolean.TRUE; int lowerBound = node.getFeatureCardinalityLowerBound(); int upperBound = node.getFeatureCardinalityUpperBound(); diff --git a/src/main/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregate.java b/src/main/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregate.java index 145affcb..f77a3f68 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregate.java +++ b/src/main/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregate.java @@ -8,7 +8,6 @@ import de.featjar.formula.structure.IFormula; import de.featjar.formula.structure.term.aggregate.IAttributeAggregate; import de.featjar.formula.structure.term.value.Variable; - import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -25,19 +24,27 @@ public class ReplaceAttributeAggregate implements ITreeVisitor { private final Map, Object>> attributes; + private boolean hasCardinalityFeatures; - public ReplaceAttributeAggregate(Map, Object>> attributes) { + public ReplaceAttributeAggregate( + Map, Object>> attributes, Boolean hasCardinalityFeatures) { this.attributes = attributes; + this.hasCardinalityFeatures = hasCardinalityFeatures; } @Override public TraversalAction lastVisit(List path) { final IExpression formula = ITreeVisitor.getCurrentNode(path); - if(formula instanceof IAttributeAggregate) { + if (formula instanceof IAttributeAggregate) { + + if (hasCardinalityFeatures) { + throw new RuntimeException("Attribute aggregates and cardinality features can not be translated."); + } + final Result parent = ITreeVisitor.getParentNode(path); - if(parent.isPresent()) { + if (parent.isPresent()) { ArrayList filteredVariables = new ArrayList<>(); ArrayList values = new ArrayList<>(); String attributeFilter = ((IAttributeAggregate) formula).getAttributeFilter(); @@ -54,7 +61,7 @@ public TraversalAction lastVisit(List path) { }); Result result = ((IAttributeAggregate) formula).translate(filteredVariables, values); - if(result.isPresent()) { + if (result.isPresent()) { parent.get().replaceChild(formula, result.get()); } } @@ -67,4 +74,4 @@ public TraversalAction lastVisit(List path) { public Result getResult() { return Result.ofVoid(); } -} \ No newline at end of file +} diff --git a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java index 0fda73f4..4f5e1cfa 100644 --- a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java +++ b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java @@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import de.featjar.base.computation.ComputeConstant; +import de.featjar.base.data.Attribute; import de.featjar.base.data.Range; import de.featjar.base.data.identifier.Identifiers; import de.featjar.feature.model.FeatureModel; @@ -36,7 +37,10 @@ import de.featjar.formula.structure.connective.Implies; import de.featjar.formula.structure.connective.Or; import de.featjar.formula.structure.connective.Reference; +import de.featjar.formula.structure.predicate.LessThan; import de.featjar.formula.structure.predicate.Literal; +import de.featjar.formula.structure.term.aggregate.AttributeSum; +import de.featjar.formula.structure.term.value.Constant; import java.util.Arrays; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -341,6 +345,57 @@ void oneFeature() { executeTest(); } + static Attribute cpuAttribute = new Attribute<>("cpu", Boolean.class); + static Attribute gpuAttribute = new Attribute<>("cpu", Boolean.class); + + @Test + void oneFeatureAndAttributeAggregate() { + + // root + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and add our only child + IFeature childFeature = featureModel.mutate().addFeature("Test1"); + IFeatureTree childFeatureTree = rootTree.mutate().addFeatureBelow(childFeature); + + // TODO: add attributes to aggregate and and corresponding cross-tree-constraint + // add attribute to aggregate + childFeatureTree.mutate().setAttributeValue(cpuAttribute, Boolean.TRUE); + childFeatureTree.mutate().setAttributeValue(gpuAttribute, Boolean.TRUE); + // cross-tree constraint for aggregate testing + IFormula aggregateConstraint = new And( + new Implies( + new Literal("cables"), new LessThan(new AttributeSum("cost"), new Constant(200L, Long.class))), + new Literal("case")); + featureModel.mutate().addConstraint(aggregateConstraint); + + // TODO: add expected to match the constructed model + // expected = new Reference(new And(new Literal("root"), new Implies(new Literal("Test1"), new + // Literal("root")))); + + executeTest(); + } + + @Test + void cardinalityAndAttributeAggregate() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and add our only child + IFeature childFeature = featureModel.mutate().addFeature("Test1"); + IFeatureTree childFeatureTree = rootTree.mutate().addFeatureBelow(childFeature); + childFeatureTree.mutate().setFeatureCardinality(Range.of(0, 4)); + + // TODO: add aggregate constraint + + // TODO: add expected, which is exception + } + @Test void withCardinalityGroup() { IFeatureTree rootTree = diff --git a/src/test/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregateTest.java b/src/test/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregateTest.java index f1af994c..9e5e09be 100644 --- a/src/test/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregateTest.java +++ b/src/test/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregateTest.java @@ -1,5 +1,7 @@ package de.featjar.feature.model.transformer; +import static org.junit.jupiter.api.Assertions.assertTrue; + import de.featjar.base.FeatJAR; import de.featjar.base.data.Attribute; import de.featjar.base.data.IAttribute; @@ -17,15 +19,11 @@ import de.featjar.formula.structure.term.function.RealDivide; import de.featjar.formula.structure.term.value.Constant; import de.featjar.formula.structure.term.value.Variable; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - import java.util.Collections; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; public class ReplaceAttributeAggregateTest { @@ -36,49 +34,54 @@ public static void init() { FeatJAR.testConfiguration().initialize(); attributes = new LinkedHashMap<>(); - attributes.put(new Variable("cpu", Boolean.class), - Map.of( - new Attribute<>("cost", Long.class), 10L, - new Attribute<>("required", Boolean.class), true, - new Attribute<>("power", Float.class), 104.5f - ) - ); - attributes.put(new Variable("gpu", Boolean.class), + attributes.put( + new Variable("cpu", Boolean.class), Map.of( - new Attribute<>("cost", Long.class), 100L, - new Attribute<>("required", Boolean.class), false, - new Attribute<>("power", Float.class), 200.5f - ) - ); - attributes.put(new Variable("ram", Boolean.class), + new Attribute<>("cost", Long.class), + 10L, + new Attribute<>("required", Boolean.class), + true, + new Attribute<>("power", Float.class), + 104.5f)); + attributes.put( + new Variable("gpu", Boolean.class), Map.of( - new Attribute<>("cost", Long.class), 20L, - new Attribute<>("required", Boolean.class), true - ) - ); - attributes.put(new Variable("motherboard", Boolean.class), - Map.of( - new Attribute<>("required", Boolean.class), true, - new Attribute<>("power", Float.class), 3.5f - ) - ); + new Attribute<>("cost", Long.class), + 100L, + new Attribute<>("required", Boolean.class), + false, + new Attribute<>("power", Float.class), + 200.5f)); + attributes.put( + new Variable("ram", Boolean.class), + Map.of(new Attribute<>("cost", Long.class), 20L, new Attribute<>("required", Boolean.class), true)); + attributes.put( + new Variable("motherboard", Boolean.class), + Map.of(new Attribute<>("required", Boolean.class), true, new Attribute<>("power", Float.class), 3.5f)); attributes.put(new Variable("power_supply", Boolean.class), Collections.emptyMap()); } @Test public void test1() { IFormula test = new LessThan(new AttributeSum("cost"), new Constant(200L, Long.class)); - ReplaceAttributeAggregate replaceAttributeAggregate = new ReplaceAttributeAggregate(attributes); + ReplaceAttributeAggregate replaceAttributeAggregate = new ReplaceAttributeAggregate(attributes, Boolean.FALSE); Trees.traverse(test, replaceAttributeAggregate); IFormula comparison = new LessThan( new IntegerAdd( - new IfThenElse(new Variable("cpu", Boolean.class), new Constant(10L, Long.class), new Constant(0L, Long.class)), - new IfThenElse(new Variable("gpu", Boolean.class), new Constant(100L, Long.class), new Constant(0L, Long.class)), - new IfThenElse(new Variable("ram", Boolean.class), new Constant(20L, Long.class), new Constant(0L, Long.class)) - ), - new Constant(200L, Long.class) - ); + new IfThenElse( + new Variable("cpu", Boolean.class), + new Constant(10L, Long.class), + new Constant(0L, Long.class)), + new IfThenElse( + new Variable("gpu", Boolean.class), + new Constant(100L, Long.class), + new Constant(0L, Long.class)), + new IfThenElse( + new Variable("ram", Boolean.class), + new Constant(20L, Long.class), + new Constant(0L, Long.class))), + new Constant(200L, Long.class)); assertTrue(test.equalsTree(comparison)); } @@ -86,28 +89,50 @@ public void test1() { @Test public void test2() { IFormula test = new LessThan(new AttributeSum("cost"), new AttributeAverage("power")); - ReplaceAttributeAggregate replaceAttributeAggregate = new ReplaceAttributeAggregate(attributes); + ReplaceAttributeAggregate replaceAttributeAggregate = new ReplaceAttributeAggregate(attributes, Boolean.FALSE); Trees.traverse(test, replaceAttributeAggregate); IFormula comparison = new LessThan( new IntegerAdd( - new IfThenElse(new Variable("cpu", Boolean.class), new Constant(10L, Long.class), new Constant(0L, Long.class)), - new IfThenElse(new Variable("gpu", Boolean.class), new Constant(100L, Long.class), new Constant(0L, Long.class)), - new IfThenElse(new Variable("ram", Boolean.class), new Constant(20L, Long.class), new Constant(0L, Long.class)) - ), + new IfThenElse( + new Variable("cpu", Boolean.class), + new Constant(10L, Long.class), + new Constant(0L, Long.class)), + new IfThenElse( + new Variable("gpu", Boolean.class), + new Constant(100L, Long.class), + new Constant(0L, Long.class)), + new IfThenElse( + new Variable("ram", Boolean.class), + new Constant(20L, Long.class), + new Constant(0L, Long.class))), new RealDivide( new RealAdd( - new IfThenElse(new Variable("cpu", Boolean.class), new Constant(104.5, Double.class), new Constant(0.0, Double.class)), - new IfThenElse(new Variable("gpu", Boolean.class), new Constant(200.5, Double.class), new Constant(0.0, Double.class)), - new IfThenElse(new Variable("motherboard", Boolean.class), new Constant(3.5, Double.class), new Constant(0.0, Double.class)) - ), + new IfThenElse( + new Variable("cpu", Boolean.class), + new Constant(104.5, Double.class), + new Constant(0.0, Double.class)), + new IfThenElse( + new Variable("gpu", Boolean.class), + new Constant(200.5, Double.class), + new Constant(0.0, Double.class)), + new IfThenElse( + new Variable("motherboard", Boolean.class), + new Constant(3.5, Double.class), + new Constant(0.0, Double.class))), new RealAdd( - new IfThenElse(new Variable("cpu", Boolean.class), new Constant(1.0, Double.class), new Constant(0.0, Double.class)), - new IfThenElse(new Variable("gpu", Boolean.class), new Constant(1.0, Double.class), new Constant(0.0, Double.class)), - new IfThenElse(new Variable("motherboard", Boolean.class), new Constant(1.0, Double.class), new Constant(0.0, Double.class)) - ) - ) - ); + new IfThenElse( + new Variable("cpu", Boolean.class), + new Constant(1.0, Double.class), + new Constant(0.0, Double.class)), + new IfThenElse( + new Variable("gpu", Boolean.class), + new Constant(1.0, Double.class), + new Constant(0.0, Double.class)), + new IfThenElse( + new Variable("motherboard", Boolean.class), + new Constant(1.0, Double.class), + new Constant(0.0, Double.class))))); assertTrue(test.equalsTree(comparison)); } @@ -116,14 +141,9 @@ public void test2() { public void test3() { IFormula test = new And( new Implies( - new Literal("cables"), - new LessThan( - new AttributeSum("cost"), - new Constant(200L, Long.class)) - ), - new Literal("case") - ); - ReplaceAttributeAggregate replaceAttributeAggregate = new ReplaceAttributeAggregate(attributes); + new Literal("cables"), new LessThan(new AttributeSum("cost"), new Constant(200L, Long.class))), + new Literal("case")); + ReplaceAttributeAggregate replaceAttributeAggregate = new ReplaceAttributeAggregate(attributes, Boolean.FALSE); Trees.traverse(test, replaceAttributeAggregate); IFormula comparison = new And( @@ -131,16 +151,21 @@ public void test3() { new Literal("cables"), new LessThan( new IntegerAdd( - new IfThenElse(new Variable("cpu", Boolean.class), new Constant(10L, Long.class), new Constant(0L, Long.class)), - new IfThenElse(new Variable("gpu", Boolean.class), new Constant(100L, Long.class), new Constant(0L, Long.class)), - new IfThenElse(new Variable("ram", Boolean.class), new Constant(20L, Long.class), new Constant(0L, Long.class)) - ), - new Constant(200L, Long.class) - ) - ), - new Literal("case") - ); + new IfThenElse( + new Variable("cpu", Boolean.class), + new Constant(10L, Long.class), + new Constant(0L, Long.class)), + new IfThenElse( + new Variable("gpu", Boolean.class), + new Constant(100L, Long.class), + new Constant(0L, Long.class)), + new IfThenElse( + new Variable("ram", Boolean.class), + new Constant(20L, Long.class), + new Constant(0L, Long.class))), + new Constant(200L, Long.class))), + new Literal("case")); assertTrue(test.equalsTree(comparison)); } -} \ No newline at end of file +} From 9f665bfe8add9235d274bdb018b6ebf0499c3b6c Mon Sep 17 00:00:00 2001 From: Malena Horstmann Date: Tue, 14 Oct 2025 11:02:04 +0200 Subject: [PATCH 47/67] Added unit tests for attribute aggregates in compute formula as well as expected exception tests. Fix of collecting attributes --- .../model/transformer/ComputeFormula.java | 8 +- .../ComputeSimpleFormulaVisitor.java | 5 +- .../ReplaceAttributeAggregate.java | 3 +- .../model/transformer/ComputeFormulaTest.java | 106 +++++++++++++++--- 4 files changed, 98 insertions(+), 24 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java index 0373e52f..dd1f9905 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java @@ -110,8 +110,8 @@ private void traverseFeatureModel( Variable variable = new Variable( root.getFeature().getName().get(), root.getFeature().getType()); variables.add(variable); - if (root.getAttributes().isPresent()) { - attributes.put(variable, root.getAttributes().get()); + if (root.getFeature().getAttributes().isPresent()) { + attributes.put(variable, root.getFeature().getAttributes().get()); } Literal rootLiteral = new Literal(root.getFeature().getName().orElse("")); @@ -135,8 +135,8 @@ private void addChildConstraints( Variable variable = new Variable( node.getFeature().getName().get(), node.getFeature().getType()); variables.add(variable); - if (node.getAttributes().isPresent()) { - attributes.put(variable, node.getAttributes().get()); + if (node.getFeature().getAttributes().isPresent()) { + attributes.put(variable, node.getFeature().getAttributes().get()); } Literal parentLiteral = new Literal(getLiteralName(node)); diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java b/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java index b769f29b..b59ba301 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java @@ -80,8 +80,9 @@ public TraversalAction firstVisit(List path) { Variable variable = new Variable(featureName, feature.getType()); variables.add(variable); - if (node.getAttributes().isPresent()) { - attributes.put(variable, node.getAttributes().get()); + if (node.getFeature().getAttributes().isPresent()) { + // name is an attribute as well + attributes.put(variable, node.getFeature().getAttributes().get()); } // TODO take featureRanges into Account diff --git a/src/main/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregate.java b/src/main/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregate.java index f77a3f68..8da3ea34 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregate.java +++ b/src/main/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregate.java @@ -39,7 +39,8 @@ public TraversalAction lastVisit(List path) { if (formula instanceof IAttributeAggregate) { if (hasCardinalityFeatures) { - throw new RuntimeException("Attribute aggregates and cardinality features can not be translated."); + throw new UnsupportedOperationException( + "Attribute aggregates and cardinality features can not be translated."); } final Result parent = ITreeVisitor.getParentNode(path); diff --git a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java index 4f5e1cfa..52892733 100644 --- a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java +++ b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java @@ -21,6 +21,7 @@ package de.featjar.feature.model.transformer; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; import de.featjar.base.computation.ComputeConstant; import de.featjar.base.data.Attribute; @@ -40,7 +41,10 @@ import de.featjar.formula.structure.predicate.LessThan; import de.featjar.formula.structure.predicate.Literal; import de.featjar.formula.structure.term.aggregate.AttributeSum; +import de.featjar.formula.structure.term.function.IfThenElse; +import de.featjar.formula.structure.term.function.RealAdd; import de.featjar.formula.structure.term.value.Constant; +import de.featjar.formula.structure.term.value.Variable; import java.util.Arrays; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -348,8 +352,10 @@ void oneFeature() { static Attribute cpuAttribute = new Attribute<>("cpu", Boolean.class); static Attribute gpuAttribute = new Attribute<>("cpu", Boolean.class); + static Attribute costAttribute = new Attribute<>("cost", Double.class); + @Test - void oneFeatureAndAttributeAggregate() { + void simpleOneFeatureAndAttributeAggregate() { // root IFeatureTree rootTree = @@ -358,25 +364,25 @@ void oneFeatureAndAttributeAggregate() { rootTree.mutate().toAndGroup(); // create and add our only child - IFeature childFeature = featureModel.mutate().addFeature("Test1"); - IFeatureTree childFeatureTree = rootTree.mutate().addFeatureBelow(childFeature); + IFeature childFeature = featureModel.mutate().addFeature("A"); + rootTree.mutate().addFeatureBelow(childFeature); - // TODO: add attributes to aggregate and and corresponding cross-tree-constraint // add attribute to aggregate - childFeatureTree.mutate().setAttributeValue(cpuAttribute, Boolean.TRUE); - childFeatureTree.mutate().setAttributeValue(gpuAttribute, Boolean.TRUE); + childFeature.mutate().setAttributeValue(costAttribute, 10.0); + // cross-tree constraint for aggregate testing - IFormula aggregateConstraint = new And( - new Implies( - new Literal("cables"), new LessThan(new AttributeSum("cost"), new Constant(200L, Long.class))), - new Literal("case")); + IFormula aggregateConstraint = new LessThan(new AttributeSum("cost"), new Constant(200.0, Double.class)); featureModel.mutate().addConstraint(aggregateConstraint); - // TODO: add expected to match the constructed model - // expected = new Reference(new And(new Literal("root"), new Implies(new Literal("Test1"), new - // Literal("root")))); + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("A"), new Literal("root")), + new LessThan( + new RealAdd(new IfThenElse( + new Variable("A"), new Constant(10.0, Double.class), new Constant(0.0, Double.class))), + new Constant(200.0, Double.class)))); - executeTest(); + executeSimpleTest(); } @Test @@ -387,13 +393,40 @@ void cardinalityAndAttributeAggregate() { rootTree.mutate().toAndGroup(); // create and add our only child - IFeature childFeature = featureModel.mutate().addFeature("Test1"); + IFeature childFeature = featureModel.mutate().addFeature("A"); + IFeatureTree childFeatureTree = rootTree.mutate().addFeatureBelow(childFeature); + childFeatureTree.mutate().setFeatureCardinality(Range.of(0, 4)); + + // add attribute to aggregate + childFeature.mutate().setAttributeValue(costAttribute, 10.0); + + // cross-tree constraint for aggregate testing + IFormula aggregateConstraint = new LessThan(new AttributeSum("cost"), new Constant(200.0, Double.class)); + featureModel.mutate().addConstraint(aggregateConstraint); + + executeExpectedException(); + } + + @Test + void simpleCardinalityAndAttributeAggregate() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and add our only child + IFeature childFeature = featureModel.mutate().addFeature("A"); IFeatureTree childFeatureTree = rootTree.mutate().addFeatureBelow(childFeature); childFeatureTree.mutate().setFeatureCardinality(Range.of(0, 4)); - // TODO: add aggregate constraint + // add attribute to aggregate + childFeature.mutate().setAttributeValue(costAttribute, 10.0); - // TODO: add expected, which is exception + // cross-tree constraint for aggregate testing + IFormula aggregateConstraint = new LessThan(new AttributeSum("cost"), new Constant(200.0, Double.class)); + featureModel.mutate().addConstraint(aggregateConstraint); + + executeSimpleExpectedException(); } @Test @@ -476,4 +509,43 @@ private void executeSimpleTest() { // assert assertEquals(expected, resultFormula); } + + private void executeExpectedException() { + ComputeConstant computeConstant = new ComputeConstant(featureModel); + ComputeFormula computeFormula = new ComputeFormula(computeConstant); + + try { + computeFormula.computeResult().get(); + + } + // TODO: UnsupportedOperationException is the one we want to catch. Internally, this is changed to + // NoSuchElementException: no object present + // The specific exception we want to catch is thrown in ReplaceAttributeAggregate. It is thrown but not present + // in stack trace + // catch (UnsupportedOperationException re) { + catch (RuntimeException re) { + assertTrue(Boolean.TRUE); + } + } + + private void executeSimpleExpectedException() { + ComputeConstant computeConstant = new ComputeConstant(featureModel); + ComputeFormula computeFormula = new ComputeFormula(computeConstant); + + try { + computeFormula + .set(ComputeFormula.SIMPLE_TRANSLATION, Boolean.TRUE) + .computeResult() + .get(); + + } + // TODO: UnsupportedOperationException is the one we want to catch. Internally, this is changed to + // NoSuchElementException: no object present + // The specific exception we want to catch is thrown in ReplaceAttributeAggregate. It is thrown but not present + // in stack trace + // catch (UnsupportedOperationException re) { + catch (RuntimeException re) { + assertTrue(Boolean.TRUE); + } + } } From bf958c772b03487fea0d0b3636309cb6f7c3bd7d Mon Sep 17 00:00:00 2001 From: Malena Horstmann Date: Tue, 14 Oct 2025 12:33:46 +0200 Subject: [PATCH 48/67] better error handling in unit test --- .../model/transformer/ComputeFormulaTest.java | 36 +++++-------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java index 52892733..6a5413ce 100644 --- a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java +++ b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java @@ -21,7 +21,7 @@ package de.featjar.feature.model.transformer; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; import de.featjar.base.computation.ComputeConstant; import de.featjar.base.data.Attribute; @@ -514,38 +514,18 @@ private void executeExpectedException() { ComputeConstant computeConstant = new ComputeConstant(featureModel); ComputeFormula computeFormula = new ComputeFormula(computeConstant); - try { - computeFormula.computeResult().get(); - - } - // TODO: UnsupportedOperationException is the one we want to catch. Internally, this is changed to - // NoSuchElementException: no object present - // The specific exception we want to catch is thrown in ReplaceAttributeAggregate. It is thrown but not present - // in stack trace - // catch (UnsupportedOperationException re) { - catch (RuntimeException re) { - assertTrue(Boolean.TRUE); - } + assertThrows( + UnsupportedOperationException.class, + () -> computeFormula.computeResult().orElseThrow()); } private void executeSimpleExpectedException() { ComputeConstant computeConstant = new ComputeConstant(featureModel); ComputeFormula computeFormula = new ComputeFormula(computeConstant); - try { - computeFormula - .set(ComputeFormula.SIMPLE_TRANSLATION, Boolean.TRUE) - .computeResult() - .get(); - - } - // TODO: UnsupportedOperationException is the one we want to catch. Internally, this is changed to - // NoSuchElementException: no object present - // The specific exception we want to catch is thrown in ReplaceAttributeAggregate. It is thrown but not present - // in stack trace - // catch (UnsupportedOperationException re) { - catch (RuntimeException re) { - assertTrue(Boolean.TRUE); - } + assertThrows(UnsupportedOperationException.class, () -> computeFormula + .set(ComputeFormula.SIMPLE_TRANSLATION, Boolean.TRUE) + .computeResult() + .orElseThrow()); } } From 9d2cfccde435f490a7b17f1334b2d537c4dcdb73 Mon Sep 17 00:00:00 2001 From: Jonas Hanke Date: Tue, 14 Oct 2025 16:44:19 +0200 Subject: [PATCH 49/67] feat: added feature cardinalities --- .../tikz/TikzGraphicalFeatureModelFormat.java | 2 +- .../model/io/tikz/helper/PrintVisitor.java | 33 ++- src/main/resources/head.tex | 235 ++++++++++-------- 3 files changed, 157 insertions(+), 113 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java index af8c9122..12221dbb 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java @@ -54,4 +54,4 @@ public String getFileExtension() { public String getName() { return "LaTeX-Document with TikZ"; } -} +} \ No newline at end of file diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java index 6b35967f..6a818cd2 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java @@ -27,8 +27,10 @@ public PrintVisitor() { public TraversalAction firstVisit(List path) { IFeature feature = ITreeVisitor.getCurrentNode(path).getFeature(); - new AttributeHelper(stringBuilder/*, Arrays.asList("abstract", "name")*/) - .writeAttributes(feature); + //new AttributeHelper(stringBuilder/*, Arrays.asList("abstract", "name")*/) + // .writeAttributes(feature); + stringBuilder.append("["); + insertAttributes(feature); insertNodeHead(feature); return TraversalAction.CONTINUE; } @@ -44,6 +46,25 @@ public Result getResult() { return Result.of(stringBuilder.toString()); } + private void insertAttributes(IFeature feature) { + if(feature.getAttributes().isPresent() && !feature.getAttributes().get().isEmpty()) { + stringBuilder.append(String.format("\\multicolumn{2}{c}{%s} \\\\\\hline", + feature.getName().orElse(""))); + + feature.getAttributes().get().forEach((attribute, value) -> { + if(!attribute.getName().equals("name")) { + stringBuilder.append(String.format("\\small\\texttt{%s (%s)} &\\small\\texttt{= %s} \\\\", + attribute.getName(), attribute.getType().getSimpleName(), + value.toString())); + } + }); + + stringBuilder.append(",align=ll"); + } else { + stringBuilder.append(feature.getName().orElse("")); + } + } + private void insertNodeHead(IFeature feature) { if (feature.isAbstract()) { stringBuilder.append(",abstract"); @@ -53,7 +74,13 @@ private void insertNodeHead(IFeature feature) { stringBuilder.append(",concrete"); } - if (isNotRootFeature(feature) && feature.getFeatureTree().isPresent()) { + if(feature.getFeatureTree().isPresent()) { + if (feature.getFeatureTree().get().getFeatureCardinalityUpperBound() > 1) { + stringBuilder.append(String.format(",featurecardinality={%d}{%d}", + feature.getFeatureTree().get().getFeatureCardinalityLowerBound(), + feature.getFeatureTree().get().getFeatureCardinalityUpperBound())); + } + if (feature.getFeatureTree().get().isMandatory()) { stringBuilder.append(",mandatory"); } else { diff --git a/src/main/resources/head.tex b/src/main/resources/head.tex index 792cc049..2471d261 100644 --- a/src/main/resources/head.tex +++ b/src/main/resources/head.tex @@ -4,119 +4,136 @@ \usepackage{xcolor} \usetikzlibrary{angles} \usetikzlibrary{positioning} +\usetikzlibrary{quotes} \definecolor{drawColor}{RGB}{128 128 128} \newcommand{\circleSize}{0.25em} \newcommand{\angleSize}{0.8em} +\newcommand{\angleSizeCardinalityGroup}{1.0em} %------------------------------------------------------------------------------- %---Define the style of the tree------------------------------------------------ \forestset{ - /tikz/mandatory/.style={ - circle,fill=drawColor, - draw=drawColor, - inner sep=\circleSize - }, - /tikz/optional/.style={ - circle, - fill=white, - draw=drawColor, - inner sep=\circleSize - }, - featureDiagram/.style={ - for tree={ - text depth = 0, - draw = drawColor, - edge = {draw=drawColor}, - anchor=north, - {replaceWithVerticalSetting} - } - }, - /tikz/redColor/.style={ - fill = red!60, - draw = drawColor - }, - /tikz/orangeColor/.style={ - fill = orange!50, - draw = drawColor - }, - /tikz/yellowColor/.style={ - fill = yellow!50, - draw = drawColor - }, - /tikz/darkGreenColor/.style={ - fill = black!30!green, - draw = drawColor - }, - /tikz/lightGreenColor/.style={ - fill = green!30, - draw = drawColor - }, - /tikz/cyanColor/.style={ - fill = cyan!30, - draw = drawColor - }, - /tikz/lightGrayColor/.style={ - fill = black!10, - draw = drawColor - }, - /tikz/blueColor/.style={ - fill = blue!50, - draw = drawColor - }, - /tikz/magentaColor/.style={ - fill = magenta, - draw = drawColor - }, - /tikz/pinkColor/.style={ - fill = pink!90, - draw = drawColor - }, - /tikz/abstract/.style={ - fill = blue!85!cyan!5, - draw = drawColor - }, - /tikz/concrete/.style={ - fill = blue!85!cyan!20, - draw = drawColor - }, - mandatory/.style={ - edge label={node [mandatory] {} } - }, - optional/.style={ - edge label={node [optional] {} } - }, - or/.style={ - tikz+={ - \path (.parent) coordinate (A) -- (!u.children) coordinate (B) -- (!ul.parent) coordinate (C) pic[fill=drawColor, angle radius=\angleSize]{angle}; - } - }, - /tikz/or/.style={ - }, - alternative/.style={ - tikz+={ - \path (.parent) coordinate (A) -- (!u.children) coordinate (B) -- (!ul.parent) coordinate (C) pic[draw=drawColor, angle radius=\angleSize]{angle}; - } - }, - /tikz/alternative/.style={ - }, - /tikz/placeholder/.style={ - }, - collapsed/.style={ - rounded corners, - no edge, - for tree={ - fill opacity=0, - draw opacity=0, - l = 0em, - } - }, - /tikz/hiddenNodes/.style={ - midway, - rounded corners, - draw=drawColor, - fill=white, - minimum size = 1.2em, - minimum width = 0.8em, - scale=0.9 - }, + /tikz/mandatory/.style={ + circle,fill=drawColor, + draw=drawColor, + inner sep=\circleSize + }, + /tikz/optional/.style={ + circle, + fill=white, + draw=drawColor, + inner sep=\circleSize + }, + featureDiagram/.style={ + for tree={ + draw = drawColor, + edge = {draw=drawColor}, + anchor=north, + parent anchor = south, + child anchor = north, + l sep = 2em, + s sep = 1em, + } + }, + /tikz/redColor/.style={ + fill = red!60, + draw = drawColor + }, + /tikz/orangeColor/.style={ + fill = orange!50, + draw = drawColor + }, + /tikz/yellowColor/.style={ + fill = yellow!50, + draw = drawColor + }, + /tikz/darkGreenColor/.style={ + fill = black!30!green, + draw = drawColor + }, + /tikz/lightGreenColor/.style={ + fill = green!30, + draw = drawColor + }, + /tikz/cyanColor/.style={ + fill = cyan!30, + draw = drawColor + }, + /tikz/lightGrayColor/.style={ + fill = black!10, + draw = drawColor + }, + /tikz/blueColor/.style={ + fill = blue!50, + draw = drawColor + }, + /tikz/magentaColor/.style={ + fill = magenta, + draw = drawColor + }, + /tikz/pinkColor/.style={ + fill = pink!90, + draw = drawColor + }, + /tikz/abstract/.style={ + fill = blue!85!cyan!5, + draw = drawColor + }, + /tikz/concrete/.style={ + fill = blue!85!cyan!20, + draw = drawColor + }, + mandatory/.style={ + edge label+={node [mandatory] {} } + }, + optional/.style={ + edge label+={node [optional] {} } + }, + featurecardinality/.style 2 args={ + edge label+={node[midway,fill=white,font=\scriptsize]{#1,#2}} + }, + or/.style={ + tikz+={ + \path (!c1.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!cl.parent) coordinate (C) pic[fill=drawColor, angle radius=\angleSize]{angle}; + } + }, + alternative/.style={ + tikz+={ + \path (!c1.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!cl.parent) coordinate (C) pic[draw=drawColor, angle radius=\angleSize]{angle}; + } + }, + groupcardinality/.style 2 args={ + tikz+={ + \path (!c1.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!cl.parent) coordinate (C) pic[ + draw=drawColor, + angle radius=\angleSizeCardinalityGroup, + pic text={#1,#2}, + pic text options={ + scale=0.6, + fill=white, + inner sep=0.5pt + } + ]{angle} + } + }, + /tikz/placeholder/.style={ + }, + collapsed/.style={ + rounded corners, + no edge, + for tree={ + fill opacity=0, + draw opacity=0, + l = 0em, + } + }, + /tikz/hiddenNodes/.style={ + midway, + rounded corners, + draw=drawColor, + fill=white, + minimum size = 1.2em, + minimum width = 0.8em, + scale=0.9 + }, } -%------------------------------------------------------------------------------- +%------------------------------------------------------------------------------- \ No newline at end of file From dbe1ac85925efb8a7193eb5c815e99c98a35470d Mon Sep 17 00:00:00 2001 From: Lara Date: Wed, 15 Oct 2025 15:18:14 +0200 Subject: [PATCH 50/67] feat: added class descirption --- .../de/featjar/feature/model/io/tikz/AttributeFilterTest.java | 4 ++++ .../feature/model/io/tikz/FeatureModelDisplayTikzTest.java | 2 ++ 2 files changed, 6 insertions(+) create mode 100644 src/test/java/de/featjar/feature/model/io/tikz/AttributeFilterTest.java diff --git a/src/test/java/de/featjar/feature/model/io/tikz/AttributeFilterTest.java b/src/test/java/de/featjar/feature/model/io/tikz/AttributeFilterTest.java new file mode 100644 index 00000000..aa10009b --- /dev/null +++ b/src/test/java/de/featjar/feature/model/io/tikz/AttributeFilterTest.java @@ -0,0 +1,4 @@ +package de.featjar.feature.model.io.tikz; + +public class AttributeFilterTest { +} diff --git a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java index cf77181c..82ccbe6d 100644 --- a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java +++ b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java @@ -17,6 +17,8 @@ import java.nio.file.Paths; /** + * Test the full output with a test feature model and attributes, constrains and more + * * @author Felix Behme * @author Lara Merza * @author Jonas Hanke From f57f27b5452995ef54924d6831dfcd75059dc262 Mon Sep 17 00:00:00 2001 From: Lara Date: Wed, 15 Oct 2025 15:19:13 +0200 Subject: [PATCH 51/67] feat: added new attribute test --- .../model/io/tikz/AttributeFilterTest.java | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/src/test/java/de/featjar/feature/model/io/tikz/AttributeFilterTest.java b/src/test/java/de/featjar/feature/model/io/tikz/AttributeFilterTest.java index aa10009b..b9ce8b1f 100644 --- a/src/test/java/de/featjar/feature/model/io/tikz/AttributeFilterTest.java +++ b/src/test/java/de/featjar/feature/model/io/tikz/AttributeFilterTest.java @@ -1,4 +1,117 @@ package de.featjar.feature.model.io.tikz; +import de.featjar.base.FeatJAR; +import de.featjar.base.data.Attribute; +import de.featjar.base.data.identifier.Identifiers; +import de.featjar.feature.model.Feature; +import de.featjar.feature.model.FeatureModel; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.io.tikz.helper.AttributeHelper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Test for displaying the attributes in tikz (filter) + * + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ public class AttributeFilterTest { + + private static IFeatureModel featureModel; + private static IFeature feature; + + private final String FIRST_TEST_OUTPUT = "[\\multicolumn{2}{c}{attribute-test} \\\\\\hline\n" + + "\\small\\texttt{int-attribute (Integer)} &\\small\\texttt{= 1} \\\\\n" + + ",align=ll"; + private final String SECOND_TEST_OUTPUT = "[\\multicolumn{2}{c}{attribute-test} \\\\\\hline\n" + + "\\small\\texttt{int-attribute (Integer)} &\\small\\texttt{= 1} \\\\\n" + + "\\small\\texttt{bool-attribute (Boolean)} &\\small\\texttt{= true} \\\\\n" + + ",align=ll"; + private final String THIRD_TEST_OUTPUT = "[\\multicolumn{2}{c}{attribute-test} \\\\\\hline\n" + + "\\small\\texttt{name (String)} &\\small\\texttt{= attribute-test} \\\\\n" + + "\\small\\texttt{bool-attribute (Boolean)} &\\small\\texttt{= true} \\\\\n" + + "\\small\\texttt{string-attribute (String)} &\\small\\texttt{= hallo} \\\\\n" + + ",align=ll"; + private final String FOURTH_TEST_OUTPUT = "[\\multicolumn{2}{c}{attribute-test} \\\\\\hline\n" + + "\\small\\texttt{name (String)} &\\small\\texttt{= attribute-test} \\\\\n" + + "\\small\\texttt{string-attribute (String)} &\\small\\texttt{= hallo} \\\\\n" + + ",align=ll"; + + @BeforeAll + public static void init() { + FeatJAR.testConfiguration().initialize(); + + FeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + + IFeature feature = featureModel.addFeature("attribute-test"); + + Attribute attribute = new Attribute<>("test", "int-attribute", Integer.class); + Attribute attribute2 = new Attribute<>("test", "bool-attribute", Boolean.class); + Attribute attribute3 = new Attribute<>("test", "string-attribute", String.class); + + feature.mutate().setAttributeValue(attribute, 1); + feature.mutate().setAttributeValue(attribute2, true); + feature.mutate().setAttributeValue(attribute3, "hallo"); + + AttributeFilterTest.featureModel = featureModel; + AttributeFilterTest.feature = feature; + } + + @Test + public void test1() { + StringBuilder stringBuilder = new StringBuilder(); + + AttributeHelper attributeHelper = new AttributeHelper(feature, stringBuilder) + .setFilterType(AttributeHelper.FilterType.DISPLAY) + .addFilterValue("int-attribute"); + + attributeHelper.build(); + + Assertions.assertEquals(stringBuilder.toString(), FIRST_TEST_OUTPUT); + } + + @Test + public void test2() { + StringBuilder stringBuilder = new StringBuilder(); + + AttributeHelper attributeHelper = new AttributeHelper(feature, stringBuilder) + .setFilterType(AttributeHelper.FilterType.DISPLAY) + .addFilterValue("int-attribute", "bool-attribute"); + + attributeHelper.build(); + + Assertions.assertEquals(stringBuilder.toString(), SECOND_TEST_OUTPUT); + } + + @Test + public void test3() { + StringBuilder stringBuilder = new StringBuilder(); + + AttributeHelper attributeHelper = new AttributeHelper(feature, stringBuilder) + .setFilterType(AttributeHelper.FilterType.WITH_OUT) + .addFilterValue("int-attribute"); + + attributeHelper.build(); + + Assertions.assertEquals(stringBuilder.toString(), THIRD_TEST_OUTPUT); + } + + @Test + public void test4() { + StringBuilder stringBuilder = new StringBuilder(); + + AttributeHelper attributeHelper = new AttributeHelper(feature, stringBuilder) + .setFilterType(AttributeHelper.FilterType.WITH_OUT) + .addFilterValue("int-attribute", "bool-attribute"); + + attributeHelper.build(); + + Assertions.assertEquals(stringBuilder.toString(), FOURTH_TEST_OUTPUT); + } + } From 9213392eaa1330b22f17746d42014f48cfb1e91e Mon Sep 17 00:00:00 2001 From: Lara Date: Wed, 15 Oct 2025 15:19:46 +0200 Subject: [PATCH 52/67] update test file --- src/main/resources/test/test-output.tex | 254 +++++++++++++----------- 1 file changed, 137 insertions(+), 117 deletions(-) diff --git a/src/main/resources/test/test-output.tex b/src/main/resources/test/test-output.tex index c5f95d8e..6ff7afd5 100644 --- a/src/main/resources/test/test-output.tex +++ b/src/main/resources/test/test-output.tex @@ -5,130 +5,154 @@ \usepackage{xcolor} \usetikzlibrary{angles} \usetikzlibrary{positioning} +\usetikzlibrary{quotes} \definecolor{drawColor}{RGB}{128 128 128} \newcommand{\circleSize}{0.25em} \newcommand{\angleSize}{0.8em} +\newcommand{\angleSizeCardinalityGroup}{1.0em} %------------------------------------------------------------------------------- %---Define the style of the tree------------------------------------------------ \forestset{ - /tikz/mandatory/.style={ - circle,fill=drawColor, - draw=drawColor, - inner sep=\circleSize - }, - /tikz/optional/.style={ - circle, - fill=white, - draw=drawColor, - inner sep=\circleSize - }, - featureDiagram/.style={ - for tree={ - minimum height = 0.6cm, - text depth = 0, - draw = drawColor, - edge = {draw=drawColor}, - parent anchor = south, - child anchor = north, - l sep = 2em, - s sep = 1em, - } - }, - /tikz/redColor/.style={ - fill = red!60, - draw = drawColor - }, - /tikz/orangeColor/.style={ - fill = orange!50, - draw = drawColor - }, - /tikz/yellowColor/.style={ - fill = yellow!50, - draw = drawColor - }, - /tikz/darkGreenColor/.style={ - fill = black!30!green, - draw = drawColor - }, - /tikz/lightGreenColor/.style={ - fill = green!30, - draw = drawColor - }, - /tikz/cyanColor/.style={ - fill = cyan!30, - draw = drawColor - }, - /tikz/lightGrayColor/.style={ - fill = black!10, - draw = drawColor - }, - /tikz/blueColor/.style={ - fill = blue!50, - draw = drawColor - }, - /tikz/magentaColor/.style={ - fill = magenta, - draw = drawColor - }, - /tikz/pinkColor/.style={ - fill = pink!90, - draw = drawColor - }, - /tikz/abstract/.style={ - fill = blue!85!cyan!5, - draw = drawColor - }, - /tikz/concrete/.style={ - fill = blue!85!cyan!20, - draw = drawColor - }, - mandatory/.style={ - edge label={node [mandatory] {} } - }, - optional/.style={ - edge label={node [optional] {} } - }, - or/.style={ - tikz+={ - \path (.parent) coordinate (A) -- (!u.children) coordinate (B) -- (!ul.parent) coordinate (C) pic[fill=drawColor, angle radius=\angleSize]{angle}; - } - }, - /tikz/or/.style={ - }, - alternative/.style={ - tikz+={ - \path (.parent) coordinate (A) -- (!u.children) coordinate (B) -- (!ul.parent) coordinate (C) pic[draw=drawColor, angle radius=\angleSize]{angle}; - } - }, - /tikz/alternative/.style={ - }, - /tikz/placeholder/.style={ - }, - collapsed/.style={ - rounded corners, - no edge, - for tree={ - fill opacity=0, - draw opacity=0, - l = 0em, - } - }, - /tikz/hiddenNodes/.style={ - midway, - rounded corners, - draw=drawColor, - fill=white, - minimum size = 1.2em, - minimum width = 0.8em, - scale=0.9 - }, + /tikz/mandatory/.style={ + circle,fill=drawColor, + draw=drawColor, + inner sep=\circleSize + }, + /tikz/optional/.style={ + circle, + fill=white, + draw=drawColor, + inner sep=\circleSize + }, + featureDiagram/.style={ + for tree={ + draw = drawColor, + edge = {draw=drawColor}, + anchor=north, + parent anchor = south, + child anchor = north, + l sep = 2em, + s sep = 1em, + } + }, + /tikz/redColor/.style={ + fill = red!60, + draw = drawColor + }, + /tikz/orangeColor/.style={ + fill = orange!50, + draw = drawColor + }, + /tikz/yellowColor/.style={ + fill = yellow!50, + draw = drawColor + }, + /tikz/darkGreenColor/.style={ + fill = black!30!green, + draw = drawColor + }, + /tikz/lightGreenColor/.style={ + fill = green!30, + draw = drawColor + }, + /tikz/cyanColor/.style={ + fill = cyan!30, + draw = drawColor + }, + /tikz/lightGrayColor/.style={ + fill = black!10, + draw = drawColor + }, + /tikz/blueColor/.style={ + fill = blue!50, + draw = drawColor + }, + /tikz/magentaColor/.style={ + fill = magenta, + draw = drawColor + }, + /tikz/pinkColor/.style={ + fill = pink!90, + draw = drawColor + }, + /tikz/abstract/.style={ + fill = blue!85!cyan!5, + draw = drawColor + }, + /tikz/concrete/.style={ + fill = blue!85!cyan!20, + draw = drawColor + }, + mandatory/.style={ + edge label+={node [mandatory] {} } + }, + optional/.style={ + edge label+={node [optional] {} } + }, + featurecardinality/.style 2 args={ + edge label+={node[midway,fill=white,font=\scriptsize]{#1,#2}} + }, + or/.style={ + tikz+={ + \path (!c1.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!cl.parent) coordinate (C) pic[fill=drawColor, angle radius=\angleSize]{angle}; + } + }, + alternative/.style={ + tikz+={ + \path (!c1.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!cl.parent) coordinate (C) pic[draw=drawColor, angle radius=\angleSize]{angle}; + } + }, + groupcardinality/.style 2 args={ + tikz+={ + \path (!c1.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!cl.parent) coordinate (C) pic[ + draw=drawColor, + angle radius=\angleSizeCardinalityGroup, + pic text={#1,#2}, + pic text options={ + scale=0.6, + fill=white, + inner sep=0.5pt + } + ]{angle} + } + }, + /tikz/placeholder/.style={ + }, + collapsed/.style={ + rounded corners, + no edge, + for tree={ + fill opacity=0, + draw opacity=0, + l = 0em, + } + }, + /tikz/hiddenNodes/.style={ + midway, + rounded corners, + draw=drawColor, + fill=white, + minimum size = 1.2em, + minimum width = 0.8em, + scale=0.9 + }, } %------------------------------------------------------------------------------- \begin{document} %---The Feature Diagram----------------------------------------------------- \begin{forest} featureDiagram - [Hello,abstract[Feature,concrete,mandatory,or[Wonderful,concrete,optional,alternative][Beautiful,concrete,optional,alternative]][World,concrete,optional,or]] + [\multicolumn{2}{c}{Hello} \\\hline +\small\texttt{name (String)} &\small\texttt{= Hello} \\ +,align=ll,abstract,optional[\multicolumn{2}{c}{Feature} \\\hline +\small\texttt{name (String)} &\small\texttt{= Feature} \\ +,align=ll,concrete,mandatory,or[\multicolumn{2}{c}{Wonderful} \\\hline +\small\texttt{name (String)} &\small\texttt{= Wonderful} \\ +,align=ll,concrete,optional][\multicolumn{2}{c}{Beautiful} \\\hline +\small\texttt{name (String)} &\small\texttt{= Beautiful} \\ +,align=ll,concrete,optional]][\multicolumn{2}{c}{World} \\\hline +\small\texttt{name (String)} &\small\texttt{= World} \\ +,align=ll,concrete,optional]] \matrix [anchor=north west] at (current bounding box.north east) { \node [placeholder] {}; \\ }; @@ -142,11 +166,7 @@ \draw[drawColor] (0.1,0) -- +(-0.2, -0.4); \draw[drawColor] (0.1,0) -- +(0.2,-0.4); \fill[drawColor] (0,-0.2) arc (240:300:0.2); - \node [or,label=right:Or Group] {}; \\ - \draw[drawColor] (0.1,0) -- +(-0.2, -0.4); - \draw[drawColor] (0.1,0) -- +(0.2,-0.4); - \draw[drawColor] (0,-0.2) arc (240:300:0.2); - \node [alternative,label=right:Alternative Group] {}; \\ + \node [label=right:Or Group] {}; \\ }; \matrix [below=1mm of current bounding box] { \node {\( \text{A} \land \text{B} \)}; \\ From 50fa621995ef28ddab263bd8d26f82381de8ed37 Mon Sep 17 00:00:00 2001 From: Lara Date: Wed, 15 Oct 2025 15:20:11 +0200 Subject: [PATCH 53/67] feat: added attribute helper and comments in the classes. --- .../model/io/tikz/color/FeatureColor.java | 2 + .../model/io/tikz/format/TikzMainFormat.java | 2 +- .../model/io/tikz/helper/AttributeHelper.java | 141 ++++++++++++------ .../model/io/tikz/helper/PrintVisitor.java | 48 +++--- 4 files changed, 118 insertions(+), 75 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/io/tikz/color/FeatureColor.java b/src/main/java/de/featjar/feature/model/io/tikz/color/FeatureColor.java index e08c38c3..27b05b7d 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/color/FeatureColor.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/color/FeatureColor.java @@ -1,6 +1,8 @@ package de.featjar.feature.model.io.tikz.color; /** + * Color in tikz for later implemntation (colors doesnt exists in features in the moment) + * * @author Felix Behme * @author Lara Merza * @author Jonas Hanke diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java index 522cd757..d7f5c7ee 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java @@ -82,7 +82,7 @@ private void printLegend() { .writeDraw("(0.1,0) -- +(-0.2, -0.4)") .writeDraw("(0.1,0) -- +(0.2,-0.4)") .writeFill("(0,-0.2) arc (240:300:0.2)") - .writeNode("[or,label=right:Or Group] {}"); + .writeNode("[label=right:Or Group] {}"); } if (stringBuilder.indexOf(",alternative") != -1) { diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/AttributeHelper.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/AttributeHelper.java index 687c2eb0..68e76c10 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/helper/AttributeHelper.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/AttributeHelper.java @@ -1,89 +1,136 @@ package de.featjar.feature.model.io.tikz.helper; -import de.featjar.base.FeatJAR; import de.featjar.base.data.IAttribute; import de.featjar.feature.model.IFeature; import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; import java.util.List; - +import java.util.Map; + +/** + * This class allows you to display the attributes of a feature. You can also filter specific names to display + * in the tikz file. + * + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ public class AttributeHelper { - private final static String NAME = "\\underline{{replace}}"; - private final static String SHORTSTACK = "[{\\shortstack{{replace}\\\\" + System.lineSeparator(); - private final static String SCRIPTSIZE = "\\scriptsize {replace}\\\\" + System.lineSeparator(); - private final static String SCRIPTSIZE_END = "\\scriptsize {replace} " + System.lineSeparator(); - private final static String END = "}}"; + private final String MULTICOLUMN = "\\multicolumn{2}{c}{{0}} \\\\\\hline" + System.lineSeparator(); + private final String VALUE = "\\small\\texttt{{0} ({1})} &\\small\\texttt{= {2}} \\\\" + System.lineSeparator(); + private final IFeature feature; private final StringBuilder stringBuilder; private List filter; + private FilterType filterType = FilterType.WITH_OUT; // FALLBACK - private int size = 0; - private int runs = 0; - - private int frontLength = 0; - private int backLength = 0; - - public AttributeHelper(StringBuilder stringBuilder) { + public AttributeHelper(IFeature feature, StringBuilder stringBuilder) { + this.feature = feature; this.stringBuilder = stringBuilder; this.filter = new ArrayList<>(); - - makeListUpperCase(); } - public AttributeHelper(StringBuilder stringBuilder, List filter) { + public AttributeHelper(IFeature feature, StringBuilder stringBuilder, List filter) { + this.feature = feature; this.stringBuilder = stringBuilder; this.filter = filter; + } - makeListUpperCase(); + /** + * Set a filter type to customize your output for the attributes + * @param filterType (DISPLAY or WITH_OUT) + * DISPLAY: Shows every value in the filter + * WITH_OUT: Shows every value that isn't in the filter + * @return this class + */ + public AttributeHelper setFilterType(FilterType filterType) { + this.filterType = filterType; + return this; + } + + /** + * + * @param values (the keys, words or whatever that will be filtered in the running process. + * @return this + */ + public AttributeHelper addFilterValue(String... values) { + filter.addAll(List.of(values)); + return this; } - public void writeAttributes(IFeature feature) { + private void writeAttributes(IFeature feature) { StringBuilder stringBuilderInternal = new StringBuilder(); - feature.getAttributes().ifPresent(iAttributeObjectMap -> { - size = iAttributeObjectMap.size(); - iAttributeObjectMap.forEach((iAttribute, object) -> writeAttributes(iAttribute, object, stringBuilderInternal)); - }); String featureName = feature.getName().orElse(""); - if (size == 0) { - this.stringBuilder.append("[").append(replace(NAME, featureName)); - } else { - stringBuilder.append(replace(SHORTSTACK, replace(NAME, featureName))); - stringBuilder.append(stringBuilderInternal); - } - } - private void writeAttributes(IAttribute attribute, Object object, StringBuilder stringBuilder) { - if (filter.contains(attribute.getName().toUpperCase())) { - size-=1; + Map, Object> iAttributeObjectMap = feature.getAttributes().orElse(null); + // check: if the attribute map is empty or null + if (iAttributeObjectMap == null || iAttributeObjectMap.isEmpty()) { + stringBuilder.append("[").append(featureName); // add the name without multicolumn return; } - String format = attribute.getName() + " (" + object.getClass().getSimpleName() + ") = " + object; - runs++; - if (isEnd()) { - stringBuilder.append(replace(SCRIPTSIZE_END, format)); - stringBuilder.append(END); - } else { - stringBuilder.append(replace(SCRIPTSIZE, format)); - } + stringBuilder.append("[").append(replace(MULTICOLUMN, featureName)); + + iAttributeObjectMap.forEach((iAttribute, object) -> { + writeAttributes(iAttribute, object, stringBuilderInternal); + }); + + stringBuilder.append(stringBuilderInternal); + stringBuilder.append(",align=ll"); } - private boolean isEnd() { - return runs == size; + private void writeAttributes(IAttribute attribute, Object object, StringBuilder stringBuilder) { + // filter with type the current boolean value for the running process + if (filterWithType(attribute.getName().toUpperCase())) { + return; // ignore attribute + } + stringBuilder.append(replace(VALUE, attribute.getName(), attribute.getType().getSimpleName(), object)); + replace(VALUE, 12, 2); } - private String replace(String key, String value) { - return key.replace("{replace}", value); + private String replace(String key, Object... values) { + String result = key; + for (short i = 0; i < values.length; i++) { + // Synatx: Hello my name is {0} and I am from {1} -> Hello my name is FeatJar, and I am from Germany + result = result.replace("{" + i + "}", values[i].toString()); + } + return result; } private void makeListUpperCase() { List upperFilter = new ArrayList<>(); filter.forEach(value -> { + // allows filtering with contains and no streams. upperFilter.add(value.toUpperCase()); }); filter = upperFilter; } + private boolean filterWithType(String key) { + if (filter.isEmpty()) { + return false; + } + if (filterType == FilterType.DISPLAY) { + return !filter.contains(key); + } + if (filterType == FilterType.WITH_OUT) { + return filter.contains(key); + } + + return false; + } + + /** + * Paste everything together in the string builder. + */ + public void build() { + makeListUpperCase(); + writeAttributes(feature); + } + + public enum FilterType { + DISPLAY, + WITH_OUT; + } + } diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java index 6a818cd2..48c5b99b 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java @@ -4,8 +4,6 @@ import de.featjar.base.tree.visitor.ITreeVisitor; import de.featjar.feature.model.IFeature; import de.featjar.feature.model.IFeatureTree; - -import java.util.Arrays; import java.util.List; /** @@ -29,8 +27,12 @@ public TraversalAction firstVisit(List path) { //new AttributeHelper(stringBuilder/*, Arrays.asList("abstract", "name")*/) // .writeAttributes(feature); - stringBuilder.append("["); - insertAttributes(feature); + //stringBuilder.append("["); + //insertAttributes(feature); + new AttributeHelper(feature, stringBuilder) + .addFilterValue("name") + .setFilterType(AttributeHelper.FilterType.DISPLAY) + .build(); insertNodeHead(feature); return TraversalAction.CONTINUE; } @@ -46,25 +48,6 @@ public Result getResult() { return Result.of(stringBuilder.toString()); } - private void insertAttributes(IFeature feature) { - if(feature.getAttributes().isPresent() && !feature.getAttributes().get().isEmpty()) { - stringBuilder.append(String.format("\\multicolumn{2}{c}{%s} \\\\\\hline", - feature.getName().orElse(""))); - - feature.getAttributes().get().forEach((attribute, value) -> { - if(!attribute.getName().equals("name")) { - stringBuilder.append(String.format("\\small\\texttt{%s (%s)} &\\small\\texttt{= %s} \\\\", - attribute.getName(), attribute.getType().getSimpleName(), - value.toString())); - } - }); - - stringBuilder.append(",align=ll"); - } else { - stringBuilder.append(feature.getName().orElse("")); - } - } - private void insertNodeHead(IFeature feature) { if (feature.isAbstract()) { stringBuilder.append(",abstract"); @@ -89,19 +72,30 @@ private void insertNodeHead(IFeature feature) { } if (isNotRootFeature(feature) && feature.getFeatureTree().isPresent()) { - if (feature.getFeatureTree().get().getParentGroup().isPresent() && - feature.getFeatureTree().get().getParentGroup().get().isOr()) { + IFeatureTree featureTree = feature.getFeatureTree().get(); + if (featureTree.getParentGroup().isPresent() && + featureTree.getParentGroup().get().isOr() && hasMoreChildrens(feature)) { stringBuilder.append(",or"); } } if (isNotRootFeature(feature) && feature.getFeatureTree().isPresent()) { - if (feature.getFeatureTree().get().getParentGroup().isPresent() && - feature.getFeatureTree().get().getParentGroup().get().isAlternative()) { + IFeatureTree featureTree = feature.getFeatureTree().get(); + if (featureTree.getParentGroup().isPresent() && + featureTree.getParentGroup().get().isAlternative() && hasMoreChildrens(feature)) { stringBuilder.append(",alternative"); } } } + private boolean hasMoreChildrens(IFeature feature) { + IFeatureTree featureTree = feature.getFeatureTree().orElse(null); + if (featureTree == null) { + return false; + } + + return featureTree.getChildren().size() >= 2; + } + private boolean isNotRootFeature(IFeature feature) { return !feature.getFeatureModel().getRootFeatures().contains(feature); } From 7d90e28f62cf45a21a6e0fc90aea8463e6734f68 Mon Sep 17 00:00:00 2001 From: Jonas Hanke Date: Wed, 15 Oct 2025 18:41:12 +0200 Subject: [PATCH 54/67] feat: simple formula visitor can use numeric features --- .../ComputeSimpleFormulaVisitor.java | 85 ++++++++++++------- 1 file changed, 54 insertions(+), 31 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java b/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java index b59ba301..9d850f37 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java @@ -20,6 +20,7 @@ */ package de.featjar.feature.model.transformer; +import de.featjar.base.FeatJAR; import de.featjar.base.data.IAttribute; import de.featjar.base.data.Range; import de.featjar.base.data.Result; @@ -36,6 +37,8 @@ import de.featjar.formula.structure.connective.Implies; import de.featjar.formula.structure.connective.Or; import de.featjar.formula.structure.predicate.Literal; +import de.featjar.formula.structure.predicate.NotEquals; +import de.featjar.formula.structure.term.value.Constant; import de.featjar.formula.structure.term.value.Variable; import java.util.ArrayList; import java.util.HashSet; @@ -50,10 +53,10 @@ */ public class ComputeSimpleFormulaVisitor implements ITreeVisitor { - protected ArrayList constraints = new ArrayList<>(); - protected HashSet variables = new HashSet<>(); + protected ArrayList constraints; + protected HashSet variables; private Map, Object>> attributes; - private Boolean hasCardinalityFeature = Boolean.FALSE; + private Boolean hasCardinalityFeature; public Boolean getHasCardinalityFeature() { return hasCardinalityFeature; @@ -63,10 +66,10 @@ public ComputeSimpleFormulaVisitor( ArrayList constraints, HashSet variables, Map, Object>> attributes) { - this.constraints = constraints; this.variables = variables; this.attributes = attributes; + this.hasCardinalityFeature = true; } @Override @@ -76,6 +79,7 @@ public TraversalAction firstVisit(List path) { // TODO use better error value IFeature feature = node.getFeature(); String featureName = feature.getName().orElse(""); + // TODO: do not add variable if its a cardinality var. Add duplicates instead Variable variable = new Variable(featureName, feature.getType()); variables.add(variable); @@ -87,33 +91,53 @@ public TraversalAction firstVisit(List path) { // TODO take featureRanges into Account Result potentialParentTree = node.getParent(); - Literal featureLiteral = Expressions.literal(featureName); + //Literal featureLiteral = Expressions.literal(featureName); + + IFormula featureFormula = createFeatureFormel(node.getFeature()); + if (potentialParentTree.isEmpty()) { - handleRoot(featureLiteral, node); + handleRoot(featureFormula, node); } else if (node.getFeatureCardinalityUpperBound() > 1) { - handleCardinalityFeature(featureLiteral, node); + handleCardinalityFeature(featureFormula, node); } else { - handleParent(featureLiteral, node); + handleParent(featureFormula, node); } - handleGroups(featureLiteral, node); + handleGroups(featureFormula, node); return ITreeVisitor.super.firstVisit(path); } - private void handleParent(Literal featureLiteral, IFeatureTree node) { + private IFormula createFeatureFormel(IFeature feature) { + return createFeatureFormel(feature, feature.getName().orElse("")); + } + + private IFormula createFeatureFormel(IFeature feature, String featureName) { + if (feature.getType().equals(Boolean.class)) { + return Expressions.literal(featureName); + } else if (feature.getType().equals(Integer.class)) { + return new NotEquals(new Variable(featureName, feature.getType()), new Constant(0)); + } else if(feature.getType().equals(Float.class) || feature.getType().equals(Double.class)) { + return new NotEquals(new Variable(featureName, Double.class), new Constant(0.0)); + } else { + FeatJAR.log().warning("Could not handle type "+ feature.getType()); + return null; + } + } + + private void handleParent(IFormula featureLiteral, IFeatureTree node) { // cardinal features must not be a parent - Literal parentLiteral = getNextNonCardinalityParent(node); - constraints.add(new Implies(featureLiteral, parentLiteral)); + IFormula parentFormula = getNextNonCardinalityParent(node); + constraints.add(new Implies(featureLiteral, parentFormula)); } - private void handleRoot(Literal featureLiteral, IFeatureTree node) { + private void handleRoot(IFormula featureLiteral, IFeatureTree node) { if (node.isMandatory()) { constraints.add(featureLiteral); } } - private void handleCardinalityFeature(Literal featureLiteral, IFeatureTree node) { + private void handleCardinalityFeature(IFormula featureFormula, IFeatureTree node) { hasCardinalityFeature = Boolean.TRUE; int lowerBound = node.getFeatureCardinalityLowerBound(); @@ -123,28 +147,27 @@ private void handleCardinalityFeature(Literal featureLiteral, IFeatureTree node) // add literals and implication to parent String literalName = ""; - Literal parentLiteral = getNextNonCardinalityParent(node); + IFormula parentFormula = getNextNonCardinalityParent(node); for (int i = 1; i <= upperBound; i++) { - literalName = node.getFeature().getName().get() + "_" + i; - featureLiteral = new Literal(literalName); - handleParent(featureLiteral, node); + featureFormula = createFeatureFormel(node.getFeature(), literalName); + handleParent(featureFormula, node); if (i > 1) { // add to implication chain IFormula previousLiteral = featureList.get(featureList.size() - 1); - constraints.add(new Implies(featureLiteral, previousLiteral)); + constraints.add(new Implies(featureFormula, previousLiteral)); } - featureList.add(featureLiteral); + featureList.add(featureFormula); } // add cardinality constraint // check if 0 and do not add implication - if (lowerBound != 0) constraints.add(new Implies(parentLiteral, new AtLeast(lowerBound, featureList))); + if (lowerBound != 0) constraints.add(new Implies(parentFormula, new AtLeast(lowerBound, featureList))); } - private Literal getNextNonCardinalityParent(IFeatureTree node) { + private IFormula getNextNonCardinalityParent(IFeatureTree node) { // if it is possible that root can be as well a cardinality feature - there must // be an alternative @@ -154,10 +177,10 @@ private Literal getNextNonCardinalityParent(IFeatureTree node) { return getNextNonCardinalityParent(node); } - return Expressions.literal(node.getFeature().getName().orElse("")); + return createFeatureFormel(node.getFeature()); } - private void handleGroups(Literal featureLiteral, IFeatureTree node) { + private void handleGroups(IFormula featureFormula, IFeatureTree node) { List childrenGroups = node.getChildrenGroups(); int groupCount = childrenGroups.size(); ArrayList> groupLiterals = new ArrayList<>(groupCount); @@ -168,7 +191,7 @@ private void handleGroups(Literal featureLiteral, IFeatureTree node) { // if node is cardinality feature, set feature literal to parent with no // cardinality if (node.getFeatureCardinalityUpperBound() > 1) { - featureLiteral = getNextNonCardinalityParent(node); + featureFormula = getNextNonCardinalityParent(node); } List children = node.getChildren(); @@ -177,7 +200,7 @@ private void handleGroups(Literal featureLiteral, IFeatureTree node) { Expressions.literal(childNode.getFeature().getName().orElse("")); if (childNode.isMandatory()) { - constraints.add(new Implies(featureLiteral, childLiteral)); + constraints.add(new Implies(featureFormula, childLiteral)); } int groupID = childNode.getParentGroupID(); @@ -192,22 +215,22 @@ private void handleGroups(Literal featureLiteral, IFeatureTree node) { Group group = childrenGroups.get(i); if (group != null) { if (group.isOr()) { - constraints.add(new Implies(featureLiteral, new Or(groupLiterals.get(i)))); + constraints.add(new Implies(featureFormula, new Or(groupLiterals.get(i)))); } else if (group.isAlternative()) { - constraints.add(new Implies(featureLiteral, new Choose(1, groupLiterals.get(i)))); + constraints.add(new Implies(featureFormula, new Choose(1, groupLiterals.get(i)))); } else { int lowerBound = group.getLowerBound(); int upperBound = group.getUpperBound(); if (lowerBound > 0) { if (upperBound != Range.OPEN) { constraints.add(new Implies( - featureLiteral, new Between(lowerBound, upperBound, groupLiterals.get(i)))); + featureFormula, new Between(lowerBound, upperBound, groupLiterals.get(i)))); } else { - constraints.add(new Implies(featureLiteral, new AtMost(upperBound, groupLiterals.get(i)))); + constraints.add(new Implies(featureFormula, new AtMost(upperBound, groupLiterals.get(i)))); } } else { if (upperBound != Range.OPEN) { - constraints.add(new Implies(featureLiteral, new AtLeast(lowerBound, groupLiterals.get(i)))); + constraints.add(new Implies(featureFormula, new AtLeast(lowerBound, groupLiterals.get(i)))); } } } From 01b282ef161cb841f2bccbb25db71feffa5fc232 Mon Sep 17 00:00:00 2001 From: Lara Date: Thu, 16 Oct 2025 14:57:20 +0200 Subject: [PATCH 55/67] feat: added new style and new test file --- src/main/resources/head.tex | 22 ++++++------ src/main/resources/test/test-output.tex | 48 +++++++++++++++---------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/main/resources/head.tex b/src/main/resources/head.tex index 2471d261..79ebc7aa 100644 --- a/src/main/resources/head.tex +++ b/src/main/resources/head.tex @@ -101,18 +101,18 @@ \path (!c1.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!cl.parent) coordinate (C) pic[draw=drawColor, angle radius=\angleSize]{angle}; } }, - groupcardinality/.style 2 args={ + groupcardinality/.style n args={4}{ tikz+={ - \path (!c1.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!cl.parent) coordinate (C) pic[ - draw=drawColor, - angle radius=\angleSizeCardinalityGroup, - pic text={#1,#2}, - pic text options={ - scale=0.6, - fill=white, - inner sep=0.5pt - } - ]{angle} + \path (!{current, n=#1}.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!{current, n=#2}.parent) coordinate (C) pic[ + draw=drawColor, + angle radius=7em, + pic text={#3,#4}, + pic text options={ + scale=0.6, + fill=white, + inner sep=0.5pt + } + ]{angle} } }, /tikz/placeholder/.style={ diff --git a/src/main/resources/test/test-output.tex b/src/main/resources/test/test-output.tex index 6ff7afd5..505187bd 100644 --- a/src/main/resources/test/test-output.tex +++ b/src/main/resources/test/test-output.tex @@ -102,18 +102,18 @@ \path (!c1.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!cl.parent) coordinate (C) pic[draw=drawColor, angle radius=\angleSize]{angle}; } }, - groupcardinality/.style 2 args={ + groupcardinality/.style n args={4}{ tikz+={ - \path (!c1.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!cl.parent) coordinate (C) pic[ - draw=drawColor, - angle radius=\angleSizeCardinalityGroup, - pic text={#1,#2}, - pic text options={ - scale=0.6, - fill=white, - inner sep=0.5pt - } - ]{angle} + \path (!{current, n=#1}.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!{current, n=#2}.parent) coordinate (C) pic[ + draw=drawColor, + angle radius=7em, + pic text={#3,#4}, + pic text options={ + scale=0.6, + fill=white, + inner sep=0.5pt + } + ]{angle} } }, /tikz/placeholder/.style={ @@ -144,15 +144,21 @@ featureDiagram [\multicolumn{2}{c}{Hello} \\\hline \small\texttt{name (String)} &\small\texttt{= Hello} \\ -,align=ll,abstract,optional[\multicolumn{2}{c}{Feature} \\\hline +,align=ll,abstract[\multicolumn{2}{c}{Feature} \\\hline \small\texttt{name (String)} &\small\texttt{= Feature} \\ -,align=ll,concrete,mandatory,or[\multicolumn{2}{c}{Wonderful} \\\hline -\small\texttt{name (String)} &\small\texttt{= Wonderful} \\ -,align=ll,concrete,optional][\multicolumn{2}{c}{Beautiful} \\\hline -\small\texttt{name (String)} &\small\texttt{= Beautiful} \\ -,align=ll,concrete,optional]][\multicolumn{2}{c}{World} \\\hline -\small\texttt{name (String)} &\small\texttt{= World} \\ -,align=ll,concrete,optional]] +,align=ll,abstract,featurecardinality={0}{2},alternative={1}{2},or={3}{2}[\multicolumn{2}{c}{Wonderful1} \\\hline +\small\texttt{name (String)} &\small\texttt{= Wonderful1} \\ +,align=ll,concrete][\multicolumn{2}{c}{Beautiful1} \\\hline +\small\texttt{name (String)} &\small\texttt{= Beautiful1} \\ +,align=ll,concrete][\multicolumn{2}{c}{Wonderful2} \\\hline +\small\texttt{name (String)} &\small\texttt{= Wonderful2} \\ +,align=ll,concrete][\multicolumn{2}{c}{Beautiful2} \\\hline +\small\texttt{name (String)} &\small\texttt{= Beautiful2} \\ +,align=ll,concrete]][\multicolumn{2}{c}{World1} \\\hline +\small\texttt{name (String)} &\small\texttt{= World1} \\ +,align=ll,concrete,optional][\multicolumn{2}{c}{World2} \\\hline +\small\texttt{name (String)} &\small\texttt{= World2} \\ +,align=ll,concrete,mandatory]] \matrix [anchor=north west] at (current bounding box.north east) { \node [placeholder] {}; \\ }; @@ -167,6 +173,10 @@ \draw[drawColor] (0.1,0) -- +(0.2,-0.4); \fill[drawColor] (0,-0.2) arc (240:300:0.2); \node [label=right:Or Group] {}; \\ + \draw[drawColor] (0.1,0) -- +(-0.2, -0.4); + \draw[drawColor] (0.1,0) -- +(0.2,-0.4); + \draw[drawColor] (0,-0.2) arc (240:300:0.2); + \node [label=right:Alternative Group] {}; \\ }; \matrix [below=1mm of current bounding box] { \node {\( \text{A} \land \text{B} \)}; \\ From 48085914d1f43fe8bcc3146a913280e32cb2fc01 Mon Sep 17 00:00:00 2001 From: Lara Date: Thu, 16 Oct 2025 14:58:22 +0200 Subject: [PATCH 56/67] fix: correct feature model and added cardinality --- .../io/tikz/FeatureModelDisplayTikzTest.java | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java index 82ccbe6d..7e38ea94 100644 --- a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java +++ b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java @@ -2,6 +2,7 @@ import de.featjar.base.FeatJAR; import de.featjar.base.data.Attribute; +import de.featjar.base.data.Range; import de.featjar.base.data.identifier.Identifiers; import de.featjar.feature.model.*; import de.featjar.formula.structure.Expressions; @@ -10,7 +11,6 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; - import java.io.*; import java.nio.file.Files; import java.nio.file.Path; @@ -35,34 +35,48 @@ public static void init() { FeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); IFeature featureRootS = featureModel.mutate().addFeature("Hello"); + IFeature feature = featureModel.mutate().addFeature("Feature"); + IFeature world1 = featureModel.mutate().addFeature("World1"); + IFeature world2 = featureModel.mutate().addFeature("World2"); + IFeature wonderful1 = featureModel.mutate().addFeature("Wonderful1"); + IFeature beautiful1 = featureModel.mutate().addFeature("Beautiful1"); + IFeature wonderful2 = featureModel.mutate().addFeature("Wonderful2"); + IFeature beautiful2 = featureModel.mutate().addFeature("Beautiful2"); + featureRootS.mutate().setAbstract(); - // First Tree + + // first tree IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureRootS); - rootTree.mutate().toOrGroup(); + rootTree.mutate().toAndGroup(); - IFeature feature = featureModel.mutate().addFeature("Feature"); IFeatureTree firstFeatureTree = rootTree.mutate().addFeatureBelow(feature); - firstFeatureTree.mutate().makeMandatory(); - firstFeatureTree.mutate().toAlternativeGroup(); - - IFeature world = featureModel.mutate().addFeature("World"); - rootTree.mutate().addFeatureBelow(world); + feature.mutate().setAbstract(); + int group1 = firstFeatureTree.mutate().addAlternativeGroup(); + int group2 = firstFeatureTree.mutate().addOrGroup(); Attribute attribute = new Attribute<>("any", "test", String.class); Attribute attribute2 = new Attribute<>("anyd", "testd", String.class); + wonderful1.mutate().setAttributeValue(attribute, "value"); + wonderful1.mutate().setAttributeValue(attribute2, "value2"); - IFeature wonderful = featureModel.addFeature("Wonderful"); - firstFeatureTree.mutate().addFeatureBelow(wonderful); - wonderful.mutate().setAttributeValue(attribute, "value"); - wonderful.mutate().setAttributeValue(attribute2, "value2"); + firstFeatureTree.mutate().addFeatureBelow(wonderful1, 0, group1); - IFeature beautiful = featureModel.addFeature("Beautiful"); - firstFeatureTree.mutate().addFeatureBelow(beautiful); + firstFeatureTree.mutate().setFeatureCardinality(Range.of(0,2)); + firstFeatureTree.mutate().addFeatureBelow(beautiful1, 1, group1); + + firstFeatureTree.mutate().addFeatureBelow(wonderful2, 2, group2); + firstFeatureTree.mutate().addFeatureBelow(beautiful2, 3, group2); + + rootTree.mutate().addFeatureBelow(world1); + + IFeatureTree world2FeatureTree = rootTree.mutate().addFeatureBelow(world2); + world2FeatureTree.mutate().setFeatureCardinality(Range.of(1, 1)); + + // Constraints featureModel.addConstraint(new And(Expressions.literal("A"), Expressions.literal("B"))); featureModel.addConstraint(new Implies(Expressions.literal("C"), new ForAll(new Variable("A"), new BiImplies(Expressions.literal("A"), Expressions.literal("C"))))); - FeatureModelDisplayTikzTest.featureModel = featureModel; } From 2e0f345c056583325515aed73ee46f2f27c102ae Mon Sep 17 00:00:00 2001 From: Lara Date: Thu, 16 Oct 2025 14:59:19 +0200 Subject: [PATCH 57/67] fix: removed value --- .../de/featjar/feature/model/io/tikz/format/TikzMainFormat.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java index d7f5c7ee..4d6af136 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java @@ -90,7 +90,7 @@ private void printLegend() { .writeDraw("(0.1,0) -- +(-0.2, -0.4)") .writeDraw("(0.1,0) -- +(0.2,-0.4)") .writeDraw("(0,-0.2) arc (240:300:0.2)") - .writeNode("[alternative,label=right:Alternative Group] {}"); + .writeNode("[label=right:Alternative Group] {}"); } stringBuilder.append(matrixHelper.build()); From 3afcdb843bb3fb7c55f557fe5e8e3d6d798a4716 Mon Sep 17 00:00:00 2001 From: Lara Date: Thu, 16 Oct 2025 14:59:48 +0200 Subject: [PATCH 58/67] feat: added cardinality and fixed nodes --- .../model/io/tikz/helper/PrintVisitor.java | 79 +++++++++++-------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java index 48c5b99b..5ecc69a2 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java @@ -1,10 +1,13 @@ package de.featjar.feature.model.io.tikz.helper; +import de.featjar.base.FeatJAR; import de.featjar.base.data.Result; import de.featjar.base.tree.visitor.ITreeVisitor; +import de.featjar.feature.model.FeatureTree; import de.featjar.feature.model.IFeature; import de.featjar.feature.model.IFeatureTree; import java.util.List; +import java.util.Optional; /** * This class travers a given {@link IFeatureTree} and generates the Tikz representation of the tree. @@ -25,18 +28,27 @@ public PrintVisitor() { public TraversalAction firstVisit(List path) { IFeature feature = ITreeVisitor.getCurrentNode(path).getFeature(); - //new AttributeHelper(stringBuilder/*, Arrays.asList("abstract", "name")*/) - // .writeAttributes(feature); - //stringBuilder.append("["); - //insertAttributes(feature); new AttributeHelper(feature, stringBuilder) .addFilterValue("name") .setFilterType(AttributeHelper.FilterType.DISPLAY) .build(); insertNodeHead(feature); + handleGroups(feature); return TraversalAction.CONTINUE; } + private void handleGroups(IFeature feature) { + IFeatureTree featureTree = feature.getFeatureTree().orElse(null); + if (featureTree == null) { + return; + } + + featureTree.getChildren().forEach(featureStream -> { + FeatJAR.log().info(feature.getName().get() + " - " + featureStream.getFeature().getName().get()); + }); + + } + @Override public TraversalAction lastVisit(List path) { stringBuilder.append("]"); @@ -49,6 +61,9 @@ public Result getResult() { } private void insertNodeHead(IFeature feature) { + IFeatureTree featureTree = feature.getFeatureTree().orElse(null); + FeatureTree.Group featureTreeParentGroup = feature.getFeatureTree().get().getParentGroup().orElse(null); + if (feature.isAbstract()) { stringBuilder.append(",abstract"); } @@ -57,45 +72,43 @@ private void insertNodeHead(IFeature feature) { stringBuilder.append(",concrete"); } - if(feature.getFeatureTree().isPresent()) { - if (feature.getFeatureTree().get().getFeatureCardinalityUpperBound() > 1) { + if (isNotRootFeature(feature) && featureTreeParentGroup != null && featureTreeParentGroup.isAnd()) { + if (featureTree.getFeatureCardinalityLowerBound() == 0 && + featureTree.getFeatureCardinalityUpperBound() == 1) { + stringBuilder.append(",optional"); + } else if(featureTree.getFeatureCardinalityLowerBound() == 1 && + featureTree.getFeatureCardinalityUpperBound() == 1) { + stringBuilder.append(",mandatory"); + } else { stringBuilder.append(String.format(",featurecardinality={%d}{%d}", feature.getFeatureTree().get().getFeatureCardinalityLowerBound(), feature.getFeatureTree().get().getFeatureCardinalityUpperBound())); } - - if (feature.getFeatureTree().get().isMandatory()) { - stringBuilder.append(",mandatory"); - } else { - stringBuilder.append(",optional"); - } } - if (isNotRootFeature(feature) && feature.getFeatureTree().isPresent()) { - IFeatureTree featureTree = feature.getFeatureTree().get(); - if (featureTree.getParentGroup().isPresent() && - featureTree.getParentGroup().get().isOr() && hasMoreChildrens(feature)) { - stringBuilder.append(",or"); - } - } - if (isNotRootFeature(feature) && feature.getFeatureTree().isPresent()) { - IFeatureTree featureTree = feature.getFeatureTree().get(); - if (featureTree.getParentGroup().isPresent() && - featureTree.getParentGroup().get().isAlternative() && hasMoreChildrens(feature)) { - stringBuilder.append(",alternative"); + if (isNotRootFeature(feature)) { + int previousChildrenCount = 0; + for(int i = 0; i < featureTree.getChildrenGroups().size(); i++) { + if(featureTree.getChildrenGroup(i).isPresent()) { + FeatureTree.Group group = featureTree.getChildrenGroup(i).get(); + + if(group.isOr()) { + stringBuilder.append(String.format(",or={%d}{%d}", previousChildrenCount + 1, + featureTree.getChildren(i).size())); + } else if(group.isAlternative()) { + stringBuilder.append(String.format(",alternative={%d}{%d}", previousChildrenCount + 1, + featureTree.getChildren(i).size())); + } else if(group.isCardinalityGroup()) { + stringBuilder.append(String.format(",groupcardinality={%d}{%d}", previousChildrenCount + 1, + featureTree.getChildren(i).size())); + } + + previousChildrenCount += featureTree.getChildren(i).size(); + } } } } - private boolean hasMoreChildrens(IFeature feature) { - IFeatureTree featureTree = feature.getFeatureTree().orElse(null); - if (featureTree == null) { - return false; - } - - return featureTree.getChildren().size() >= 2; - } - private boolean isNotRootFeature(IFeature feature) { return !feature.getFeatureModel().getRootFeatures().contains(feature); } From 8c7ae295b650004fe970a5408841e6d536a2f032 Mon Sep 17 00:00:00 2001 From: Lara Date: Thu, 16 Oct 2025 16:04:56 +0200 Subject: [PATCH 59/67] change: renamed classes for a better split --- ...eatureColor.java => TikzFeatureColor.java} | 12 ++++---- .../model/io/tikz/format/TikzMainFormat.java | 24 ++++++++-------- .../model/io/tikz/helper/PrintVisitor.java | 5 ++-- ...teHelper.java => TikzAttributeHelper.java} | 10 +++---- ...atrixHelper.java => TikzMatrixHelper.java} | 14 +++++----- .../{MatrixType.java => TikzMatrixType.java} | 4 +-- .../model/io/tikz/AttributeFilterTest.java | 28 +++++++++---------- 7 files changed, 47 insertions(+), 50 deletions(-) rename src/main/java/de/featjar/feature/model/io/tikz/color/{FeatureColor.java => TikzFeatureColor.java} (69%) rename src/main/java/de/featjar/feature/model/io/tikz/helper/{AttributeHelper.java => TikzAttributeHelper.java} (92%) rename src/main/java/de/featjar/feature/model/io/tikz/helper/{MatrixHelper.java => TikzMatrixHelper.java} (80%) rename src/main/java/de/featjar/feature/model/io/tikz/helper/{MatrixType.java => TikzMatrixType.java} (91%) diff --git a/src/main/java/de/featjar/feature/model/io/tikz/color/FeatureColor.java b/src/main/java/de/featjar/feature/model/io/tikz/color/TikzFeatureColor.java similarity index 69% rename from src/main/java/de/featjar/feature/model/io/tikz/color/FeatureColor.java rename to src/main/java/de/featjar/feature/model/io/tikz/color/TikzFeatureColor.java index 27b05b7d..5f15eec8 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/color/FeatureColor.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/color/TikzFeatureColor.java @@ -7,7 +7,7 @@ * @author Lara Merza * @author Jonas Hanke */ -public enum FeatureColor { +public enum TikzFeatureColor { RED("redColor"), ORANGE("orangeColor"), @@ -21,10 +21,10 @@ public enum FeatureColor { PINK("pinkColor"), NO_COLOR(""); - public static String color(FeatureColor featureColor) { - for (FeatureColor featureColors : values()) { - if (featureColor.equals(featureColors)) { - return featureColors.getColor(); + public static String color(TikzFeatureColor tikzFeatureColor) { + for (TikzFeatureColor tikzFeatureColors : values()) { + if (tikzFeatureColor.equals(tikzFeatureColors)) { + return tikzFeatureColors.getColor(); } } return NO_COLOR.getColor(); @@ -32,7 +32,7 @@ public static String color(FeatureColor featureColor) { final String color; - FeatureColor(String color) { + TikzFeatureColor(String color) { this.color = color; } diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java index 4d6af136..abd48353 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java @@ -5,8 +5,8 @@ import de.featjar.feature.model.IFeatureModel; import de.featjar.feature.model.IFeatureTree; import de.featjar.feature.model.io.tikz.TikzGraphicalFeatureModelFormat; -import de.featjar.feature.model.io.tikz.helper.MatrixHelper; -import de.featjar.feature.model.io.tikz.helper.MatrixType; +import de.featjar.feature.model.io.tikz.helper.TikzMatrixHelper; +import de.featjar.feature.model.io.tikz.helper.TikzMatrixType; import de.featjar.feature.model.io.tikz.helper.PrintVisitor; import de.featjar.formula.io.textual.ExpressionSerializer; import de.featjar.formula.io.textual.LaTexSymbols; @@ -57,27 +57,27 @@ private void postProcessing() { } private void printLegend() { - MatrixHelper matrixHelper = new MatrixHelper(MatrixType.LEGEND); + TikzMatrixHelper tikzMatrixHelper = new TikzMatrixHelper(TikzMatrixType.LEGEND); if (stringBuilder.indexOf(",abstract") != -1 && stringBuilder.indexOf(",concrete") != -1) { - matrixHelper.writeNode("[abstract,label=right:Abstract Feature] {}"); - matrixHelper.writeNode("[concrete,label=right:Concrete Feature] {}"); + tikzMatrixHelper.writeNode("[abstract,label=right:Abstract Feature] {}"); + tikzMatrixHelper.writeNode("[concrete,label=right:Concrete Feature] {}"); } else if (stringBuilder.indexOf(",abstract") != -1) { - matrixHelper.writeNode("[abstract,label=right:Feature] {}"); + tikzMatrixHelper.writeNode("[abstract,label=right:Feature] {}"); } else if (stringBuilder.indexOf(",concrete") != -1) { - matrixHelper.writeNode("[concrete,label=right:Feature] {}"); + tikzMatrixHelper.writeNode("[concrete,label=right:Feature] {}"); } if (stringBuilder.indexOf(",mandatory") != -1) { - matrixHelper.writeNode("[mandatory,label=right:Mandatory] {}"); + tikzMatrixHelper.writeNode("[mandatory,label=right:Mandatory] {}"); } if (stringBuilder.indexOf(",optional") != -1) { - matrixHelper.writeNode("[optional,label=right:Optional] {}"); + tikzMatrixHelper.writeNode("[optional,label=right:Optional] {}"); } if (stringBuilder.indexOf(",or") != -1) { - matrixHelper + tikzMatrixHelper .writeFillDraw("(0.1,0) - +(-0,-0.2) - +(0.2,-0.2)- +(0.1,0)") .writeDraw("(0.1,0) -- +(-0.2, -0.4)") .writeDraw("(0.1,0) -- +(0.2,-0.4)") @@ -86,14 +86,14 @@ private void printLegend() { } if (stringBuilder.indexOf(",alternative") != -1) { - matrixHelper + tikzMatrixHelper .writeDraw("(0.1,0) -- +(-0.2, -0.4)") .writeDraw("(0.1,0) -- +(0.2,-0.4)") .writeDraw("(0,-0.2) arc (240:300:0.2)") .writeNode("[label=right:Alternative Group] {}"); } - stringBuilder.append(matrixHelper.build()); + stringBuilder.append(tikzMatrixHelper.build()); } private void printConstraints() { diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java index 5ecc69a2..315bf298 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java @@ -7,7 +7,6 @@ import de.featjar.feature.model.IFeature; import de.featjar.feature.model.IFeatureTree; import java.util.List; -import java.util.Optional; /** * This class travers a given {@link IFeatureTree} and generates the Tikz representation of the tree. @@ -28,9 +27,9 @@ public PrintVisitor() { public TraversalAction firstVisit(List path) { IFeature feature = ITreeVisitor.getCurrentNode(path).getFeature(); - new AttributeHelper(feature, stringBuilder) + new TikzAttributeHelper(feature, stringBuilder) .addFilterValue("name") - .setFilterType(AttributeHelper.FilterType.DISPLAY) + .setFilterType(TikzAttributeHelper.FilterType.DISPLAY) .build(); insertNodeHead(feature); handleGroups(feature); diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/AttributeHelper.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/TikzAttributeHelper.java similarity index 92% rename from src/main/java/de/featjar/feature/model/io/tikz/helper/AttributeHelper.java rename to src/main/java/de/featjar/feature/model/io/tikz/helper/TikzAttributeHelper.java index 68e76c10..8d1d3ac6 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/helper/AttributeHelper.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/TikzAttributeHelper.java @@ -14,7 +14,7 @@ * @author Lara Merza * @author Jonas Hanke */ -public class AttributeHelper { +public class TikzAttributeHelper { private final String MULTICOLUMN = "\\multicolumn{2}{c}{{0}} \\\\\\hline" + System.lineSeparator(); private final String VALUE = "\\small\\texttt{{0} ({1})} &\\small\\texttt{= {2}} \\\\" + System.lineSeparator(); @@ -25,13 +25,13 @@ public class AttributeHelper { private List filter; private FilterType filterType = FilterType.WITH_OUT; // FALLBACK - public AttributeHelper(IFeature feature, StringBuilder stringBuilder) { + public TikzAttributeHelper(IFeature feature, StringBuilder stringBuilder) { this.feature = feature; this.stringBuilder = stringBuilder; this.filter = new ArrayList<>(); } - public AttributeHelper(IFeature feature, StringBuilder stringBuilder, List filter) { + public TikzAttributeHelper(IFeature feature, StringBuilder stringBuilder, List filter) { this.feature = feature; this.stringBuilder = stringBuilder; this.filter = filter; @@ -44,7 +44,7 @@ public AttributeHelper(IFeature feature, StringBuilder stringBuilder, List Date: Thu, 16 Oct 2025 16:45:33 +0200 Subject: [PATCH 60/67] refactor: numeric features (integer, float) can be used --- .../de/featjar/feature/model/Features.java | 28 ++++ .../model/transformer/ComputeFormula.java | 125 +++++++----------- .../ComputeSimpleFormulaVisitor.java | 39 ++---- .../ReplaceAttributeAggregate.java | 27 ++-- .../model/transformer/ComputeFormulaTest.java | 69 +++++++++- .../ReplaceAttributeAggregateTest.java | 100 +++++++++----- 6 files changed, 234 insertions(+), 154 deletions(-) create mode 100644 src/main/java/de/featjar/feature/model/Features.java diff --git a/src/main/java/de/featjar/feature/model/Features.java b/src/main/java/de/featjar/feature/model/Features.java new file mode 100644 index 00000000..72b79938 --- /dev/null +++ b/src/main/java/de/featjar/feature/model/Features.java @@ -0,0 +1,28 @@ +package de.featjar.feature.model; + +import de.featjar.base.FeatJAR; +import de.featjar.formula.structure.Expressions; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.predicate.NotEquals; +import de.featjar.formula.structure.term.value.Constant; +import de.featjar.formula.structure.term.value.Variable; + +public class Features { + + public static IFormula createFeatureFormel(IFeature feature) { + return createFeatureFormel(feature, feature.getName().orElse("")); + } + + public static IFormula createFeatureFormel(IFeature feature, String featureName) { + if (feature.getType().equals(Boolean.class)) { + return Expressions.literal(featureName); + } else if (feature.getType().equals(Integer.class)) { + return new NotEquals(new Variable(featureName, feature.getType()), new Constant(0)); + } else if(feature.getType().equals(Float.class)) { + return new NotEquals(new Variable(featureName, feature.getType()), new Constant(0.0f)); + } else { + FeatJAR.log().warning("Could not handle type "+ feature.getType()); + return null; + } + } +} diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java index 36227117..145317d9 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java @@ -20,7 +20,6 @@ */ package de.featjar.feature.model.transformer; -import de.featjar.base.FeatJAR; import de.featjar.base.computation.AComputation; import de.featjar.base.computation.Computations; import de.featjar.base.computation.Dependency; @@ -32,6 +31,7 @@ import de.featjar.base.data.Result; import de.featjar.base.tree.Trees; import de.featjar.feature.model.FeatureTree.Group; +import de.featjar.feature.model.Features; import de.featjar.feature.model.IFeatureModel; import de.featjar.feature.model.IFeatureTree; import de.featjar.formula.structure.IFormula; @@ -43,9 +43,6 @@ import de.featjar.formula.structure.connective.Implies; import de.featjar.formula.structure.connective.Or; import de.featjar.formula.structure.connective.Reference; -import de.featjar.formula.structure.predicate.Literal; -import de.featjar.formula.structure.predicate.NotEquals; -import de.featjar.formula.structure.term.value.Constant; import de.featjar.formula.structure.term.value.Variable; import java.util.*; @@ -74,30 +71,8 @@ public Result compute(List dependencyList, Progress progress) IFeatureModel featureModel = FEATURE_MODEL.get(dependencyList); ArrayList constraints = new ArrayList<>(); HashSet variables = new HashSet<>(); - Map, Object>> attributes = new LinkedHashMap<>(); - -/* IFormula featureLiteral; - if (feature.getType().equals(Boolean.class)) { - featureLiteral = Expressions.literal(featureName); - } else if (feature.getType().equals(Integer.class)) { - featureLiteral = new NotEquals(variable, new Constant(0)); - } else if(feature.getType().equals(Float.class)) { - featureLiteral = new NotEquals(variable, new Constant((float) 0.0)); - } else { - FeatJAR.log().warning("Could not handle type "+ feature.getType()); - return; - } + Map, Object>> attributes = new LinkedHashMap<>(); - // TODO take featureRanges into Account - Result potentialParentTree = node.getParent(); - - if (potentialParentTree.isEmpty()) { - handleRoot(constraints, featureLiteral, node); - } else { - handleParent(constraints, featureLiteral, node); - } - handleGroups(constraints, featureLiteral, node); -*/ if (SIMPLE_TRANSLATION.get(dependencyList)) { IFeatureTree iFeatureTree = featureModel.getRoots().get(0); @@ -123,23 +98,11 @@ public Result compute(List dependencyList, Progress progress) return Result.of(reference); } -/* - private void handleParent(ArrayList constraints, IFormula featureLiteral, IFeatureTree node) { - constraints.add(new Implies( - featureLiteral, - Expressions.literal( - node.getParent().get().getFeature().getName().orElse("")))); - } - - private void handleRoot(ArrayList constraints, IFormula featureLiteral, IFeatureTree node) { - if (node.isMandatory()) { - constraints.add(featureLiteral); -*/ private void traverseFeatureModel( IFeatureModel featureModel, ArrayList constraints, HashSet variables, - Map, Object>> attributes) { + Map, Object>> attributes) { for (IFeatureTree root : featureModel.getRoots()) { @@ -148,14 +111,14 @@ private void traverseFeatureModel( root.getFeature().getName().get(), root.getFeature().getType()); variables.add(variable); if (root.getFeature().getAttributes().isPresent()) { - attributes.put(variable, root.getFeature().getAttributes().get()); + attributes.put(Features.createFeatureFormel(root.getFeature()), root.getFeature().getAttributes().get()); } - Literal rootLiteral = new Literal(root.getFeature().getName().orElse("")); + IFormula rootFormula = Features.createFeatureFormel(root.getFeature()); if (root.isMandatory()) { - constraints.add(rootLiteral); + constraints.add(rootFormula); } - handleGroups(rootLiteral, root, constraints); + handleGroups(rootFormula, root, constraints); addChildConstraints(root, constraints, variables, attributes); } @@ -165,7 +128,7 @@ private void addChildConstraints( IFeatureTree node, ArrayList constraints, HashSet variables, - Map, Object>> attributes) { + Map, Object>> attributes) { // collect the attributes of all features // TODO: check if the variables need to be duplicated? @@ -173,10 +136,10 @@ private void addChildConstraints( node.getFeature().getName().get(), node.getFeature().getType()); variables.add(variable); if (node.getFeature().getAttributes().isPresent()) { - attributes.put(variable, node.getFeature().getAttributes().get()); + attributes.put(Features.createFeatureFormel(node.getFeature()), node.getFeature().getAttributes().get()); } - Literal parentLiteral = new Literal(getLiteralName(node)); + IFormula parentFormula = Features.createFeatureFormel(node.getFeature(), getFormulaName(node)); for (IFeatureTree child : node.getChildren()) { @@ -186,64 +149,64 @@ private void addChildConstraints( int upperBound = child.getFeatureCardinalityUpperBound(); int lowerBound = child.getFeatureCardinalityLowerBound(); - LinkedList constraintGroupLiterals = new LinkedList(); + LinkedList constraintGroupFormulas = new LinkedList<>(); for (int i = 1; i <= upperBound; i++) { - String literalName = getLiteralName(child) + "_" + i; + String formulaName = getFormulaName(child) + "_" + i; if (cardinalityFeatureAbove(child)) { - literalName += "." + getLiteralName(node); + formulaName += "." + getFormulaName(node); } // clone only tree for traversal, not its children IFeatureTree cardinalityClone = child.cloneTree(); - cardinalityClone.mutate().setAttributeValue(literalNameAttribute, literalName); + cardinalityClone.mutate().setAttributeValue(literalNameAttribute, formulaName); - Literal currentLiteral = new Literal(literalName); + IFormula currentFormula = Features.createFeatureFormel(child.getFeature(), formulaName); // add all the constraints // imply parent - constraints.add(new Implies(currentLiteral, parentLiteral)); + constraints.add(new Implies(currentFormula, parentFormula)); // implication chain part if (i > 1) { - Literal previousLiteral = constraintGroupLiterals.getLast(); - constraints.add(new Implies(currentLiteral, previousLiteral)); + IFormula previousFormula = constraintGroupFormulas.getLast(); + constraints.add(new Implies(currentFormula, previousFormula)); } // group constraints - handleGroups(currentLiteral, cardinalityClone, constraints); + handleGroups(currentFormula, cardinalityClone, constraints); - constraintGroupLiterals.add(currentLiteral); + constraintGroupFormulas.add(currentFormula); addChildConstraints(cardinalityClone, constraints, variables, attributes); } // check if 0 and do not add implication if (lowerBound != 0) - constraints.add(new Implies(parentLiteral, new AtLeast(lowerBound, constraintGroupLiterals))); + constraints.add(new Implies(parentFormula, new AtLeast(lowerBound, constraintGroupFormulas))); return; } else { - String literalName = getLiteralName(child); + String formulaName = getFormulaName(child); if (cardinalityFeatureAbove(child)) { - literalName += "." + getLiteralName(node); + formulaName += "." + getFormulaName(node); } - Literal childFeatureLiteral = new Literal(literalName); - child.mutate().setAttributeValue(literalNameAttribute, literalName); + IFormula childFeatureFormula = Features.createFeatureFormel(child.getFeature(), formulaName); + child.mutate().setAttributeValue(literalNameAttribute, formulaName); // add constraints // always add parent implications (child implies parent) - constraints.add(new Implies(childFeatureLiteral, parentLiteral)); + constraints.add(new Implies(childFeatureFormula, parentFormula)); // handle group - handleGroups(childFeatureLiteral, child, constraints); + handleGroups(childFeatureFormula, child, constraints); addChildConstraints(child, constraints, variables, attributes); } } } - private String getLiteralName(IFeatureTree node) { + private String getFormulaName(IFeatureTree node) { String literalName = ""; if (node.getAttributeValue(literalNameAttribute).isEmpty()) { literalName = node.getFeature().getName().orElse(""); @@ -273,53 +236,55 @@ private boolean isCardinalityFeature(IFeatureTree node) { } // private void handleGroups(ArrayList constraints, IFormula featureLiteral, IFeatureTree node) { - private void handleGroups(Literal featureLiteral, IFeatureTree node, ArrayList constraints) { + private void handleGroups(IFormula featureFormula, IFeatureTree node, ArrayList constraints) { List childrenGroups = node.getChildrenGroups(); int groupCount = childrenGroups.size(); - ArrayList> groupLiterals = new ArrayList<>(groupCount); + ArrayList> groupFormulas = new ArrayList<>(groupCount); + for (int i = 0; i < groupCount; i++) { - groupLiterals.add(null); + groupFormulas.add(null); } + List children = node.getChildren(); for (IFeatureTree childNode : children) { - String childLiteralName = getLiteralName(childNode); + String childFormulaName = getFormulaName(childNode); if (childNode.getAttributeValue(literalNameAttribute).isEmpty() && cardinalityFeatureAbove(childNode)) - childLiteralName += "." + getLiteralName(node); + childFormulaName += "." + getFormulaName(node); - Literal childLiteral = new Literal(childLiteralName); + IFormula childFormula = Features.createFeatureFormel(childNode.getFeature(), childFormulaName); if (childNode.isMandatory()) { - constraints.add(new Implies(featureLiteral, childLiteral)); + constraints.add(new Implies(featureFormula, childFormula)); } int groupID = childNode.getParentGroupID(); - List list = groupLiterals.get(groupID); + List list = groupFormulas.get(groupID); if (list == null) { - groupLiterals.set(groupID, list = new ArrayList<>()); + groupFormulas.set(groupID, list = new ArrayList<>()); } - list.add(childLiteral); + list.add(childFormula); } for (int i = 0; i < groupCount; i++) { Group group = childrenGroups.get(i); if (group != null) { if (group.isOr()) { - constraints.add(new Implies(featureLiteral, new Or(groupLiterals.get(i)))); + constraints.add(new Implies(featureFormula, new Or(groupFormulas.get(i)))); } else if (group.isAlternative()) { - constraints.add(new Implies(featureLiteral, new Choose(1, groupLiterals.get(i)))); + constraints.add(new Implies(featureFormula, new Choose(1, groupFormulas.get(i)))); } else { int lowerBound = group.getLowerBound(); int upperBound = group.getUpperBound(); if (lowerBound > 0) { if (upperBound != Range.OPEN) { constraints.add(new Implies( - featureLiteral, new Between(lowerBound, upperBound, groupLiterals.get(i)))); + featureFormula, new Between(lowerBound, upperBound, groupFormulas.get(i)))); } else { - constraints.add(new Implies(featureLiteral, new AtMost(upperBound, groupLiterals.get(i)))); + constraints.add(new Implies(featureFormula, new AtMost(upperBound, groupFormulas.get(i)))); } } else { if (upperBound != Range.OPEN) { - constraints.add(new Implies(featureLiteral, new AtLeast(lowerBound, groupLiterals.get(i)))); + constraints.add(new Implies(featureFormula, new AtLeast(lowerBound, groupFormulas.get(i)))); } } } diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java b/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java index 9d850f37..d976558c 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java @@ -26,6 +26,7 @@ import de.featjar.base.data.Result; import de.featjar.base.tree.visitor.ITreeVisitor; import de.featjar.feature.model.FeatureTree.Group; +import de.featjar.feature.model.Features; import de.featjar.feature.model.IFeature; import de.featjar.feature.model.IFeatureTree; import de.featjar.formula.structure.Expressions; @@ -55,7 +56,7 @@ public class ComputeSimpleFormulaVisitor implements ITreeVisitor constraints; protected HashSet variables; - private Map, Object>> attributes; + private Map, Object>> attributes; private Boolean hasCardinalityFeature; public Boolean getHasCardinalityFeature() { @@ -65,11 +66,11 @@ public Boolean getHasCardinalityFeature() { public ComputeSimpleFormulaVisitor( ArrayList constraints, HashSet variables, - Map, Object>> attributes) { + Map, Object>> attributes) { this.constraints = constraints; this.variables = variables; this.attributes = attributes; - this.hasCardinalityFeature = true; + this.hasCardinalityFeature = false; } @Override @@ -86,14 +87,14 @@ public TraversalAction firstVisit(List path) { if (node.getFeature().getAttributes().isPresent()) { // name is an attribute as well - attributes.put(variable, node.getFeature().getAttributes().get()); + attributes.put(Features.createFeatureFormel(feature), node.getFeature().getAttributes().get()); } // TODO take featureRanges into Account Result potentialParentTree = node.getParent(); //Literal featureLiteral = Expressions.literal(featureName); - IFormula featureFormula = createFeatureFormel(node.getFeature()); + IFormula featureFormula = Features.createFeatureFormel(node.getFeature()); if (potentialParentTree.isEmpty()) { handleRoot(featureFormula, node); @@ -108,22 +109,7 @@ public TraversalAction firstVisit(List path) { return ITreeVisitor.super.firstVisit(path); } - private IFormula createFeatureFormel(IFeature feature) { - return createFeatureFormel(feature, feature.getName().orElse("")); - } - private IFormula createFeatureFormel(IFeature feature, String featureName) { - if (feature.getType().equals(Boolean.class)) { - return Expressions.literal(featureName); - } else if (feature.getType().equals(Integer.class)) { - return new NotEquals(new Variable(featureName, feature.getType()), new Constant(0)); - } else if(feature.getType().equals(Float.class) || feature.getType().equals(Double.class)) { - return new NotEquals(new Variable(featureName, Double.class), new Constant(0.0)); - } else { - FeatJAR.log().warning("Could not handle type "+ feature.getType()); - return null; - } - } private void handleParent(IFormula featureLiteral, IFeatureTree node) { // cardinal features must not be a parent @@ -150,7 +136,7 @@ private void handleCardinalityFeature(IFormula featureFormula, IFeatureTree node IFormula parentFormula = getNextNonCardinalityParent(node); for (int i = 1; i <= upperBound; i++) { literalName = node.getFeature().getName().get() + "_" + i; - featureFormula = createFeatureFormel(node.getFeature(), literalName); + featureFormula = Features.createFeatureFormel(node.getFeature(), literalName); handleParent(featureFormula, node); if (i > 1) { @@ -177,7 +163,7 @@ private IFormula getNextNonCardinalityParent(IFeatureTree node) { return getNextNonCardinalityParent(node); } - return createFeatureFormel(node.getFeature()); + return Features.createFeatureFormel(node.getFeature()); } private void handleGroups(IFormula featureFormula, IFeatureTree node) { @@ -196,11 +182,10 @@ private void handleGroups(IFormula featureFormula, IFeatureTree node) { List children = node.getChildren(); for (IFeatureTree childNode : children) { - Literal childLiteral = - Expressions.literal(childNode.getFeature().getName().orElse("")); + IFormula childFormula = Features.createFeatureFormel(childNode.getFeature()); if (childNode.isMandatory()) { - constraints.add(new Implies(featureFormula, childLiteral)); + constraints.add(new Implies(featureFormula, childFormula)); } int groupID = childNode.getParentGroupID(); @@ -208,7 +193,7 @@ private void handleGroups(IFormula featureFormula, IFeatureTree node) { if (list == null) { groupLiterals.set(groupID, list = new ArrayList<>()); } - list.add(childLiteral); + list.add(childFormula); } for (int i = 0; i < groupCount; i++) { @@ -237,4 +222,4 @@ private void handleGroups(IFormula featureFormula, IFeatureTree node) { } } } -} +} \ No newline at end of file diff --git a/src/main/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregate.java b/src/main/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregate.java index 8da3ea34..0dfdafe3 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregate.java +++ b/src/main/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregate.java @@ -7,7 +7,6 @@ import de.featjar.formula.structure.IExpression; import de.featjar.formula.structure.IFormula; import de.featjar.formula.structure.term.aggregate.IAttributeAggregate; -import de.featjar.formula.structure.term.value.Variable; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -23,20 +22,20 @@ */ public class ReplaceAttributeAggregate implements ITreeVisitor { - private final Map, Object>> attributes; - private boolean hasCardinalityFeatures; + private final Map, Object>> attributes; + private final boolean hasCardinalityFeatures; public ReplaceAttributeAggregate( - Map, Object>> attributes, Boolean hasCardinalityFeatures) { + Map, Object>> attributes, Boolean hasCardinalityFeatures) { this.attributes = attributes; this.hasCardinalityFeatures = hasCardinalityFeatures; } @Override public TraversalAction lastVisit(List path) { - final IExpression formula = ITreeVisitor.getCurrentNode(path); + final IExpression expression = ITreeVisitor.getCurrentNode(path); - if (formula instanceof IAttributeAggregate) { + if (expression instanceof IAttributeAggregate) { if (hasCardinalityFeatures) { throw new UnsupportedOperationException( @@ -44,26 +43,26 @@ public TraversalAction lastVisit(List path) { } final Result parent = ITreeVisitor.getParentNode(path); - if (parent.isPresent()) { - ArrayList filteredVariables = new ArrayList<>(); + ArrayList filteredFeatures = new ArrayList<>(); ArrayList values = new ArrayList<>(); - String attributeFilter = ((IAttributeAggregate) formula).getAttributeFilter(); + String attributeFilter = ((IAttributeAggregate) expression).getAttributeFilter(); - attributes.forEach((variable, value) -> { + // formula -> feature as a formula, value -> attribute map + attributes.forEach((formula, value) -> { Optional, Object>> attributeMatch = value.entrySet().stream() .filter(predicate -> predicate.getKey().getName().equals(attributeFilter)) .findFirst(); if (attributeMatch.isPresent()) { - filteredVariables.add(variable); + filteredFeatures.add(formula); values.add(attributeMatch.get().getValue()); } }); - Result result = ((IAttributeAggregate) formula).translate(filteredVariables, values); + Result result = ((IAttributeAggregate) expression).translate(filteredFeatures, values); if (result.isPresent()) { - parent.get().replaceChild(formula, result.get()); + parent.get().replaceChild(expression, result.get()); } } } @@ -75,4 +74,4 @@ public TraversalAction lastVisit(List path) { public Result getResult() { return Result.ofVoid(); } -} +} \ No newline at end of file diff --git a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java index 6a5413ce..a9caabe0 100644 --- a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java +++ b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java @@ -40,6 +40,7 @@ import de.featjar.formula.structure.connective.Reference; import de.featjar.formula.structure.predicate.LessThan; import de.featjar.formula.structure.predicate.Literal; +import de.featjar.formula.structure.predicate.NotEquals; import de.featjar.formula.structure.term.aggregate.AttributeSum; import de.featjar.formula.structure.term.function.IfThenElse; import de.featjar.formula.structure.term.function.RealAdd; @@ -59,7 +60,7 @@ public void createFeatureModel() { } @Test - void simpleWithTwoCardinalies() { + void simpleWithTwoCardinalities() { IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); rootTree.mutate().makeMandatory(); @@ -86,6 +87,36 @@ void simpleWithTwoCardinalies() { executeSimpleTest(); } + @Test + void simpleWithTwoCardinalitiesNumericFeatures() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + childFeature1.mutate().setType(Integer.class); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + childFeature2.mutate().setType(Float.class); + IFeatureTree childFeature2Tree = childFeature1Tree.mutate().addFeatureBelow(childFeature2); + childFeature2Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new NotEquals(new Variable("A_1", Integer.class), new Constant(0)), new Literal("root")), + new Implies(new NotEquals(new Variable("A_2", Integer.class), new Constant(0)), new Literal("root")), + new Implies(new NotEquals(new Variable("A_2", Integer.class), new Constant(0)), new NotEquals(new Variable("A_1", Integer.class), new Constant(0))), + new Implies(new NotEquals(new Variable("B_1", Float.class), new Constant(0.0f)), new Literal("root")), + new Implies(new NotEquals(new Variable("B_2", Float.class), new Constant(0.0f)), new Literal("root")), + new Implies(new NotEquals(new Variable("B_2", Float.class), new Constant(0.0f)), new NotEquals(new Variable("B_1", Float.class), new Constant(0.0f))))); + + executeSimpleTest(); + } + @Test void withTwoCardinalies() { IFeatureTree rootTree = @@ -149,6 +180,40 @@ void simpleWithCardinalityAndChildGroup() { executeSimpleTest(); } + @Test + void simpleWithCardinalityAndChildGroupNumericFeatures() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + childFeature1.mutate().setType(Float.class); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + childFeature1Tree.mutate().toAlternativeGroup(); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + childFeature2.mutate().setType(Integer.class); + childFeature1Tree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + childFeature1Tree.mutate().addFeatureBelow(childFeature3); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new NotEquals(new Variable("A_1", Float.class), new Constant(0.0f)), new Literal("root")), + new Implies(new NotEquals(new Variable("A_2", Float.class), new Constant(0.0f)), new Literal("root")), + new Implies(new NotEquals(new Variable("A_2", Float.class), new Constant(0.0f)), new NotEquals(new Variable("A_1", Float.class), new Constant(0.0f))), + new Implies(new Literal("root"), new Choose(1, Arrays.asList(new NotEquals(new Variable("B", Integer.class), new Constant(0)), new Literal("C")))), + new Implies(new NotEquals(new Variable("B", Integer.class), new Constant(0)), new Literal("root")), + new Implies(new Literal("C"), new Literal("root")))); + + executeSimpleTest(); + } + @Test void withCardinalityAndChildInbetween() { IFeatureTree rootTree = @@ -379,7 +444,7 @@ void simpleOneFeatureAndAttributeAggregate() { new Implies(new Literal("A"), new Literal("root")), new LessThan( new RealAdd(new IfThenElse( - new Variable("A"), new Constant(10.0, Double.class), new Constant(0.0, Double.class))), + new Literal("A"), new Constant(10.0, Double.class), new Constant(0.0, Double.class))), new Constant(200.0, Double.class)))); executeSimpleTest(); diff --git a/src/test/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregateTest.java b/src/test/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregateTest.java index 9e5e09be..f6353895 100644 --- a/src/test/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregateTest.java +++ b/src/test/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregateTest.java @@ -6,11 +6,13 @@ import de.featjar.base.data.Attribute; import de.featjar.base.data.IAttribute; import de.featjar.base.tree.Trees; +import de.featjar.formula.structure.Expressions; import de.featjar.formula.structure.IFormula; import de.featjar.formula.structure.connective.And; import de.featjar.formula.structure.connective.Implies; import de.featjar.formula.structure.predicate.LessThan; import de.featjar.formula.structure.predicate.Literal; +import de.featjar.formula.structure.predicate.NotEquals; import de.featjar.formula.structure.term.aggregate.AttributeAverage; import de.featjar.formula.structure.term.aggregate.AttributeSum; import de.featjar.formula.structure.term.function.IfThenElse; @@ -27,7 +29,7 @@ public class ReplaceAttributeAggregateTest { - private static Map, Object>> attributes; + private static Map, Object>> attributes; @BeforeAll public static void init() { @@ -35,50 +37,75 @@ public static void init() { attributes = new LinkedHashMap<>(); attributes.put( - new Variable("cpu", Boolean.class), + Expressions.literal("cpu"), Map.of( new Attribute<>("cost", Long.class), 10L, new Attribute<>("required", Boolean.class), true, new Attribute<>("power", Float.class), - 104.5f)); + 104.5f + ) + ); attributes.put( - new Variable("gpu", Boolean.class), + Expressions.literal("gpu"), Map.of( new Attribute<>("cost", Long.class), 100L, new Attribute<>("required", Boolean.class), false, new Attribute<>("power", Float.class), - 200.5f)); + 200.5f + ) + ); attributes.put( - new Variable("ram", Boolean.class), - Map.of(new Attribute<>("cost", Long.class), 20L, new Attribute<>("required", Boolean.class), true)); + Expressions.literal("ram"), + Map.of( + new Attribute<>("cost", Long.class), + 20L, + new Attribute<>("required", Boolean.class), + true + ) + ); attributes.put( - new Variable("motherboard", Boolean.class), - Map.of(new Attribute<>("required", Boolean.class), true, new Attribute<>("power", Float.class), 3.5f)); - attributes.put(new Variable("power_supply", Boolean.class), Collections.emptyMap()); + Expressions.literal("motherboard"), + Map.of( + new Attribute<>("required", Boolean.class), + true, + new Attribute<>("power", Float.class), + 3.5f + ) + ); + attributes.put(Expressions.literal("power_supply"), Collections.emptyMap()); + attributes.put( + new NotEquals(new Variable("refreshrate", Double.class), new Constant(0.0)), + Map.of( + new Attribute<>("required", Boolean.class), + true, + new Attribute<>("power", Float.class), + 1.0f + ) + ); } @Test public void test1() { IFormula test = new LessThan(new AttributeSum("cost"), new Constant(200L, Long.class)); - ReplaceAttributeAggregate replaceAttributeAggregate = new ReplaceAttributeAggregate(attributes, Boolean.FALSE); + ReplaceAttributeAggregate replaceAttributeAggregate = new ReplaceAttributeAggregate(attributes, false); Trees.traverse(test, replaceAttributeAggregate); IFormula comparison = new LessThan( new IntegerAdd( new IfThenElse( - new Variable("cpu", Boolean.class), + new Literal("cpu"), new Constant(10L, Long.class), new Constant(0L, Long.class)), new IfThenElse( - new Variable("gpu", Boolean.class), + new Literal("gpu"), new Constant(100L, Long.class), new Constant(0L, Long.class)), new IfThenElse( - new Variable("ram", Boolean.class), + new Literal("ram"), new Constant(20L, Long.class), new Constant(0L, Long.class))), new Constant(200L, Long.class)); @@ -89,50 +116,61 @@ public void test1() { @Test public void test2() { IFormula test = new LessThan(new AttributeSum("cost"), new AttributeAverage("power")); - ReplaceAttributeAggregate replaceAttributeAggregate = new ReplaceAttributeAggregate(attributes, Boolean.FALSE); + ReplaceAttributeAggregate replaceAttributeAggregate = new ReplaceAttributeAggregate(attributes, false); Trees.traverse(test, replaceAttributeAggregate); IFormula comparison = new LessThan( new IntegerAdd( new IfThenElse( - new Variable("cpu", Boolean.class), + new Literal("cpu"), new Constant(10L, Long.class), new Constant(0L, Long.class)), new IfThenElse( - new Variable("gpu", Boolean.class), + new Literal("gpu"), new Constant(100L, Long.class), new Constant(0L, Long.class)), new IfThenElse( - new Variable("ram", Boolean.class), + new Literal("ram"), new Constant(20L, Long.class), new Constant(0L, Long.class))), new RealDivide( new RealAdd( new IfThenElse( - new Variable("cpu", Boolean.class), + new Literal("cpu"), new Constant(104.5, Double.class), new Constant(0.0, Double.class)), new IfThenElse( - new Variable("gpu", Boolean.class), + new Literal("gpu"), new Constant(200.5, Double.class), new Constant(0.0, Double.class)), new IfThenElse( - new Variable("motherboard", Boolean.class), + new Literal("motherboard"), new Constant(3.5, Double.class), - new Constant(0.0, Double.class))), + new Constant(0.0, Double.class)), + new IfThenElse( + new NotEquals(new Variable("refreshrate", Double.class), new Constant(0.0)), + new Constant(1.0, Double.class), + new Constant(0.0, Double.class)) + ), new RealAdd( new IfThenElse( - new Variable("cpu", Boolean.class), + new Literal("cpu"), + new Constant(1.0, Double.class), + new Constant(0.0, Double.class)), + new IfThenElse( + new Literal("gpu"), new Constant(1.0, Double.class), new Constant(0.0, Double.class)), new IfThenElse( - new Variable("gpu", Boolean.class), + new Literal("motherboard"), new Constant(1.0, Double.class), new Constant(0.0, Double.class)), new IfThenElse( - new Variable("motherboard", Boolean.class), + new NotEquals(new Variable("refreshrate", Double.class), new Constant(0.0)), new Constant(1.0, Double.class), - new Constant(0.0, Double.class))))); + new Constant(0.0, Double.class)) + ) + )); assertTrue(test.equalsTree(comparison)); } @@ -143,7 +181,7 @@ public void test3() { new Implies( new Literal("cables"), new LessThan(new AttributeSum("cost"), new Constant(200L, Long.class))), new Literal("case")); - ReplaceAttributeAggregate replaceAttributeAggregate = new ReplaceAttributeAggregate(attributes, Boolean.FALSE); + ReplaceAttributeAggregate replaceAttributeAggregate = new ReplaceAttributeAggregate(attributes, false); Trees.traverse(test, replaceAttributeAggregate); IFormula comparison = new And( @@ -152,15 +190,15 @@ public void test3() { new LessThan( new IntegerAdd( new IfThenElse( - new Variable("cpu", Boolean.class), + new Literal("cpu"), new Constant(10L, Long.class), new Constant(0L, Long.class)), new IfThenElse( - new Variable("gpu", Boolean.class), + new Literal("gpu"), new Constant(100L, Long.class), new Constant(0L, Long.class)), new IfThenElse( - new Variable("ram", Boolean.class), + new Literal("ram"), new Constant(20L, Long.class), new Constant(0L, Long.class))), new Constant(200L, Long.class))), @@ -168,4 +206,4 @@ public void test3() { assertTrue(test.equalsTree(comparison)); } -} +} \ No newline at end of file From 870d1aad7a497716e0dc0b1d9d780c24cbd48771 Mon Sep 17 00:00:00 2001 From: Jonas Hanke Date: Thu, 16 Oct 2025 16:48:57 +0200 Subject: [PATCH 61/67] refactor: runtime exception for unsupported feature types --- src/main/java/de/featjar/feature/model/Features.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/Features.java b/src/main/java/de/featjar/feature/model/Features.java index 72b79938..761b8bc2 100644 --- a/src/main/java/de/featjar/feature/model/Features.java +++ b/src/main/java/de/featjar/feature/model/Features.java @@ -1,6 +1,5 @@ package de.featjar.feature.model; -import de.featjar.base.FeatJAR; import de.featjar.formula.structure.Expressions; import de.featjar.formula.structure.IFormula; import de.featjar.formula.structure.predicate.NotEquals; @@ -21,8 +20,7 @@ public static IFormula createFeatureFormel(IFeature feature, String featureName) } else if(feature.getType().equals(Float.class)) { return new NotEquals(new Variable(featureName, feature.getType()), new Constant(0.0f)); } else { - FeatJAR.log().warning("Could not handle type "+ feature.getType()); - return null; + throw new UnsupportedOperationException("Unsupported feature type: " + feature.getType()); } } } From 13750d0f5381cb760d3319454adc8708fc3fe710 Mon Sep 17 00:00:00 2001 From: Jonas Hanke Date: Fri, 17 Oct 2025 11:24:19 +0200 Subject: [PATCH 62/67] feat: dynamic angle size calculation for groups in latex --- .../tikz/TikzGraphicalFeatureModelFormat.java | 15 +- .../model/io/tikz/format/TikzMainFormat.java | 4 +- .../model/io/tikz/helper/PrintVisitor.java | 52 ++++--- src/main/resources/head.tex | 105 ++++++-------- src/main/resources/test/test-output.tex | 129 ++++++++---------- .../io/tikz/FeatureModelDisplayTikzTest.java | 37 +++-- 6 files changed, 157 insertions(+), 185 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java index 12221dbb..a88b8f24 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java @@ -28,15 +28,20 @@ public Result serialize(IFeatureModel featureModel) { StringBuilder stringBuilder = new StringBuilder(); List problemList = new ArrayList<>(); - stringBuilder.append("\\documentclass[border=5pt]{standalone}"); - stringBuilder.append(LINE_SEPERATOR); + stringBuilder.append("\\documentclass[border=5pt]{standalone}").append(LINE_SEPERATOR); TikzHeadFormat.header(stringBuilder, problemList, false); - stringBuilder.append("\\begin{document}").append(LINE_SEPERATOR).append(" %---The Feature Diagram-----------------------------------------------------").append(LINE_SEPERATOR); + + stringBuilder + .append("\\begin{document}").append(LINE_SEPERATOR) + .append(" %---The Feature Diagram-----------------------------------------------------").append(LINE_SEPERATOR); for (IFeatureTree featureTree : featureModel.getRoots()) { new TikzMainFormat(featureModel, featureTree, stringBuilder).printForest(); } - stringBuilder.append(LINE_SEPERATOR); - stringBuilder.append("\t%---------------------------------------------------------------------------").append(LINE_SEPERATOR).append("\\end{document}"); + stringBuilder + .append(LINE_SEPERATOR) + .append("\t%---------------------------------------------------------------------------").append(LINE_SEPERATOR) + .append("\\end{document}"); + return Result.of(stringBuilder.toString(), problemList); } diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java index abd48353..139c517e 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java @@ -34,7 +34,9 @@ public TikzMainFormat(IFeatureModel featureModel ,IFeatureTree featureTree, Stri * Build the complete tree of the FeatureModel. */ public void printForest() { - stringBuilder.append("\\begin{forest}").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append("\tfeatureDiagram").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append("\t"); + stringBuilder + .append("\\begin{forest}").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR) + .append("\tfeatureDiagram").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append("\t"); PrintVisitor printVisitor = new PrintVisitor(); Trees.traverse(featureTree, printVisitor); diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java index 315bf298..e9e8a6a8 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java @@ -29,23 +29,14 @@ public TraversalAction firstVisit(List path) { new TikzAttributeHelper(feature, stringBuilder) .addFilterValue("name") - .setFilterType(TikzAttributeHelper.FilterType.DISPLAY) + .addFilterValue("abstract") + .setFilterType(TikzAttributeHelper.FilterType.WITH_OUT) .build(); - insertNodeHead(feature); - handleGroups(feature); - return TraversalAction.CONTINUE; - } - - private void handleGroups(IFeature feature) { - IFeatureTree featureTree = feature.getFeatureTree().orElse(null); - if (featureTree == null) { - return; - } - - featureTree.getChildren().forEach(featureStream -> { - FeatJAR.log().info(feature.getName().get() + " - " + featureStream.getFeature().getName().get()); - }); + insertFeatureType(feature); + insertFeatureCardinality(feature); + insertGroupCardinality(feature); + return TraversalAction.CONTINUE; } @Override @@ -59,10 +50,7 @@ public Result getResult() { return Result.of(stringBuilder.toString()); } - private void insertNodeHead(IFeature feature) { - IFeatureTree featureTree = feature.getFeatureTree().orElse(null); - FeatureTree.Group featureTreeParentGroup = feature.getFeatureTree().get().getParentGroup().orElse(null); - + private void insertFeatureType(IFeature feature) { if (feature.isAbstract()) { stringBuilder.append(",abstract"); } @@ -70,6 +58,11 @@ private void insertNodeHead(IFeature feature) { if (feature.isConcrete()) { stringBuilder.append(",concrete"); } + } + + private void insertFeatureCardinality(IFeature feature) { + IFeatureTree featureTree = feature.getFeatureTree().orElse(null); + FeatureTree.Group featureTreeParentGroup = feature.getFeatureTree().get().getParentGroup().orElse(null); if (isNotRootFeature(feature) && featureTreeParentGroup != null && featureTreeParentGroup.isAnd()) { if (featureTree.getFeatureCardinalityLowerBound() == 0 && @@ -84,25 +77,30 @@ private void insertNodeHead(IFeature feature) { feature.getFeatureTree().get().getFeatureCardinalityUpperBound())); } } + } + + private void insertGroupCardinality(IFeature feature) { + IFeatureTree featureTree = feature.getFeatureTree().orElse(null); if (isNotRootFeature(feature)) { - int previousChildrenCount = 0; + int previousChildrenCount = 1; for(int i = 0; i < featureTree.getChildrenGroups().size(); i++) { if(featureTree.getChildrenGroup(i).isPresent()) { FeatureTree.Group group = featureTree.getChildrenGroup(i).get(); + int childrenCount = featureTree.getChildren(i).size(); if(group.isOr()) { - stringBuilder.append(String.format(",or={%d}{%d}", previousChildrenCount + 1, - featureTree.getChildren(i).size())); + stringBuilder.append(String.format(",or={%d}{%d}{%d}", previousChildrenCount, previousChildrenCount + childrenCount - 1, + (2 * previousChildrenCount + childrenCount - 1) / 2)); } else if(group.isAlternative()) { - stringBuilder.append(String.format(",alternative={%d}{%d}", previousChildrenCount + 1, - featureTree.getChildren(i).size())); + stringBuilder.append(String.format(",alternative={%d}{%d}{%d}", previousChildrenCount, previousChildrenCount + childrenCount - 1, + (2 * previousChildrenCount + childrenCount - 1) / 2)); } else if(group.isCardinalityGroup()) { - stringBuilder.append(String.format(",groupcardinality={%d}{%d}", previousChildrenCount + 1, - featureTree.getChildren(i).size())); + stringBuilder.append(String.format(",groupcardinality={%d}{%d}{%d}{%d}{%d}", previousChildrenCount, previousChildrenCount + childrenCount - 1, + (2 * previousChildrenCount + childrenCount - 1) / 2, group.getLowerBound(), group.getUpperBound())); } - previousChildrenCount += featureTree.getChildren(i).size(); + previousChildrenCount += childrenCount; } } } diff --git a/src/main/resources/head.tex b/src/main/resources/head.tex index 79ebc7aa..6567c0d8 100644 --- a/src/main/resources/head.tex +++ b/src/main/resources/head.tex @@ -4,11 +4,8 @@ \usepackage{xcolor} \usetikzlibrary{angles} \usetikzlibrary{positioning} -\usetikzlibrary{quotes} \definecolor{drawColor}{RGB}{128 128 128} \newcommand{\circleSize}{0.25em} -\newcommand{\angleSize}{0.8em} -\newcommand{\angleSizeCardinalityGroup}{1.0em} %------------------------------------------------------------------------------- %---Define the style of the tree------------------------------------------------ \forestset{ @@ -34,46 +31,6 @@ s sep = 1em, } }, - /tikz/redColor/.style={ - fill = red!60, - draw = drawColor - }, - /tikz/orangeColor/.style={ - fill = orange!50, - draw = drawColor - }, - /tikz/yellowColor/.style={ - fill = yellow!50, - draw = drawColor - }, - /tikz/darkGreenColor/.style={ - fill = black!30!green, - draw = drawColor - }, - /tikz/lightGreenColor/.style={ - fill = green!30, - draw = drawColor - }, - /tikz/cyanColor/.style={ - fill = cyan!30, - draw = drawColor - }, - /tikz/lightGrayColor/.style={ - fill = black!10, - draw = drawColor - }, - /tikz/blueColor/.style={ - fill = blue!50, - draw = drawColor - }, - /tikz/magentaColor/.style={ - fill = magenta, - draw = drawColor - }, - /tikz/pinkColor/.style={ - fill = pink!90, - draw = drawColor - }, /tikz/abstract/.style={ fill = blue!85!cyan!5, draw = drawColor @@ -83,40 +40,60 @@ draw = drawColor }, mandatory/.style={ - edge label+={node [mandatory] {} } + edge label+={ + node [mandatory] {} + } }, optional/.style={ - edge label+={node [optional] {} } + edge label+={ + node [optional] {} + } }, - featurecardinality/.style 2 args={ - edge label+={node[midway,fill=white,font=\scriptsize]{#1,#2}} + featurecardinality/.style n args={2}{ + edge label+={ + node[midway,fill=white,font=\scriptsize]{#1,#2} + } }, - or/.style={ + or/.style n args={3}{ tikz+={ - \path (!c1.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!cl.parent) coordinate (C) pic[fill=drawColor, angle radius=\angleSize]{angle}; + \path + (!{current, n=#1}.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!{current, n=#2}.parent) coordinate (C) -- (!{current, n=#3}.parent) coordinate (D) + let \p1 = (A), \p2 = (B), \p3 = (C), \p4 = (D), \n1 = {veclen(\x2 - \x1, \y2 - \y1)}, \n2 = {veclen(\x2 - \x3, \y2 - \y3)}, \n3 = {veclen(\x2 - \x4, \y2 - \y4)} + in pic[ + fill=drawColor, + angle radius={min(\n1/2,\n2/2,\n3/2)} + ]{angle}; } }, - alternative/.style={ + alternative/.style n args={3}{ tikz+={ - \path (!c1.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!cl.parent) coordinate (C) pic[draw=drawColor, angle radius=\angleSize]{angle}; + \path + (!{current, n=#1}.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!{current, n=#2}.parent) coordinate (C) -- (!{current, n=#3}.parent) coordinate (D) + let \p1 = (A), \p2 = (B), \p3 = (C), \p4 = (D), \n1 = {veclen(\x2 - \x1, \y2 - \y1)}, \n2 = {veclen(\x2 - \x3, \y2 - \y3)}, \n3 = {veclen(\x2 - \x4, \y2 - \y4)} + in pic[ + draw=drawColor, + angle radius={min(\n1/2,\n2/2,\n3/2)} + ]{angle}; } }, - groupcardinality/.style n args={4}{ + groupcardinality/.style n args={5}{ tikz+={ - \path (!{current, n=#1}.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!{current, n=#2}.parent) coordinate (C) pic[ - draw=drawColor, - angle radius=7em, - pic text={#3,#4}, - pic text options={ - scale=0.6, - fill=white, - inner sep=0.5pt - } - ]{angle} + \path (!{current, n=#1}.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!{current, n=#2}.parent) coordinate (C) -- (!{current, n=#3}.parent) coordinate (D) + let \p1 = (A), \p2 = (B), \p3 = (C), \p4 = (D), \n1 = {veclen(\x2 - \x1, \y2 - \y1)}, \n2 = {veclen(\x2 - \x3, \y2 - \y3)}, \n3 = {veclen(\x2 - \x4, \y2 - \y4)} + in pic[ + draw=drawColor, + angle radius={min(\n1/2,\n2/2,\n3/2)}, + pic text={#4,#5}, + pic text options={ + scale=0.6, + fill=white, + inner sep=0.3pt + } + ]{angle}; } }, /tikz/placeholder/.style={ - }, + }, collapsed/.style={ rounded corners, no edge, @@ -134,6 +111,6 @@ minimum size = 1.2em, minimum width = 0.8em, scale=0.9 - }, + } } %------------------------------------------------------------------------------- \ No newline at end of file diff --git a/src/main/resources/test/test-output.tex b/src/main/resources/test/test-output.tex index 505187bd..1d9015fe 100644 --- a/src/main/resources/test/test-output.tex +++ b/src/main/resources/test/test-output.tex @@ -5,11 +5,8 @@ \usepackage{xcolor} \usetikzlibrary{angles} \usetikzlibrary{positioning} -\usetikzlibrary{quotes} \definecolor{drawColor}{RGB}{128 128 128} \newcommand{\circleSize}{0.25em} -\newcommand{\angleSize}{0.8em} -\newcommand{\angleSizeCardinalityGroup}{1.0em} %------------------------------------------------------------------------------- %---Define the style of the tree------------------------------------------------ \forestset{ @@ -35,46 +32,6 @@ s sep = 1em, } }, - /tikz/redColor/.style={ - fill = red!60, - draw = drawColor - }, - /tikz/orangeColor/.style={ - fill = orange!50, - draw = drawColor - }, - /tikz/yellowColor/.style={ - fill = yellow!50, - draw = drawColor - }, - /tikz/darkGreenColor/.style={ - fill = black!30!green, - draw = drawColor - }, - /tikz/lightGreenColor/.style={ - fill = green!30, - draw = drawColor - }, - /tikz/cyanColor/.style={ - fill = cyan!30, - draw = drawColor - }, - /tikz/lightGrayColor/.style={ - fill = black!10, - draw = drawColor - }, - /tikz/blueColor/.style={ - fill = blue!50, - draw = drawColor - }, - /tikz/magentaColor/.style={ - fill = magenta, - draw = drawColor - }, - /tikz/pinkColor/.style={ - fill = pink!90, - draw = drawColor - }, /tikz/abstract/.style={ fill = blue!85!cyan!5, draw = drawColor @@ -84,40 +41,60 @@ draw = drawColor }, mandatory/.style={ - edge label+={node [mandatory] {} } + edge label+={ + node [mandatory] {} + } }, optional/.style={ - edge label+={node [optional] {} } + edge label+={ + node [optional] {} + } }, - featurecardinality/.style 2 args={ - edge label+={node[midway,fill=white,font=\scriptsize]{#1,#2}} + featurecardinality/.style n args={2}{ + edge label+={ + node[midway,fill=white,font=\scriptsize]{#1,#2} + } }, - or/.style={ + or/.style n args={3}{ tikz+={ - \path (!c1.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!cl.parent) coordinate (C) pic[fill=drawColor, angle radius=\angleSize]{angle}; + \path + (!{current, n=#1}.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!{current, n=#2}.parent) coordinate (C) -- (!{current, n=#3}.parent) coordinate (D) + let \p1 = (A), \p2 = (B), \p3 = (C), \p4 = (D), \n1 = {veclen(\x2 - \x1, \y2 - \y1)}, \n2 = {veclen(\x2 - \x3, \y2 - \y3)}, \n3 = {veclen(\x2 - \x4, \y2 - \y4)} + in pic[ + fill=drawColor, + angle radius={min(\n1/2,\n2/2,\n3/2)} + ]{angle}; } }, - alternative/.style={ + alternative/.style n args={3}{ tikz+={ - \path (!c1.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!cl.parent) coordinate (C) pic[draw=drawColor, angle radius=\angleSize]{angle}; + \path + (!{current, n=#1}.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!{current, n=#2}.parent) coordinate (C) -- (!{current, n=#3}.parent) coordinate (D) + let \p1 = (A), \p2 = (B), \p3 = (C), \p4 = (D), \n1 = {veclen(\x2 - \x1, \y2 - \y1)}, \n2 = {veclen(\x2 - \x3, \y2 - \y3)}, \n3 = {veclen(\x2 - \x4, \y2 - \y4)} + in pic[ + draw=drawColor, + angle radius={min(\n1/2,\n2/2,\n3/2)} + ]{angle}; } }, - groupcardinality/.style n args={4}{ + groupcardinality/.style n args={5}{ tikz+={ - \path (!{current, n=#1}.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!{current, n=#2}.parent) coordinate (C) pic[ - draw=drawColor, - angle radius=7em, - pic text={#3,#4}, - pic text options={ - scale=0.6, - fill=white, - inner sep=0.5pt - } - ]{angle} + \path (!{current, n=#1}.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!{current, n=#2}.parent) coordinate (C) -- (!{current, n=#3}.parent) coordinate (D) + let \p1 = (A), \p2 = (B), \p3 = (C), \p4 = (D), \n1 = {veclen(\x2 - \x1, \y2 - \y1)}, \n2 = {veclen(\x2 - \x3, \y2 - \y3)}, \n3 = {veclen(\x2 - \x4, \y2 - \y4)} + in pic[ + draw=drawColor, + angle radius={min(\n1/2,\n2/2,\n3/2)}, + pic text={#4,#5}, + pic text options={ + scale=0.6, + fill=white, + inner sep=0.3pt + } + ]{angle}; } }, /tikz/placeholder/.style={ - }, + }, collapsed/.style={ rounded corners, no edge, @@ -135,7 +112,7 @@ minimum size = 1.2em, minimum width = 0.8em, scale=0.9 - }, + } } %------------------------------------------------------------------------------- \begin{document} @@ -143,21 +120,23 @@ \begin{forest} featureDiagram [\multicolumn{2}{c}{Hello} \\\hline -\small\texttt{name (String)} &\small\texttt{= Hello} \\ ,align=ll,abstract[\multicolumn{2}{c}{Feature} \\\hline -\small\texttt{name (String)} &\small\texttt{= Feature} \\ -,align=ll,abstract,featurecardinality={0}{2},alternative={1}{2},or={3}{2}[\multicolumn{2}{c}{Wonderful1} \\\hline -\small\texttt{name (String)} &\small\texttt{= Wonderful1} \\ +,align=ll,abstract,featurecardinality={0}{2},alternative={1}{2}{1},or={3}{4}{3},groupcardinality={5}{6}{5}{7}{8}[\multicolumn{2}{c}{Wonderful1} \\\hline +\small\texttt{who (String)} &\small\texttt{= you} \\ +\small\texttt{when (String)} &\small\texttt{= now} \\ ,align=ll,concrete][\multicolumn{2}{c}{Beautiful1} \\\hline -\small\texttt{name (String)} &\small\texttt{= Beautiful1} \\ ,align=ll,concrete][\multicolumn{2}{c}{Wonderful2} \\\hline -\small\texttt{name (String)} &\small\texttt{= Wonderful2} \\ ,align=ll,concrete][\multicolumn{2}{c}{Beautiful2} \\\hline -\small\texttt{name (String)} &\small\texttt{= Beautiful2} \\ +,align=ll,concrete,groupcardinality={1}{3}{2}{0}{2}[\multicolumn{2}{c}{Meaningful1} \\\hline +,align=ll,concrete][\multicolumn{2}{c}{Meaningful2} \\\hline +,align=ll,concrete][\multicolumn{2}{c}{Meaningful3} \\\hline +,align=ll,concrete]][\multicolumn{2}{c}{Wonderful3} \\\hline +\small\texttt{who (String)} &\small\texttt{= you} \\ +,align=ll,concrete][\multicolumn{2}{c}{Beautiful3} \\\hline ,align=ll,concrete]][\multicolumn{2}{c}{World1} \\\hline -\small\texttt{name (String)} &\small\texttt{= World1} \\ +\small\texttt{size (Double)} &\small\texttt{= 6000.0} \\ +\small\texttt{population (Integer)} &\small\texttt{= 1} \\ ,align=ll,concrete,optional][\multicolumn{2}{c}{World2} \\\hline -\small\texttt{name (String)} &\small\texttt{= World2} \\ ,align=ll,concrete,mandatory]] \matrix [anchor=north west] at (current bounding box.north east) { \node [placeholder] {}; \\ @@ -179,8 +158,8 @@ \node [label=right:Alternative Group] {}; \\ }; \matrix [below=1mm of current bounding box] { - \node {\( \text{A} \land \text{B} \)}; \\ - \node {\( \text{C} \Rightarrow \forall( \text{A} \Leftrightarrow \text{C} )\)}; \\ + \node {\( \text{World1} \land \text{Wonderful1} \)}; \\ + \node {\( \text{World2} \Rightarrow ( \text{Beautiful2} \Leftrightarrow \text{Beautiful3} )\)}; \\ }; \end{forest} diff --git a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java index 7e38ea94..0bffacd7 100644 --- a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java +++ b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java @@ -25,7 +25,6 @@ */ public class FeatureModelDisplayTikzTest { - private final StringBuilder stringBuilder = new StringBuilder(); private static IFeatureModel featureModel; @BeforeAll @@ -37,11 +36,21 @@ public static void init() { IFeature featureRootS = featureModel.mutate().addFeature("Hello"); IFeature feature = featureModel.mutate().addFeature("Feature"); IFeature world1 = featureModel.mutate().addFeature("World1"); + world1.mutate().setAttributeValue(new Attribute<>("size", Double.class), 6000.0); + world1.mutate().setAttributeValue(new Attribute<>("population", Integer.class), 1); IFeature world2 = featureModel.mutate().addFeature("World2"); IFeature wonderful1 = featureModel.mutate().addFeature("Wonderful1"); + wonderful1.mutate().setAttributeValue(new Attribute<>("who", String.class), "you"); + wonderful1.mutate().setAttributeValue(new Attribute<>( "when", String.class), "now"); IFeature beautiful1 = featureModel.mutate().addFeature("Beautiful1"); IFeature wonderful2 = featureModel.mutate().addFeature("Wonderful2"); IFeature beautiful2 = featureModel.mutate().addFeature("Beautiful2"); + IFeature wonderful3 = featureModel.mutate().addFeature("Wonderful3"); + wonderful3.mutate().setAttributeValue(new Attribute<>("who", String.class), "you"); + IFeature beautiful3 = featureModel.mutate().addFeature("Beautiful3"); + IFeature meaningful1 = featureModel.mutate().addFeature("Meaningful1"); + IFeature meaningful2 = featureModel.mutate().addFeature("Meaningful2"); + IFeature meaningful3 = featureModel.mutate().addFeature("Meaningful3"); featureRootS.mutate().setAbstract(); @@ -54,19 +63,22 @@ public static void init() { feature.mutate().setAbstract(); int group1 = firstFeatureTree.mutate().addAlternativeGroup(); int group2 = firstFeatureTree.mutate().addOrGroup(); - - Attribute attribute = new Attribute<>("any", "test", String.class); - Attribute attribute2 = new Attribute<>("anyd", "testd", String.class); - wonderful1.mutate().setAttributeValue(attribute, "value"); - wonderful1.mutate().setAttributeValue(attribute2, "value2"); + int group3 = firstFeatureTree.mutate().addCardinalityGroup(Range.of(7,8)); firstFeatureTree.mutate().addFeatureBelow(wonderful1, 0, group1); - firstFeatureTree.mutate().setFeatureCardinality(Range.of(0,2)); firstFeatureTree.mutate().addFeatureBelow(beautiful1, 1, group1); firstFeatureTree.mutate().addFeatureBelow(wonderful2, 2, group2); - firstFeatureTree.mutate().addFeatureBelow(beautiful2, 3, group2); + IFeatureTree beautiful2FeatureTree = firstFeatureTree.mutate().addFeatureBelow(beautiful2, 3, group2); + + int group4 = beautiful2FeatureTree.mutate().addCardinalityGroup(Range.of(0,2)); + beautiful2FeatureTree.mutate().addFeatureBelow(meaningful1, 0, group4); + beautiful2FeatureTree.mutate().addFeatureBelow(meaningful2, 1, group4); + beautiful2FeatureTree.mutate().addFeatureBelow(meaningful3, 2, group4); + + firstFeatureTree.mutate().addFeatureBelow(wonderful3, 4, group3); + firstFeatureTree.mutate().addFeatureBelow(beautiful3, 5, group3); rootTree.mutate().addFeatureBelow(world1); @@ -74,8 +86,8 @@ public static void init() { world2FeatureTree.mutate().setFeatureCardinality(Range.of(1, 1)); // Constraints - featureModel.addConstraint(new And(Expressions.literal("A"), Expressions.literal("B"))); - featureModel.addConstraint(new Implies(Expressions.literal("C"), new ForAll(new Variable("A"), new BiImplies(Expressions.literal("A"), Expressions.literal("C"))))); + featureModel.addConstraint(new And(Expressions.literal("World1"), Expressions.literal("Wonderful1"))); + featureModel.addConstraint(new Implies(Expressions.literal("World2"), new BiImplies(Expressions.literal("Beautiful2"), Expressions.literal("Beautiful3")))); FeatureModelDisplayTikzTest.featureModel = featureModel; } @@ -99,7 +111,7 @@ public void perform() { } // Todo: Add @Test here and remove the other @Test on the method perform - //@Test + // @Test public void createTestFile() { new TikzGraphicalFeatureModelFormat().serialize(featureModel).ifPresent(this::writeToFile); } @@ -155,5 +167,4 @@ private StringBuilder loadTestFile() { return stringBuilderFile; } - -} +} \ No newline at end of file From 2509783b28273d98a92aac6ef073fdf9685bc97e Mon Sep 17 00:00:00 2001 From: Lara Date: Fri, 17 Oct 2025 12:50:32 +0200 Subject: [PATCH 63/67] fix: added correct user function for the attributes and remove underline if the feature has no attributes to display --- .../tikz/TikzGraphicalFeatureModelFormat.java | 21 ++++++- .../model/io/tikz/format/TikzMainFormat.java | 11 +++- .../model/io/tikz/helper/PrintVisitor.java | 17 +++++- .../io/tikz/helper/TikzAttributeHelper.java | 22 ++++++- src/test/java/show/TikzShow.java | 57 +++++++++++++++++++ 5 files changed, 119 insertions(+), 9 deletions(-) create mode 100644 src/test/java/show/TikzShow.java diff --git a/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java index a88b8f24..f53f53c3 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java @@ -7,6 +7,7 @@ import de.featjar.feature.model.IFeatureTree; import de.featjar.feature.model.io.tikz.format.TikzHeadFormat; import de.featjar.feature.model.io.tikz.format.TikzMainFormat; +import de.featjar.feature.model.io.tikz.helper.TikzAttributeHelper; import java.util.ArrayList; import java.util.List; @@ -23,6 +24,16 @@ public class TikzGraphicalFeatureModelFormat implements IFormat { public static String LINE_SEPERATOR = System.lineSeparator(); + private TikzAttributeHelper.FilterType filterType = TikzAttributeHelper.FilterType.WITH_OUT; // default + private List filterValues = new ArrayList<>(); // default + + public TikzGraphicalFeatureModelFormat() {} // default + + public TikzGraphicalFeatureModelFormat(TikzAttributeHelper.FilterType filterType, List filterValues) { + this.filterType = filterType; + this.filterValues = filterValues; + } + @Override public Result serialize(IFeatureModel featureModel) { StringBuilder stringBuilder = new StringBuilder(); @@ -35,7 +46,7 @@ public Result serialize(IFeatureModel featureModel) { .append("\\begin{document}").append(LINE_SEPERATOR) .append(" %---The Feature Diagram-----------------------------------------------------").append(LINE_SEPERATOR); for (IFeatureTree featureTree : featureModel.getRoots()) { - new TikzMainFormat(featureModel, featureTree, stringBuilder).printForest(); + new TikzMainFormat(featureModel, featureTree, stringBuilder, filterType, filterValues).printForest(); } stringBuilder .append(LINE_SEPERATOR) @@ -45,6 +56,14 @@ public Result serialize(IFeatureModel featureModel) { return Result.of(stringBuilder.toString(), problemList); } + public void setFilterType(TikzAttributeHelper.FilterType filterType) { + this.filterType = filterType; + } + + public void setFilterValues(List filterValues) { + this.filterValues = filterValues; + } + @Override public boolean supportsWrite() { return true; diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java index 139c517e..6fa3f731 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java @@ -5,12 +5,15 @@ import de.featjar.feature.model.IFeatureModel; import de.featjar.feature.model.IFeatureTree; import de.featjar.feature.model.io.tikz.TikzGraphicalFeatureModelFormat; +import de.featjar.feature.model.io.tikz.helper.TikzAttributeHelper; import de.featjar.feature.model.io.tikz.helper.TikzMatrixHelper; import de.featjar.feature.model.io.tikz.helper.TikzMatrixType; import de.featjar.feature.model.io.tikz.helper.PrintVisitor; import de.featjar.formula.io.textual.ExpressionSerializer; import de.featjar.formula.io.textual.LaTexSymbols; +import java.util.List; + /** * This class generates the Tikz representation of a {@link IFeatureModel} including all constraints ({@link IConstraint}). * @@ -23,11 +26,15 @@ public class TikzMainFormat { private final IFeatureModel featureModel; private final IFeatureTree featureTree; private final StringBuilder stringBuilder; + private final TikzAttributeHelper.FilterType filterType; + private final List filterValues; - public TikzMainFormat(IFeatureModel featureModel ,IFeatureTree featureTree, StringBuilder stringBuilder) { + public TikzMainFormat(IFeatureModel featureModel , IFeatureTree featureTree, StringBuilder stringBuilder, TikzAttributeHelper.FilterType filterType, List filterValues) { this.featureModel = featureModel; this.featureTree = featureTree; this.stringBuilder = stringBuilder; + this.filterType = filterType; + this.filterValues = filterValues; } /** @@ -38,7 +45,7 @@ public void printForest() { .append("\\begin{forest}").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR) .append("\tfeatureDiagram").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append("\t"); - PrintVisitor printVisitor = new PrintVisitor(); + PrintVisitor printVisitor = new PrintVisitor(filterType, filterValues); Trees.traverse(featureTree, printVisitor); stringBuilder.append(printVisitor.getResult().get()); diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java index e9e8a6a8..72496063 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java @@ -6,6 +6,7 @@ import de.featjar.feature.model.FeatureTree; import de.featjar.feature.model.IFeature; import de.featjar.feature.model.IFeatureTree; +import java.util.ArrayList; import java.util.List; /** @@ -19,8 +20,19 @@ public class PrintVisitor implements ITreeVisitor { private final StringBuilder stringBuilder; + private final TikzAttributeHelper.FilterType filterType; + private final List filterValues; + public PrintVisitor() { this.stringBuilder = new StringBuilder(); + this.filterType = TikzAttributeHelper.FilterType.WITH_OUT; // default + this.filterValues = new ArrayList<>(); + } + + public PrintVisitor(TikzAttributeHelper.FilterType filterType, List filterValues) { + this.stringBuilder = new StringBuilder(); + this.filterType = filterType; + this.filterValues = filterValues; } @Override @@ -28,9 +40,8 @@ public TraversalAction firstVisit(List path) { IFeature feature = ITreeVisitor.getCurrentNode(path).getFeature(); new TikzAttributeHelper(feature, stringBuilder) - .addFilterValue("name") - .addFilterValue("abstract") - .setFilterType(TikzAttributeHelper.FilterType.WITH_OUT) + .addFilterValue(filterValues) + .setFilterType(filterType) .build(); insertFeatureType(feature); insertFeatureCardinality(feature); diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/TikzAttributeHelper.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/TikzAttributeHelper.java index 8d1d3ac6..67cd86a1 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/helper/TikzAttributeHelper.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/TikzAttributeHelper.java @@ -54,8 +54,8 @@ public TikzAttributeHelper setFilterType(FilterType filterType) { * @param values (the keys, words or whatever that will be filtered in the running process. * @return this */ - public TikzAttributeHelper addFilterValue(String... values) { - filter.addAll(List.of(values)); + public TikzAttributeHelper addFilterValue(List values) { + filter.addAll(values); return this; } @@ -65,7 +65,7 @@ private void writeAttributes(IFeature feature) { Map, Object> iAttributeObjectMap = feature.getAttributes().orElse(null); // check: if the attribute map is empty or null - if (iAttributeObjectMap == null || iAttributeObjectMap.isEmpty()) { + if (iAttributeObjectMap == null || iAttributeObjectMap.isEmpty() || countAttributeToDisplay(iAttributeObjectMap) == 0) { stringBuilder.append("[").append(featureName); // add the name without multicolumn return; } @@ -88,6 +88,22 @@ private void writeAttributes(IAttribute attribute, Object object, StringBuild replace(VALUE, 12, 2); } + /** + * Count all items to display in the feature for more functions and more beauty + * + * @param values (Map of the feature with his attributes) + * @return count if items to diplay + */ + private int countAttributeToDisplay(Map, Object> values) { + int size = values.size(); + for (IAttribute attribute : values.keySet()) { + if (filterWithType(attribute.getName().toUpperCase())) { + size-=1; + } + } + return size; + } + private String replace(String key, Object... values) { String result = key; for (short i = 0; i < values.length; i++) { diff --git a/src/test/java/show/TikzShow.java b/src/test/java/show/TikzShow.java new file mode 100644 index 00000000..68245ede --- /dev/null +++ b/src/test/java/show/TikzShow.java @@ -0,0 +1,57 @@ +package show; + +import de.featjar.base.FeatJAR; +import de.featjar.base.data.identifier.Identifiers; +import de.featjar.base.io.IO; +import de.featjar.feature.model.FeatureModel; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureTree; +import de.featjar.feature.model.io.tikz.TikzGraphicalFeatureModelFormat; +import de.featjar.feature.model.io.tikz.helper.TikzAttributeHelper; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.List; + +public class TikzShow { + + public static void main(String[] args) { + FeatJAR.testConfiguration().initialize(); // init FeatJAR + + FeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); // create feature model + + IFeature featureRoot = featureModel.mutate().addFeature("Hello"); + featureRoot.mutate().setAbstract(); + + IFeature feature = featureModel.mutate().addFeature("Feature"); + feature.mutate().setAbstract(); + + IFeature world = featureModel.mutate().addFeature("World"); + IFeature wonderful = featureModel.mutate().addFeature("Wonderful"); + IFeature beautiful = featureModel.mutate().addFeature("Beautiful"); + + IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureRoot); // create tree and make it to an and group + rootTree.mutate().toAndGroup(); + + IFeatureTree firstFeatureTree = rootTree.mutate().addFeatureBelow(feature); + firstFeatureTree.mutate().toOrGroup(); + + firstFeatureTree.mutate().addFeatureBelow(wonderful); + firstFeatureTree.mutate().addFeatureBelow(beautiful); + + rootTree.mutate().addFeatureBelow(world); + + TikzGraphicalFeatureModelFormat tikzGraphicalFeatureModelFormat = new TikzGraphicalFeatureModelFormat( + TikzAttributeHelper.FilterType.WITH_OUT, + List.of("name") + ); + + try { + // write class output to a file + IO.save(featureModel, Paths.get("src", "test", "java", "show", "test-output-show.tex"), tikzGraphicalFeatureModelFormat); + FeatJAR.log().info("Build run successfully"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} From c4f775d5d28a39f2455f811da86eb3761c86406b Mon Sep 17 00:00:00 2001 From: Lara Date: Fri, 17 Oct 2025 12:50:59 +0200 Subject: [PATCH 64/67] fix: changed test to running version --- src/main/resources/test/test-output.tex | 18 ++++-------------- .../model/io/tikz/AttributeFilterTest.java | 10 ++++++---- .../io/tikz/FeatureModelDisplayTikzTest.java | 15 ++++++++++++--- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/src/main/resources/test/test-output.tex b/src/main/resources/test/test-output.tex index 1d9015fe..8d5cf7df 100644 --- a/src/main/resources/test/test-output.tex +++ b/src/main/resources/test/test-output.tex @@ -119,25 +119,15 @@ %---The Feature Diagram----------------------------------------------------- \begin{forest} featureDiagram - [\multicolumn{2}{c}{Hello} \\\hline -,align=ll,abstract[\multicolumn{2}{c}{Feature} \\\hline -,align=ll,abstract,featurecardinality={0}{2},alternative={1}{2}{1},or={3}{4}{3},groupcardinality={5}{6}{5}{7}{8}[\multicolumn{2}{c}{Wonderful1} \\\hline + [Hello,abstract[Feature,abstract,featurecardinality={0}{2},alternative={1}{2}{1},or={3}{4}{3},groupcardinality={5}{6}{5}{7}{8}[\multicolumn{2}{c}{Wonderful1} \\\hline \small\texttt{who (String)} &\small\texttt{= you} \\ \small\texttt{when (String)} &\small\texttt{= now} \\ -,align=ll,concrete][\multicolumn{2}{c}{Beautiful1} \\\hline -,align=ll,concrete][\multicolumn{2}{c}{Wonderful2} \\\hline -,align=ll,concrete][\multicolumn{2}{c}{Beautiful2} \\\hline -,align=ll,concrete,groupcardinality={1}{3}{2}{0}{2}[\multicolumn{2}{c}{Meaningful1} \\\hline -,align=ll,concrete][\multicolumn{2}{c}{Meaningful2} \\\hline -,align=ll,concrete][\multicolumn{2}{c}{Meaningful3} \\\hline -,align=ll,concrete]][\multicolumn{2}{c}{Wonderful3} \\\hline +,align=ll,concrete][Beautiful1,concrete][Wonderful2,concrete][Beautiful2,concrete,groupcardinality={1}{3}{2}{0}{2}[Meaningful1,concrete][Meaningful2,concrete][Meaningful3,concrete]][\multicolumn{2}{c}{Wonderful3} \\\hline \small\texttt{who (String)} &\small\texttt{= you} \\ -,align=ll,concrete][\multicolumn{2}{c}{Beautiful3} \\\hline -,align=ll,concrete]][\multicolumn{2}{c}{World1} \\\hline +,align=ll,concrete][Beautiful3,concrete]][\multicolumn{2}{c}{World1} \\\hline \small\texttt{size (Double)} &\small\texttt{= 6000.0} \\ \small\texttt{population (Integer)} &\small\texttt{= 1} \\ -,align=ll,concrete,optional][\multicolumn{2}{c}{World2} \\\hline -,align=ll,concrete,mandatory]] +,align=ll,concrete,optional][World2,concrete,mandatory]] \matrix [anchor=north west] at (current bounding box.north east) { \node [placeholder] {}; \\ }; diff --git a/src/test/java/de/featjar/feature/model/io/tikz/AttributeFilterTest.java b/src/test/java/de/featjar/feature/model/io/tikz/AttributeFilterTest.java index 04468e3c..1950e0d9 100644 --- a/src/test/java/de/featjar/feature/model/io/tikz/AttributeFilterTest.java +++ b/src/test/java/de/featjar/feature/model/io/tikz/AttributeFilterTest.java @@ -11,6 +11,8 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import java.util.List; + /** * Test for displaying the attributes in tikz (filter) * @@ -66,7 +68,7 @@ public void test1() { TikzAttributeHelper tikzAttributeHelper = new TikzAttributeHelper(feature, stringBuilder) .setFilterType(TikzAttributeHelper.FilterType.DISPLAY) - .addFilterValue("int-attribute"); + .addFilterValue(List.of("int-attribute")); tikzAttributeHelper.build(); @@ -79,7 +81,7 @@ public void test2() { TikzAttributeHelper tikzAttributeHelper = new TikzAttributeHelper(feature, stringBuilder) .setFilterType(TikzAttributeHelper.FilterType.DISPLAY) - .addFilterValue("int-attribute", "bool-attribute"); + .addFilterValue(List.of("int-attribute", "bool-attribute")); tikzAttributeHelper.build(); @@ -92,7 +94,7 @@ public void test3() { TikzAttributeHelper tikzAttributeHelper = new TikzAttributeHelper(feature, stringBuilder) .setFilterType(TikzAttributeHelper.FilterType.WITH_OUT) - .addFilterValue("int-attribute"); + .addFilterValue(List.of("int-attribute")); tikzAttributeHelper.build(); @@ -105,7 +107,7 @@ public void test4() { TikzAttributeHelper tikzAttributeHelper = new TikzAttributeHelper(feature, stringBuilder) .setFilterType(TikzAttributeHelper.FilterType.WITH_OUT) - .addFilterValue("int-attribute", "bool-attribute"); + .addFilterValue(List.of("int-attribute", "bool-attribute")); tikzAttributeHelper.build(); diff --git a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java index 0bffacd7..81caf8c0 100644 --- a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java +++ b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java @@ -5,9 +5,9 @@ import de.featjar.base.data.Range; import de.featjar.base.data.identifier.Identifiers; import de.featjar.feature.model.*; +import de.featjar.feature.model.io.tikz.helper.TikzAttributeHelper; import de.featjar.formula.structure.Expressions; import de.featjar.formula.structure.connective.*; -import de.featjar.formula.structure.term.value.Variable; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -15,6 +15,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.List; /** * Test the full output with a test feature model and attributes, constrains and more @@ -102,7 +103,11 @@ public void perform() { String value = expectedOutput.toString(); - new TikzGraphicalFeatureModelFormat().serialize(featureModel).ifPresent(output -> { + TikzGraphicalFeatureModelFormat tikzGraphicalFeatureModelFormat = new TikzGraphicalFeatureModelFormat( + TikzAttributeHelper.FilterType.WITH_OUT, + List.of("name", "abstract") + ); + tikzGraphicalFeatureModelFormat.serialize(featureModel).ifPresent(output -> { FeatJAR.log().info("Expected Output: " + value); FeatJAR.log().info("Acutally Output: " + output); @@ -113,7 +118,11 @@ public void perform() { // Todo: Add @Test here and remove the other @Test on the method perform // @Test public void createTestFile() { - new TikzGraphicalFeatureModelFormat().serialize(featureModel).ifPresent(this::writeToFile); + TikzGraphicalFeatureModelFormat tikzGraphicalFeatureModelFormat = new TikzGraphicalFeatureModelFormat( + TikzAttributeHelper.FilterType.WITH_OUT, + List.of("name", "abstract") + ); + tikzGraphicalFeatureModelFormat.serialize(featureModel).ifPresent(this::writeToFile); } /** From dd13f832698cc2d595cd0f3227d71c82084a62c6 Mon Sep 17 00:00:00 2001 From: Lara Date: Fri, 17 Oct 2025 14:35:19 +0200 Subject: [PATCH 65/67] fix: ignore constrains if they are empty --- .../featjar/feature/model/io/tikz/format/TikzMainFormat.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java index 6fa3f731..efb9bf33 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java @@ -54,7 +54,9 @@ public void printForest() { if (!featureTree.getFeature().isHidden()) { printLegend(); } - printConstraints(); + if(!featureModel.getConstraints().isEmpty()) { + printConstraints(); + } stringBuilder.append("\\end{forest}").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); } From 69a0c7434b94aeec704f394ac838feb8643b6570 Mon Sep 17 00:00:00 2001 From: Jonas Hanke Date: Fri, 17 Oct 2025 15:02:15 +0200 Subject: [PATCH 66/67] refactor: added documentation for Features --- src/main/java/de/featjar/feature/model/Features.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/de/featjar/feature/model/Features.java b/src/main/java/de/featjar/feature/model/Features.java index 761b8bc2..ad0693d8 100644 --- a/src/main/java/de/featjar/feature/model/Features.java +++ b/src/main/java/de/featjar/feature/model/Features.java @@ -6,6 +6,16 @@ import de.featjar.formula.structure.term.value.Constant; import de.featjar.formula.structure.term.value.Variable; +/** + * Defines useful methods to wrap a bool or numeric feature into a IFormula: + * bool: {@link de.featjar.formula.structure.predicate.Literal} + * int: {@link NotEquals 0} + * float: {@link NotEquals 0} + * + * Numeric features are therefore selected, if there value is not 0. + * + * @author Jonas Hanke + */ public class Features { public static IFormula createFeatureFormel(IFeature feature) { From b72aa0223fc2c26c1121e9390b7a4ce866e10110 Mon Sep 17 00:00:00 2001 From: Lara Date: Fri, 17 Oct 2025 15:03:07 +0200 Subject: [PATCH 67/67] fix: filter color with the string and not with the object --- .../featjar/feature/model/io/tikz/color/TikzFeatureColor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/featjar/feature/model/io/tikz/color/TikzFeatureColor.java b/src/main/java/de/featjar/feature/model/io/tikz/color/TikzFeatureColor.java index 5f15eec8..304ad1c7 100644 --- a/src/main/java/de/featjar/feature/model/io/tikz/color/TikzFeatureColor.java +++ b/src/main/java/de/featjar/feature/model/io/tikz/color/TikzFeatureColor.java @@ -21,9 +21,9 @@ public enum TikzFeatureColor { PINK("pinkColor"), NO_COLOR(""); - public static String color(TikzFeatureColor tikzFeatureColor) { + public static String color(String tikzFeatureColor) { for (TikzFeatureColor tikzFeatureColors : values()) { - if (tikzFeatureColor.equals(tikzFeatureColors)) { + if (tikzFeatureColors.getColor().equalsIgnoreCase(tikzFeatureColor)) { return tikzFeatureColors.getColor(); } }