/* FeatureIDE - An IDE to support feature-oriented software development
* Copyright (C) 2005-2009 FeatureIDE Team, University of Magdeburg
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
* See http://www.fosd.de/featureide/ for further information.
*/
package featureide.fm.core;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.prop4j.And;
import org.prop4j.Implies;
import org.prop4j.Literal;
import org.prop4j.Node;
import org.prop4j.Not;
import org.prop4j.Or;
import org.prop4j.SatSolver;
import org.sat4j.specs.TimeoutException;
import featureide.fm.core.editing.NodeCreator;
/**
* The model representation of the feature tree that notifies listeners of
* changes in the tree.
*
* @author Thomas Thuem
*
*/
public class FeatureModel implements PropertyConstants {
/**
* the root feature
*/
private Feature root;
/**
* a hashtable containing all features
*/
private Hashtable<String, Feature> featureTable = new Hashtable<String, Feature>();
/**
* all comment lines from the model file without line number at which they
* occur
*/
private String comments;
/**
*
*/
private List<Node> propNodes = new LinkedList<Node>();
private List<Constraint> constraints = new LinkedList<Constraint>();
/**
* This string saves the annotations from the model file as they were read,
* because they were not yet used.
*/
private String annotations;
/**
* a list containing all renamings since the last save
*/
private LinkedList<Renaming> renamings = new LinkedList<Renaming>();
private boolean abstractFeatures;
public FeatureModel() {
reset();
}
public void reset() {
if (root != null) {
while (root.hasChildren()) {
Feature child = root.getLastChild();
deleteChildFeatures(child);
root.removeChild(child);
featureTable.remove(child);
}
root = null;
}
featureTable.clear();
renamings.clear();
propNodes.clear();
constraints.clear();
comments = null;
annotations = null;
abstractFeatures = true;
}
private void deleteChildFeatures(Feature feature) {
while (feature.hasChildren()) {
Feature child = feature.getLastChild();
deleteChildFeatures(child);
feature.removeChild(child);
featureTable.remove(child);
}
}
public String getAnnotations() {
return annotations;
}
public void setAnnotations(String annotations) {
this.annotations = annotations;
}
public String getComments() {
return comments;
}
public void setComments(String comments) {
this.comments = comments;
}
public List<Node> getPropositionalNodes() {
return Collections.unmodifiableList(propNodes);
}
public void addPropositionalNode(Node node) {
propNodes.add(node);
constraints.add(new Constraint(this, constraints.size()));
}
public void removePropositionalNode(Node node) {
propNodes.remove(node);
constraints.remove(constraints.size() - 1);
}
public void removePropositionalNode(int index) {
propNodes.remove(index);
constraints.remove(constraints.size() - 1);
}
public Feature getRoot() {
return root;
}
public void setRoot(Feature root) {
this.root = root;
}
public boolean addFeature(Feature feature) {
String name = feature.getName();
if (featureTable.containsKey(name))
return false;
featureTable.put(name, feature);
return true;
}
public void deleteFeatureFromTable(Feature feature) {
featureTable.remove(feature.getName());
}
public boolean deleteFeature(Feature feature) {
// the root can not be deleted
if (feature == root)
return false;
// check if it exists
String name = feature.getName();
if (!featureTable.containsKey(name))
return false;
// add children to parent
Feature parent = feature.getParent();
int index = parent.getChildIndex(feature);
while (feature.hasChildren())
parent.addChildAtPosition(index, feature.removeLastChild());
// delete feature
boolean deleteParent = parent.isAbstract()
&& parent.getChildrenCount() == 1;
parent.removeChild(feature);
featureTable.remove(name);
// delete parent if it has no children
if (deleteParent)
return deleteFeature(parent);
return true;
}
public Feature getFeature(String name) {
if (featureTable.isEmpty()) {
// create the root feature (it is the only one without a reference)
root = new Feature(this, name);
addFeature(root);
return root;
}
return featureTable.get(name);
}
public boolean renameFeature(String oldName, String newName) {
if (!featureTable.containsKey(oldName)
|| featureTable.containsKey(newName))
return false;
Feature feature = featureTable.remove(oldName);
feature.setName(newName);
featureTable.put(newName, feature);
renamings.add(new Renaming(oldName, newName));
return true;
}
public void performRenamings() {
for (Renaming renaming : renamings) {
for (Node node : propNodes)
renameVariables(node, renaming.oldName, renaming.newName);
fireFeatureRenamed(renaming.oldName, renaming.newName);
}
renamings.clear();
}
/**
* informs listners that a feature has been renamed
*/
private void fireFeatureRenamed(String oldName, String newName) {
PropertyChangeEvent event = new PropertyChangeEvent(this,
FEATURE_NAME_CHANGED, oldName, newName);
for (PropertyChangeListener listener : listenerList)
listener.propertyChange(event);
}
private void renameVariables(Node node, String oldName, String newName) {
if (node instanceof Literal) {
if (oldName.equals(((Literal) node).var))
((Literal) node).var = newName;
return;
}
for (Node child : node.getChildren())
renameVariables(child, oldName, newName);
}
public boolean containsLayer(String featureName) {
Feature feature = featureTable.get(featureName);
return feature != null && feature.isLayer();
}
private final LinkedList<PropertyChangeListener> listenerList = new LinkedList<PropertyChangeListener>();
public void addListener(PropertyChangeListener listener) {
if (!listenerList.contains(listener))
listenerList.add(listener);
}
public void removeListener(PropertyChangeListener listener) {
listenerList.remove(listener);
}
public void handleModelDataLoaded() {
PropertyChangeEvent event = new PropertyChangeEvent(this,
MODEL_DATA_LOADED, false, true);
for (PropertyChangeListener listener : listenerList)
listener.propertyChange(event);
}
public void handleModelDataChanged() {
PropertyChangeEvent event = new PropertyChangeEvent(this,
MODEL_DATA_CHANGED, false, true);
for (PropertyChangeListener listener : listenerList)
listener.propertyChange(event);
}
public Collection<Feature> getFeatures() {
return Collections.unmodifiableCollection(featureTable.values());
}
public void createDefaultValues() {
Feature root = getFeature("Root");
Feature feature = new Feature(this, "Base");
root.addChild(feature);
addFeature(feature);
}
public void replaceRoot(Feature feature) {
featureTable.remove(root.getName());
root = feature;
}
/**
* Returns the current name of a feature given its name at the last save.
*
* @param name
* name when last saved
* @return current name of this feature
*/
public String getNewName(String name) {
for (Renaming renaming : renamings)
if (renaming.oldName.equals(name))
name = renaming.newName;
return name;
}
/**
* Returns the name of a feature at the time of the last save given its
* current name.
*
* @param name
* current name of a feature
* @return name when last saved
*/
public String getOldName(String name) {
for (int i = renamings.size() - 1; i >= 0; i--)
if (renamings.get(i).newName.equals(name))
name = renamings.get(i).oldName;
return name;
}
public Set<String> getFeatureNames() {
return Collections.unmodifiableSet(featureTable.keySet());
}
public Set<String> getOldFeatureNames() {
Set<String> names = new HashSet<String>(featureTable.keySet());
for (int i = renamings.size() - 1; i >= 0; i--) {
Renaming renaming = renamings.get(i);
names.remove(renaming.newName);
names.add(renaming.oldName);
}
return Collections.unmodifiableSet(names);
}
public Node getConstraint(int index) {
return propNodes.get(index);
}
public int getConstraintCount() {
return constraints.size();
}
public List<Constraint> getConstraints() {
return Collections.unmodifiableList(constraints);
}
public int getNumberOfFeatures() {
return featureTable.size();
}
@Override
public FeatureModel clone() {
FeatureModel fm = new FeatureModel();
fm.root = root.clone();
List<Feature> list = new LinkedList<Feature>();
list.add(fm.root);
while (!list.isEmpty()) {
Feature feature = list.remove(0);
fm.featureTable.put(feature.getName(), feature);
for (Feature child : feature.getChildren())
list.add(child);
}
fm.propNodes = new LinkedList<Node>();
for (Node node : propNodes)
fm.propNodes.add(node);
for (int i = 0; i < propNodes.size(); i++)
fm.constraints.add(new Constraint(fm, fm.constraints.size()));
fm.annotations = annotations;
fm.comments = comments;
return fm;
}
public boolean isValid() throws TimeoutException {
Node root = NodeCreator.createNodes(this);
return new SatSolver(root, 1000).isSatisfiable();
}
public void hasAbstractFeatures(boolean abstractFeatures) {
this.abstractFeatures = abstractFeatures;
}
public boolean hasAbstractFeatures() {
return abstractFeatures;
}
/**
* checks whether A implies B for the current feature model.
*
* in detail the following condition should be checked whether
*
* FM => ((A1 and A2 and ... and An) => (B1 and B2 and ... and Bn))
*
* is true for all values
*
* @param A
* set of features that form a conjunction
* @param B
* set of features that form a conjunction
* @return
* @throws TimeoutException
*/
public boolean checkImplies(Set<Feature> a, Set<Feature> b)
throws TimeoutException {
if (b.isEmpty())
return true;
Node featureModel = NodeCreator.createNodes(this);
// B1 and B2 and ... Bn
Node condition = conjunct(b);
// (A1 and ... An) => (B1 and ... Bn)
if (!a.isEmpty())
condition = new Implies(conjunct(a), condition);
// FM => (A => B)
Implies finalFormula = new Implies(featureModel, condition);
// System.out.println(finalFormula.toString());
return !new SatSolver(new Not(finalFormula), 1000).isSatisfiable();
}
/**
* checks some condition against the feature model. use only if you know
* what you are doing!
*
* @return
* @throws TimeoutException
*/
public boolean checkCondition(Node condition){
Node featureModel = NodeCreator.createNodes(this);
// FM => (condition)
Implies finalFormula = new Implies(featureModel, condition.clone());
// System.out.println(finalFormula.toString());
try {
return !new SatSolver(new Not(finalFormula), 1000).isSatisfiable();
} catch (TimeoutException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return false;
}
}
/**
* Checks whether the given featureSets are mutually exclusive in the given
* context and for the current feature model.
*
* In detail it is checked whether FM => (context => (at most one of the
* featureSets are present)) is a tautology.
*
* Here is an example for a truth table of
* "at most one the featureSets are present" for three feature sets A, B and
* C:
*
* A B C result ------------------------ T T T F T T F F T F T F T F F T F T
* T F F T F T F F T T F F F T
*
* If you want to check XOR(featureSet_1, ..., featureSet_n) you can call
* areMutualExclusive() && !mayBeMissing().
*
* @param context
* context in which everything is checked
* @param featureSets
* list of feature sets that are checked to be mutually exclusive
* in the given context and for the current feature model
*
* @return true, if the feature sets are mutually exclusive || false,
* otherwise
* @throws TimeoutException
*/
public boolean areMutualExclusive(Set<Feature> context,
List<Set<Feature>> featureSets) throws TimeoutException {
if ((featureSets == null) || (featureSets.size() < 2))
return true;
Node featureModel = NodeCreator.createNodes(this);
ArrayList<Node> conjunctions = new ArrayList<Node>(featureSets.size());
for (Set<Feature> features : featureSets) {
if ((features != null) && !features.isEmpty())
conjunctions.add(conjunct(features));
else
// If one feature set is empty (i.e. the code-fragment is always
// present) than it cannot be
// mutually exclusive to the other ones.
return false;
}
// We build the conjunctive normal form of the formula to check
LinkedList<Object> forOr = new LinkedList<Object>();
LinkedList<Object> allNot = new LinkedList<Object>();
for (int i = 0; i < conjunctions.size(); ++i) {
allNot.add(new Not(conjunctions.get(i).clone()));
LinkedList<Object> forAnd = new LinkedList<Object>();
for (int j = 0; j < conjunctions.size(); ++j) {
if (j == i)
forAnd.add(conjunctions.get(j).clone());
else
forAnd.add(new Not(conjunctions.get(j).clone()));
}
forOr.add(new And(forAnd));
}
forOr.add(new And(allNot));
Node condition = new Or(forOr);
if ((context != null) && !context.isEmpty())
condition = new Implies(conjunct(context), condition);
Implies finalFormula = new Implies(featureModel, condition);
return !new SatSolver(new Not(finalFormula), 1000).isSatisfiable();
}
/**
* Checks whether there exists a set of features that is valid within the
* feature model and the given context, so that none of the given feature
* sets are present, i.e. evaluate to true.
*
* In detail it is checked whether there exists a set F of features so that
* eval(FM, F) AND eval(context, F) AND NOT(eval(featureSet_1, F)) AND ...
* AND NOT(eval(featureSet_n, F)) is true.
*
* If you want to check XOR(featureSet_1, ..., featureSet_n) you can call
* areMutualExclusive() && !mayBeMissing().
*
* @param context
* context in which everything is checked
* @param featureSets
* list of feature sets
*
* @return true, if there exists such a set of features, i.e. if the
* code-fragment may be missing || false, otherwise
* @throws TimeoutException
*/
public boolean mayBeMissing(Set<Feature> context,
List<Set<Feature>> featureSets) throws TimeoutException {
if ((featureSets == null) || featureSets.isEmpty())
return false;
Node featureModel = NodeCreator.createNodes(this);
LinkedList<Object> forAnd = new LinkedList<Object>();
for (Set<Feature> features : featureSets) {
if ((features != null) && !features.isEmpty())
forAnd.add(new Not(conjunct(features)));
else
return false;
}
Node condition = new And(forAnd);
if ((context != null) && !context.isEmpty())
condition = new And(conjunct(context), condition);
Node finalFormula = new And(featureModel, condition);
return new SatSolver(finalFormula, 1000).isSatisfiable();
}
/**
* Checks whether there exists a set of features that is valid within the
* feature model, so that all given features are present.
*
* In detail it is checked whether there exists a set F of features so that
* eval(FM, F) AND eval(feature_1, F) AND eval(feature_n, F) is true.
*
* @param features
*
* @return true if there exists such a set of features || false, otherwise
* @throws TimeoutException
*/
public boolean exists(Set<Feature> features) throws TimeoutException {
if ((features == null) || (features.isEmpty()))
return true;
Node featureModel = NodeCreator.createNodes(this);
Node finalFormula = new And(featureModel, conjunct(features));
return new SatSolver(finalFormula, 1000).isSatisfiable();
}
private Node conjunct(Set<Feature> b) {
Iterator<Feature> iterator = b.iterator();
Node result = new Literal(NodeCreator
.getVariable(iterator.next(), this));
while (iterator.hasNext())
result = new And(result, new Literal(NodeCreator.getVariable(
iterator.next(), this)));
return result;
}
public int countConcreteFeatures() {
int number = 0;
for (Feature feature : getFeatures())
if (feature.isConcrete())
number++;
return number;
}
public int countTerminalFeatures() {
int number = 0;
for (Feature feature : getFeatures())
if (!feature.hasChildren())
number++;
return number;
}
}