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 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 "); Group g = createGroupFromAttributes(attributes, alphabet.getNumberOfNodes()); currentGroups.push(g); break; } case "node": if (alphabet==null) throw new SAXException("First element must be "); if (currentGroups.isEmpty()) throw new SAXException(" must be inside of "); if (inNode) throw new SAXException(" inside of another is invalid"); alphabet.addNode(createNodeFromAttributes(attributes, currentGroups.peek())); inNode=true; break; case "textCharAction": { if (alphabet==null) throw new SAXException("First element must be "); if (!inNode) throw new SAXException(" must be inside of "); Node node = alphabet.getLastNode(); node.addAction(createTextCharActionFromAttributes(attributes, node.getLabel())); break; } case "deleteTextAction": if (alphabet==null) throw new SAXException("First element must be "); if (!inNode) throw new SAXException(" must be inside of "); alphabet.getLastNode().addAction(createDeleteTextActionFromAttributes(attributes)); break; case "moveTextCaretAction": if (alphabet==null) throw new SAXException("First element must be "); if (!inNode) throw new SAXException(" must be inside of "); alphabet.getLastNode().addAction(createMoveTextCaretActionFromAttributes(attributes)); break; case "pauseDasherAction": if (alphabet==null) throw new SAXException("First element must be "); if (!inNode) throw new SAXException(" must be inside of "); alphabet.getLastNode().addAction(createPauseDasherActionFromAttributes(attributes)); break; case "changeAlphabetAction": if (alphabet==null) throw new SAXException("First element must be "); if (!inNode) throw new SAXException(" must be inside of "); alphabet.getLastNode().addAction(createChangeAlphabetActionFromAttributes(attributes)); break; case "keyboardAction": if (alphabet==null) throw new SAXException("First element must be "); if (!inNode) throw new SAXException(" must be inside of "); alphabet.getLastNode().addAction(createKeyboardActionFromAttributes(attributes)); break; case "accessibilityAction": if (alphabet==null) throw new SAXException("First element must be "); if (!inNode) throw new SAXException(" must be inside of "); alphabet.getLastNode().addAction(createAccessibilityActionFromAttributes(attributes)); break; case "socketOutputAction": if (alphabet==null) throw new SAXException("First element must be "); if (!inNode) throw new SAXException(" must be inside of "); 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 " +" 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 element"); String orientation = attributes.getValue("orientation"); if (orientation==null || orientation.isEmpty()) throw new SAXException("Non-empty \"orientation\" " +"attribute is required for 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 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 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 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 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 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 knownAttributeNamesList = Arrays.asList(knownAttributeNames); for (int i = 0; i element"); } } }