DasherJava / src / dasherJava / core / alphabets / xml / AlphabetFileParser.java
AlphabetFileParser.java
Raw
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");
		}
	}
}