package dasherJava.core.alphabets.xml;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Stack;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import dasherJava.core.alphabets.actions.Action.AccessibilityAction;
import dasherJava.core.alphabets.actions.Action.ChangeAlphabetAction;
import dasherJava.core.alphabets.actions.Action.DeleteTextAction;
import dasherJava.core.alphabets.actions.Action.KeyboardAction;
import dasherJava.core.alphabets.actions.Action.MoveTextCaretAction;
import dasherJava.core.alphabets.actions.Action.PauseDasherAction;
import dasherJava.core.alphabets.actions.Action.SocketOutputAction;
import dasherJava.core.alphabets.actions.Action.TextCharAction;
import dasherJava.core.output.TextCharOutput.TextRange;
import dasherJava.core.output.TextCharOutput.TextTarget;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
public class AlphabetFileParser extends DefaultHandler {
private final Stack<Group> currentGroups = new Stack<>();
private Alphabet alphabet;
private boolean inNode = false; //only needed for error checking
public AlphabetFileParser(String filename) throws ParserConfigurationException, SAXException, IOException {
SAXParserFactory.newInstance().newSAXParser().parse(filename, this);
}
public Alphabet getAlphabet() {
return alphabet;
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
switch (qName) {
case "alphabet":
alphabet=createAlphabetFromAttributes(attributes);
break;
case "group": {
if (alphabet==null) throw new SAXException("First element must be <alphabet>");
Group g = createGroupFromAttributes(attributes, alphabet.getNumberOfNodes());
currentGroups.push(g);
break;
}
case "node":
if (alphabet==null) throw new SAXException("First element must be <alphabet>");
if (currentGroups.isEmpty()) throw new SAXException("<node> must be inside of <group>");
if (inNode) throw new SAXException("<node> inside of another <node> is invalid");
alphabet.addNode(createNodeFromAttributes(attributes, currentGroups.peek()));
inNode=true;
break;
case "textCharAction": {
if (alphabet==null) throw new SAXException("First element must be <alphabet>");
if (!inNode) throw new SAXException("<textCharAction> must be inside of <node>");
Node node = alphabet.getLastNode();
node.addAction(createTextCharActionFromAttributes(attributes, node.getLabel()));
break;
}
case "deleteTextAction":
if (alphabet==null) throw new SAXException("First element must be <alphabet>");
if (!inNode) throw new SAXException("<deleteTextAction> must be inside of <node>");
alphabet.getLastNode().addAction(createDeleteTextActionFromAttributes(attributes));
break;
case "moveTextCaretAction":
if (alphabet==null) throw new SAXException("First element must be <alphabet>");
if (!inNode) throw new SAXException("<moveTextCaretAction> must be inside of <node>");
alphabet.getLastNode().addAction(createMoveTextCaretActionFromAttributes(attributes));
break;
case "pauseDasherAction":
if (alphabet==null) throw new SAXException("First element must be <alphabet>");
if (!inNode) throw new SAXException("<pauseDasherAction> must be inside of <node>");
alphabet.getLastNode().addAction(createPauseDasherActionFromAttributes(attributes));
break;
case "changeAlphabetAction":
if (alphabet==null) throw new SAXException("First element must be <alphabet>");
if (!inNode) throw new SAXException("<changeAlphabetAction> must be inside of <node>");
alphabet.getLastNode().addAction(createChangeAlphabetActionFromAttributes(attributes));
break;
case "keyboardAction":
if (alphabet==null) throw new SAXException("First element must be <alphabet>");
if (!inNode) throw new SAXException("<keyboardAction> must be inside of <node>");
alphabet.getLastNode().addAction(createKeyboardActionFromAttributes(attributes));
break;
case "accessibilityAction":
if (alphabet==null) throw new SAXException("First element must be <alphabet>");
if (!inNode) throw new SAXException("<accessibilityAction> must be inside of <node>");
alphabet.getLastNode().addAction(createAccessibilityActionFromAttributes(attributes));
break;
case "socketOutputAction":
if (alphabet==null) throw new SAXException("First element must be <alphabet>");
if (!inNode) throw new SAXException("<socketOutputAction> must be inside of <node>");
alphabet.getLastNode().addAction(createSocketOutputActionFromAttributes(attributes));
break;
default:
throw new SAXException("Unknown element: <"+qName+">");
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
switch (qName) {
case "group": {
//Don't need to check if the stack is empty here because the SAX parser already ensures
//that there are no closing tags for elements that haven't been opened before.
Group g = currentGroups.pop();
g.setEndIndex(alphabet.getNumberOfNodes()-1);
alphabet.addGroup(g);
break;
}
case "node":
inNode=false;
if (alphabet.getLastNode().getFixedProbability()<0.0f && alphabet.getLastNode().getTrainingUnicode()<0)
throw new SAXException("Cannot infer Unicode value for node without fixed probability: No "
+"<textCharAction> with valid Unicode value present, and the node label cannot be "
+"used either since it doesn't consist of exactly one character");
break;
case "alphabet":
case "textCharAction":
case "deleteTextAction":
case "moveTextCaretAction":
case "pauseDasherAction":
case "changeAlphabetAction":
case "keyboardAction":
case "accessibilityAction":
case "socketOutputAction":
break;
default: //can never happen because startElement() already checks for unknown elements
throw new SAXException("Unknown element: <"+qName+">");
}
}
//Note: Many of the known attributes listed below are not used yet, but have been included in the file format
//specification because they are needed in the old Dasher implementation. As more features are added to this
//new implementation, more and more of these attributes will be required.
private static Alphabet createAlphabetFromAttributes(Attributes attributes) throws SAXException {
checkForUnknownAttributes(attributes, "alphabet", "name", "orientation", "conversionMode",
"trainingFilename", "colorsName");
String name = attributes.getValue("name");
if (name==null || name.isEmpty()) throw new SAXException("Non-empty \"name\" "
+"attribute is required for <alphabet> element");
String orientation = attributes.getValue("orientation");
if (orientation==null || orientation.isEmpty()) throw new SAXException("Non-empty \"orientation\" "
+"attribute is required for <alphabet> element");
String trainingFilename = attributes.getValue("trainingFilename");
String colorsName = attributes.getValue("colorsName");
if (colorsName==null || colorsName.isEmpty()) throw new SAXException("Non-empty \"colorsName\" "
+"attribute is required for <alphabet> element");
return new Alphabet(name, orientation, trainingFilename, colorsName);
}
private static Group createGroupFromAttributes(Attributes attributes, int startIndex)
throws SAXException {
checkForUnknownAttributes(attributes, "group", "name", "label", "colorInfoName", "speedFactor");
String label = attributes.getValue("label");
String colorInfoName = attributes.getValue("colorInfoName");
if (colorInfoName==null || colorInfoName.isEmpty()) throw new SAXException("Non-empty \"colorInfoName\" "
+"attribute is required for <group> element");
float speedFactor = parseFloat(attributes.getValue("speedFactor"));
if (speedFactor<0.0f) speedFactor=1.0f; //if unspecified, use factor 1.0
return new Group(label, colorInfoName, speedFactor, startIndex);
}
private static Node createNodeFromAttributes(Attributes attributes, Group group) throws SAXException {
checkForUnknownAttributes(attributes, "node", "label", "trainingUnicode", "fixedProbability");
String label = attributes.getValue("label");
int trainingUnicode = parseInt(attributes.getValue("trainingUnicode"));
float fixedProbability = parseFloat(attributes.getValue("fixedProbability"));
return new Node(label, trainingUnicode, fixedProbability, group);
}
private static TextCharAction createTextCharActionFromAttributes(Attributes attributes, String nodeLabel)
throws SAXException {
checkForUnknownAttributes(attributes, "textCharAction", "unicode");
int unicode = parseInt(attributes.getValue("unicode"));
if (unicode<0) { //if unspecified, infer from the node label if it consists of exactly one Unicode character
if (nodeLabel!=null && nodeLabel.length()==1) return new TextCharAction(nodeLabel.codePointAt(0));
throw new SAXException("textCharAction: Cannot infer Unicode from node label (must be exactly "
+"one character)");
}
return new TextCharAction(unicode);
}
private static DeleteTextAction createDeleteTextActionFromAttributes(Attributes attributes)
throws SAXException {
checkForUnknownAttributes(attributes, "deleteTextAction", "range");
String rangeString = attributes.getValue("range");
if (rangeString==null || rangeString.isEmpty()) throw new SAXException("Non-empty \"range\" "
+"attribute is required for <deleteTextAction> element");
TextRange range = parseTextRange(rangeString);
return new DeleteTextAction(range);
}
private static MoveTextCaretAction createMoveTextCaretActionFromAttributes(Attributes attributes)
throws SAXException {
checkForUnknownAttributes(attributes, "moveTextCaretAction", "target");
String targetString = attributes.getValue("target");
if (targetString==null || targetString.isEmpty()) throw new SAXException("Non-empty \"target\" "
+"attribute is required for <moveTextCaretAction> element");
TextTarget target = parseTextTarget(targetString);
return new MoveTextCaretAction(target);
}
private static PauseDasherAction createPauseDasherActionFromAttributes(Attributes attributes)
throws SAXException {
checkForUnknownAttributes(attributes, "pauseDasherAction", "time");
int time = parseInt(attributes.getValue("time")); //negative if unspecified
return new PauseDasherAction(time);
}
private static ChangeAlphabetAction createChangeAlphabetActionFromAttributes(Attributes attributes)
throws SAXException {
checkForUnknownAttributes(attributes, "changeAlphabetAction", "alphabetName");
String alphabetName = attributes.getValue("alphabetName");
if (alphabetName==null || alphabetName.isEmpty()) throw new SAXException("Non-empty \"alphabetName\" "
+"attribute is required for <changeAlphabetAction> element");
return new ChangeAlphabetAction(alphabetName);
}
private static KeyboardAction createKeyboardActionFromAttributes(Attributes attributes)
throws SAXException {
checkForUnknownAttributes(attributes, "keyboardAction", "press", "key", "release", "undoPress",
"undoKey", "undoRelease");
int press = parseInt(attributes.getValue("press")); //negative if unspecified
int key = parseInt(attributes.getValue("key")); //negative if unspecified
int release = parseInt(attributes.getValue("release")); //negative if unspecified
int undoPress = parseInt(attributes.getValue("undoPress")); //negative if unspecified
int undoKey = parseInt(attributes.getValue("undoKey")); //negative if unspecified
int undoRelease = parseInt(attributes.getValue("undoRelease")); //negative if unspecified
return new KeyboardAction(press, key, release, undoPress, undoKey, undoRelease);
}
private static AccessibilityAction createAccessibilityActionFromAttributes(Attributes attributes)
throws SAXException {
checkForUnknownAttributes(attributes, "accessibilityAction", "doAction", "undoAction");
String doAction = attributes.getValue("doAction");
String undoAction = attributes.getValue("undoAction");
return new AccessibilityAction(doAction, undoAction);
}
private static SocketOutputAction createSocketOutputActionFromAttributes(Attributes attributes)
throws SAXException {
checkForUnknownAttributes(attributes, "socketOutputAction", "doString", "undoString", "suppressNewline");
String doString = attributes.getValue("doString");
String undoString = attributes.getValue("undoString");
boolean suppressNewline = Boolean.parseBoolean(attributes.getValue("suppressNewline"));
return new SocketOutputAction(doString, undoString, suppressNewline);
}
private static int parseInt(String s) throws SAXException { //either hex or dec
if (s==null) return -1; //negative means "unspecified"
try {
return s.startsWith("x") || s.startsWith("X") ? Integer.parseInt(s, 1, s.length(), 16)
: Integer.parseInt(s, 10);
} catch (NumberFormatException ex) {
throw new SAXException("parseInt(): NumberFormatException: "+ex.getMessage());
}
}
private static float parseFloat(String s) throws SAXException {
if (s==null) return -1.0f; //negative means "unspecified"
try {
return Float.parseFloat(s);
} catch (NumberFormatException ex) {
throw new SAXException("parseFloat(): NumberFormatException: "+ex.getMessage());
}
}
private static TextRange parseTextRange(String s) throws SAXException {
switch (s) {
case "char":
return TextRange.TEXT_RANGE_CHAR;
case "word":
return TextRange.TEXT_RANGE_WORD;
case "sentence":
return TextRange.TEXT_RANGE_SENTENCE;
case "line":
return TextRange.TEXT_RANGE_LINE;
case "paragraph":
return TextRange.TEXT_RANGE_PARAGRAPH;
case "all":
return TextRange.TEXT_RANGE_ALL;
default:
throw new SAXException("Invalid text range: \""+s+"\"");
}
}
public static TextTarget parseTextTarget(String s) throws SAXException {
switch (s) {
case "start":
return TextTarget.TEXT_TARGET_START;
case "end":
return TextTarget.TEXT_TARGET_END;
case "previous char":
return TextTarget.TEXT_TARGET_PREVIOUS_CHAR;
case "next char":
return TextTarget.TEXT_TARGET_NEXT_CHAR;
case "previous word":
return TextTarget.TEXT_TARGET_PREVIOUS_WORD;
case "next word":
return TextTarget.TEXT_TARGET_NEXT_WORD;
case "previous sentence":
return TextTarget.TEXT_TARGET_PREVIOUS_SENTENCE;
case "next sentence":
return TextTarget.TEXT_TARGET_NEXT_SENTENCE;
case "previous line":
return TextTarget.TEXT_TARGET_PREVIOUS_LINE;
case "next line":
return TextTarget.TEXT_TARGET_NEXT_LINE;
case "previous paragraph":
return TextTarget.TEXT_TARGET_PREVIOUS_PARAGRAPH;
case "next paragraph":
return TextTarget.TEXT_TARGET_NEXT_PARAGRAPH;
default:
throw new SAXException("Invalid text target: \""+s+"\"");
}
}
public static void checkForUnknownAttributes(Attributes attributes, String elementName,
String... knownAttributeNames) throws SAXException {
List<String> knownAttributeNamesList = Arrays.asList(knownAttributeNames);
for (int i = 0; i<attributes.getLength(); i++) {
String qName = attributes.getQName(i);
if (!knownAttributeNamesList.contains(qName))
throw new SAXException("Unknown \""+qName+"\" attribute for <"+elementName+"> element");
}
}
}