/* 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.io.waterloo;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.Scanner;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.prop4j.And;
import org.prop4j.Equals;
import org.prop4j.Implies;
import org.prop4j.Literal;
import org.prop4j.Not;
import org.prop4j.Or;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import featureide.fm.core.Feature;
import featureide.fm.core.FeatureModel;
import featureide.fm.core.io.AbstractFeatureModelReader;
import featureide.fm.core.io.UnsupportedModelException;
/**
* TODO description
*
* @author Fabian Wielgorz
*/
public class WaterlooReader extends AbstractFeatureModelReader {
private int line;
private Hashtable<String, Feature> idTable;
/**
* Creates a new reader and sets the feature model to store the data.
*
* @param featureModel the structure to fill
*/
public WaterlooReader(FeatureModel featureModel) {
setFeatureModel(featureModel);
}
@Override
protected void parseInputStream(InputStream inputStream)
throws UnsupportedModelException {
warnings.clear();
//Parse the XML-File to a DOM-Document
boolean ignoreWhitespace = false;
boolean ignoreComments = true;
boolean putCDATAIntoText = true;
boolean createEntityRefs = false;
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
dbf.setIgnoringComments(ignoreComments);
dbf.setIgnoringElementContentWhitespace(ignoreWhitespace);
dbf.setCoalescing(putCDATAIntoText);
dbf.setExpandEntityReferences(!createEntityRefs);
DocumentBuilder db = null;
try {
db = dbf.newDocumentBuilder();
} catch (ParserConfigurationException pce) {
System.err.println(pce);
System.exit(1);
}
Document doc = null;
try {
doc = db.parse(inputStream);
} catch (SAXException se) {
System.err.println(se.getMessage());
System.exit(1);
} catch (IOException ioe) {
System.err.println(ioe);
System.exit(1);
}
featureModel.reset();
featureModel.hasAbstractFeatures(false);
line = 0;
idTable = new Hashtable<String, Feature>();
// Create the Feature Model from the DOM-Document
buildFModelRec(doc);
}
/**
* Recursively traverses the Document structure
* @param n
* @throws UnsupportedModelException
*/
private void buildFModelRec(Node n) throws UnsupportedModelException {
buildFModelStep(n);
for (Node child = n.getFirstChild(); child != null;
child = child.getNextSibling()) {
buildFModelRec(child);
}
}
/**
* Processes a single Xml-Tag.
* @param n
* @throws UnsupportedModelException
*/
private void buildFModelStep(Node n) throws UnsupportedModelException {
if (n.getNodeType() != Node.ELEMENT_NODE) return;
String tag = n.getNodeName();
if (tag.equals("feature_tree")) {
handleFeatureTree(n);
} else if (tag.equals("feature_model")) {
line++;
return;
} else if (tag.equals("constraints")) {
line++;
handleConstraints(n);
} else {
throw new UnsupportedModelException("Unknown Xml-Tag", line);
}
}
/**
* Reads the input in the feature tree section, interprets the input line
* by line by using buildFeatureTree
* @param n
* @throws UnsupportedModelException
*/
private void handleFeatureTree(Node n) throws UnsupportedModelException {
NodeList children = n.getChildNodes();
StringBuffer buffer = new StringBuffer();
Node node;
for (int i = 0; i < children.getLength(); i++) {
node = children.item(i);
if (node.getNodeType() == Node.TEXT_NODE) {
buffer.append(node.getNodeValue());
}
}
BufferedReader reader = new BufferedReader(new StringReader(
buffer.toString()));
buildFeatureTree(reader);
}
/**
* Reads one line of the input Text and builds the corresponding feature
* @param reader
* @param lastFeat
* @throws UnsupportedModelException
*/
private void buildFeatureTree(BufferedReader reader) throws UnsupportedModelException {
try {
FeatureIndent lastFeat = new FeatureIndent(null, -1);
// List of Features with arbitrary cardinalities
LinkedList<FeatCardinality> arbCardGroupFeats = new LinkedList<FeatCardinality>();
String lineText = reader.readLine();
line++;
FeatureIndent feat;
String featId;
while (lineText != null) {
int countIndent = 0;
if (lineText.trim().equals("")) {
lineText = reader.readLine();
line++;
continue;
}
while (lineText.startsWith("\t")) {
countIndent++;
lineText = lineText.substring(1);
}
int relativeIndent = countIndent - lastFeat.getIndentation();
while (relativeIndent < 1) {
if (lastFeat.isRoot()) throw new UnsupportedModelException(
"Indentation error, feature has no parent", line);
lastFeat = (FeatureIndent) lastFeat.getParent();
relativeIndent = countIndent - lastFeat.getIndentation();
}
// Remove special characters and whitespaces from names
char[] lineTextChars = lineText.toCharArray();
for (int i = 0; i < lineTextChars.length; i++) {
Character c = lineTextChars[i];
if (!(Character.isLetterOrDigit(c) || c.equals(':')
|| c.equals('[') || c.equals(']') || c.equals(',')
|| c.equals('*') || c.equals('(') || c.equals(')'))) {
lineTextChars[i] = '_';
}
}
lineText = new String(lineTextChars);
if (lineText.startsWith(":r")) {
feat = new FeatureIndent(featureModel, 0);
feat.setMandatory(true);
featId = setNameGetID(feat, lineText);
featureModel.setRoot(feat);
feat.changeToAnd();
countIndent = 0;
} else if (lineText.startsWith(":m")) {
feat = new FeatureIndent(featureModel, countIndent);
feat.setMandatory(true);
featId = setNameGetID(feat, lineText);
feat.setParent(lastFeat);
lastFeat.addChild(feat);
feat.changeToAnd();
} else if (lineText.startsWith(":o")) {
feat = new FeatureIndent(featureModel, countIndent);
feat.setMandatory(false);
featId = setNameGetID(feat, lineText);
feat.setParent(lastFeat);
lastFeat.addChild(feat);
feat.changeToAnd();
} else if (lineText.startsWith(":g")) {
if (lineText.contains("(")) {
feat = new FeatureIndent(featureModel, countIndent);
feat.setMandatory(false);
featId = setNameGetID(feat, lineText);
feat.setParent(lastFeat);
lastFeat.addChild(feat);
feat.changeToAnd();
} else {
feat = new FeatureIndent(featureModel, countIndent);
feat.setMandatory(true);
featId = "sg_" + lastFeat.getName() + line;
feat.setName(featId);
feat.setParent(lastFeat);
lastFeat.addChild(feat);
feat.changeToAnd();
}
if (lineText.contains("[1,1]")) {
feat.changeToAlternative();
} else if (lineText.contains("[1,*]")) {
feat.changeToOr();
} else if ((lineText.contains("[")) && (lineText.contains("]"))) {
int index = lineText.indexOf('[');
int start = Character.getNumericValue(lineText.charAt(index + 1));
int end = Character.getNumericValue(lineText.charAt(index + 3));
FeatCardinality featCard = new FeatCardinality(feat, start, end);
arbCardGroupFeats.add(featCard);
} else throw new UnsupportedModelException("Couldn't " +
"determine group cardinality", line);
} else if (lineText.startsWith(":")) {
feat = new FeatureIndent(featureModel, countIndent);
feat.setMandatory(true);
String name;
if (lineText.contains("(")) {
name = lineText.substring(2, lineText.indexOf('(') - 1);
featId = lineText.substring(lineText.indexOf('(') + 1,
lineText.indexOf(')'));
} else {
name = lineText.substring(2, lineText.length()) + line;
featId = name;
}
if (Character.isDigit(name.charAt(0))) name = "a" + name;
feat.setName(name);
feat.setParent(lastFeat);
lastFeat.addChild(feat);
feat.changeToAnd();
} else throw new UnsupportedModelException("Couldn't match with " +
"known Types: :r, :m, :o, :g, :", line);
if (!featureModel.addFeature(feat)) {
feat.setName(featId);
featureModel.addFeature(feat);
}
if (idTable.containsKey(featId)) throw
new UnsupportedModelException("Id: " + featId + " occured" +
" second time, but may only occur once", line);
idTable.put(featId, feat);
lastFeat = feat;
lineText = reader.readLine();
line++;
}
handleArbitrayCardinality(arbCardGroupFeats);
} catch (IOException e) {
e.printStackTrace();
}
}
private String setNameGetID (Feature feat, String lineText) {
String featId, name;
if (lineText.contains("(")) {
name = lineText.substring(3, lineText.indexOf('(') - 1);
featId = lineText.substring(lineText.indexOf('(') + 1,
lineText.indexOf(')'));
} else {
name = lineText.substring(3, lineText.length()) + line;
featId = name;
}
if (Character.isDigit(name.charAt(0))) name = "a" + name;
feat.setName(name);
return featId;
}
/**
* If there are groups with a cardinality other then [1,*] or [1,1], this
* function makes the necessary adjustments to the model
* @param featList List of features with arbitrary cardinalities
* @throws UnsupportedModelException
*/
private void handleArbitrayCardinality(LinkedList<FeatCardinality> featList)
throws UnsupportedModelException {
org.prop4j.Node node;
for (FeatCardinality featCard : featList) {
Feature feat = featCard.feat;
node = new And();
LinkedList<Feature> children = feat.getChildren();
for (Feature child : children) child.setMandatory(false);
int start = featCard.start;
int end = featCard.end;
if ((start < 0) || (start > end) || (end > children.size()))
throw new UnsupportedModelException("Group cardinality " +
"invalid", line);
int f = children.size();
node = buildMinConstr(children, f - start + 1, feat.getName());
featureModel.addPropositionalNode(node);
if ((start > 0) && (end < f)) {
node = buildMaxConstr(children, end + 1);
featureModel.addPropositionalNode(node);
}
}
}
/**
* Builds the propositional constraint, denoting a minimum of features has
* to be selected
* @param list
* @param length
* @param parentName
* @return
*/
private org.prop4j.Node buildMinConstr (LinkedList<Feature> list, int length,
String parentName) {
LinkedList<org.prop4j.Node> result = new LinkedList<org.prop4j.Node>();
LinkedList<org.prop4j.Node> partResult = new LinkedList<org.prop4j.Node>();
int listLength = list.size();
int[] indexes = new int[length];
int[] resIndexes = new int[length];
for (int i = 0; i < length; i++) {
indexes[i] = i;
resIndexes[i] = i + (listLength - length);
}
while (!Arrays.equals(indexes, resIndexes)) {
partResult.add(new Literal(parentName, false));
for (int i = 0; i < length; i++) {
partResult.add(new Literal(list.get(indexes[i]).getName()));
}
result.add(new Or(partResult));
for (int i = length - 1; i >= 0; i--) {
if (indexes[i] >= resIndexes[i]) {
continue;
}
indexes[i] = indexes[i] + 1;
for (int j = i + 1; j < length; j++) {
indexes[j] = indexes[j-1] + 1;
}
break;
}
}
partResult.add(new Literal(parentName, false));
for (int i = 0; i < length; i++) {
partResult.add(new Literal(list.get(indexes[i]).getName()));
}
result.add(new Or(partResult));
return new And(result);
}
/**
* Builds the propositional constraint, denoting a maximum of features can
* be selected
* @param list
* @param length
* @return
*/
private org.prop4j.Node buildMaxConstr (LinkedList<Feature> list, int length) {
LinkedList<org.prop4j.Node> result = new LinkedList<org.prop4j.Node>();
LinkedList<org.prop4j.Node> partResult = new LinkedList<org.prop4j.Node>();
int listLength = list.size();
int[] indexes = new int[length];
int[] resIndexes = new int[length];
for (int i = 0; i < length; i++) {
indexes[i] = i;
resIndexes[i] = i + (listLength - length);
}
while (!Arrays.equals(indexes, resIndexes)) {
for (int i = 0; i < length; i++) {
partResult.add(new Literal(list.get(indexes[i]).getName(),
false));
}
result.add(new Or(partResult));
for (int i = length - 1; i >= 0; i--) {
if (indexes[i] >= resIndexes[i]) {
continue;
}
indexes[i] = indexes[i] + 1;
for (int j = i + 1; j < length; j++) {
indexes[j] = indexes[j-1] + 1;
}
break;
}
}
for (int i = 0; i < length; i++)
partResult.add(new Literal(list.get(indexes[i]).getName(), false));
result.add(new Or(partResult));
return new And(result);
}
/**
* Handles the constraints found in the 'constraints' xml-tag
* @param n
* @throws UnsupportedModelException
*/
private void handleConstraints(Node n) throws UnsupportedModelException {
NodeList children = n.getChildNodes();
StringBuffer buffer = new StringBuffer();
String lineText;
Node node;
for (int i = 0; i < children.getLength(); i++) {
node = children.item(i);
if (node.getNodeType() == Node.TEXT_NODE) {
buffer.append(node.getNodeValue());
}
}
BufferedReader reader = new BufferedReader(new StringReader(
buffer.toString()));
try {
lineText = reader.readLine();
line++;
while (lineText != null) {
if (!lineText.trim().equals("")) {
handleSingleConstraint(lineText);
}
lineText = reader.readLine();
line++;
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Handles a single constraints.
* @param lineText Text description of a Constraint
* @throws UnsupportedModelException
*/
private void handleSingleConstraint(String lineText)
throws UnsupportedModelException {
String newLine = lineText.replace("(", " ( ");
newLine = newLine.replace(")", " ) ");
newLine = newLine.replace("~", " ~ ");
Scanner scan = new Scanner(newLine);
scan.skip(".*:");
LinkedList<String> elements = new LinkedList<String>();
while (scan.hasNext()) {
elements.add(scan.next());
}
org.prop4j.Node propNode = buildPropNode(elements);
featureModel.addPropositionalNode(propNode);
}
/**
* Builds a Propositional Node from a propositional formula
* @param list
* @return
* @throws UnsupportedModelException
*/
private org.prop4j.Node buildPropNode(LinkedList<String> list)
throws UnsupportedModelException {
LinkedList<String> left = new LinkedList<String>();
org.prop4j.Node leftResult, rightResult;
int bracketCount = 0;
String element;
while (!list.isEmpty()) {
element = list.removeFirst();
if (element.equals("(")) bracketCount++;
if (element.equals(")")) bracketCount--;
if ((element.equals("~")) && (list.getFirst().equals("("))
&& (list.getLast().equals(")"))) {
list.removeFirst();
list.removeLast();
return new Not(buildPropNode(list));
}
if (element.equals("AND")) element = "and";
if (element.equals("OR")) element = "or";
if (element.equals("IMP")) element = "imp";
if (element.equals("BIIMP")) element = "biimp";
if ((element.equals("and")) || (element.equals("or")) ||
(element.equals("imp")) || (element.equals("biimp"))) {
if (bracketCount == 0) {
if ((left.getFirst().equals("(")) &&
(left.getLast().equals(")"))) {
left.removeFirst();
left.removeLast();
}
leftResult = buildPropNode(left);
if ((list.getFirst().equals("(")) &&
(list.getLast().equals(")"))) {
list.removeFirst();
list.removeLast();
}
rightResult = buildPropNode(list);
if (element.equals("and"))
return new And(leftResult, rightResult);
if (element.equals("or"))
return new Or(leftResult, rightResult);
if (element.equals("imp"))
return new Implies(leftResult, rightResult);
if (element.equals("biimp"))
return new Equals(leftResult, rightResult);
}
}
left.add(element);
}
return buildLeafNodes(left);
}
private org.prop4j.Node buildLeafNodes(LinkedList<String> list)
throws UnsupportedModelException {
String element;
if (list.isEmpty())
throw new UnsupportedModelException("Fehlendes Element", line);
element = list.removeFirst();
if ((element.equals("(")) && (!list.isEmpty()))
element = list.removeFirst();
if (element.equals("~")) {
return new Not(buildPropNode(list));
} else {
Feature feat = idTable.get(element);
if (feat == null)
throw new UnsupportedModelException("The feature '" + element +
"' does not occur in the grammar!", 0);
return new Literal(feat.getName());
}
}
private class FeatureIndent extends Feature {
private int indentation = 0;
public FeatureIndent(FeatureModel featureModel) {
super(featureModel);
}
public FeatureIndent(FeatureModel featureModel, int indent) {
super(featureModel);
indentation = indent;
}
public int getIndentation() {
return indentation;
}
public void setIndentation(int indentation) {
this.indentation = indentation;
}
}
private class FeatCardinality {
Feature feat;
int start;
int end;
FeatCardinality (Feature feat, int start, int end) {
this.feat = feat;
this.start = start;
this.end = end;
}
}
}