/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2014 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 3 only, as * published by the Free Software Foundation. * * This code 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 * version 3 for more details. * * You should have received a copy of the GNU General Public License version 3 * along with this work; if not, see http://www.gnu.org/licenses/ * * * Please visit http://neilcsmith.net if you need additional information or * have any questions. */ package net.neilcsmith.praxis.live.pxr; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import net.neilcsmith.praxis.core.Argument; import net.neilcsmith.praxis.core.ComponentAddress; import net.neilcsmith.praxis.core.ComponentType; import net.neilcsmith.praxis.core.PortAddress; import net.neilcsmith.praxis.core.syntax.Token; import static net.neilcsmith.praxis.core.syntax.Token.Type.*; import net.neilcsmith.praxis.core.syntax.Tokenizer; import net.neilcsmith.praxis.core.types.PString; import org.openide.util.Exceptions; /** * * @author Neil C Smith (http://neilcsmith.net) */ class PXRParser { final static String FORMAT_KEY = "pxr.format"; private final static String AT = "@"; private final static String CONNECT = "~"; private final static String ATTRIBUTE_PREFIX = "%"; private final static String PROPERTY_PREFIX = "."; private final static String RELATIVE_ADDRESS_PREFIX = "./"; private final static AttributeElement[] EMPTY_ATTRS = new AttributeElement[0]; private final static PropertyElement[] EMPTY_PROPS = new PropertyElement[0]; private final static ComponentElement[] EMPTY_COMPS = new ComponentElement[0]; private final static ConnectionElement[] EMPTY_CONS = new ConnectionElement[0]; // private final static Argument[] EMPTY_ARGS = new Argument[0]; private final String script; private final ComponentAddress context; private int format = 1; private PXRParser(String script) { this(null, script); } private PXRParser(ComponentAddress context, String script) { this.script = script; this.context = context; } private RootElement doParse() throws ParseException { if (context == null) { return parseFullGraph(); } else { return parseSubGraph(); } } private RootElement parseFullGraph() throws ParseException { try { Iterator<Token> tokens = new Tokenizer(script).iterator(); RootElement root = null; // ignore comments and white space at beginning of file Token t = nextNonCommentOrWhiteSpace(tokens); if (t != null && t.getType() == PLAIN && AT.equals(t.getText())) { root = new RootElement(); } if (root == null) { throw new IllegalArgumentException("No root found in script."); } parseRootElement(root, tokens); // ignore comments and white space at end of file t = nextNonCommentOrWhiteSpace(tokens); if (t != null) { throw new IllegalArgumentException("Unexpected commands found after root."); } return root; } catch (Exception ex) { throw new ParseException(ex); } } private RootElement parseSubGraph() throws ParseException { try { RootElement root = new RootElement(); root.address = context; parseComponentBody(root, script); return root; } catch (Exception ex) { throw new ParseException(ex); } } private static Token nextNonCommentOrWhiteSpace(Iterator<Token> tokens) { while (tokens.hasNext()) { Token t = tokens.next(); if (t.getType() == COMMENT || t.getType() == EOL) { continue; } return t; } return null; } private static Token[] tokensToEOL(Iterator<Token> tokens) { List<Token> tks = new ArrayList<Token>(); while (tokens.hasNext()) { Token t = tokens.next(); if (t.getType() == EOL) { break; } tks.add(t); } return tks.toArray(new Token[tks.size()]); } private void parseRootElement(RootElement root, Iterator<Token> tokens) throws Exception { Token t; if (tokens.hasNext() && (t = tokens.next()).getType() == PLAIN) { root.address = ComponentAddress.valueOf(t.getText()); } else { throw new IllegalArgumentException("No root address found."); } if (tokens.hasNext() && (t = tokens.next()).getType() == PLAIN) { root.type = ComponentType.valueOf(t.getText()); } else { throw new IllegalArgumentException("No root type found."); } if (tokens.hasNext()) { t = tokens.next(); if (t.getType() == BRACED) { parseComponentBody(root, t.getText()); return; } else if (t.getType() == EOL) { parseComponentBody(root, null); return; } } throw new IllegalArgumentException("Root body format error"); } private void parseComponentBody(ComponentElement element, String body) throws Exception { // parse empty body if (body == null || body.trim().isEmpty()) { element.attributes = EMPTY_ATTRS; element.properties = EMPTY_PROPS; element.children = EMPTY_COMPS; element.connections = EMPTY_CONS; return; } // build helper lists - can't be fields because of recursion! List<AttributeElement> attrs = new ArrayList<AttributeElement>(); List<PropertyElement> props = new ArrayList<PropertyElement>(); List<ComponentElement> comps = new ArrayList<ComponentElement>(); List<ConnectionElement> cons = new ArrayList<ConnectionElement>(); Iterator<Token> tokens = new Tokenizer(body).iterator(); while (tokens.hasNext()) { Token t = tokens.next(); String txt = t.getText(); switch (t.getType()) { case COMMENT: // comment start is trimmed of whitespace by tokenizer if (txt.startsWith(ATTRIBUTE_PREFIX)) { // parseAttribute(attrs, element.address, t.getText()); parseAttribute(attrs, element, t.getText()); } break; case PLAIN: if (txt.startsWith(PROPERTY_PREFIX) && txt.length() > 1) { parseProperty(props, element, txt.substring(1), tokensToEOL(tokens)); // parseProperty(props, element.address, txt.substring(1), tokensToEOL(tokens)); } else if (AT.equals(txt)) { parseComponent(element, comps, tokensToEOL(tokens)); } else if (CONNECT.equals(txt)) { parseConnection(element, cons, tokensToEOL(tokens)); } else { throw new IllegalArgumentException("Unexpected PLAIN token : " + txt); } break; case EOL: // no op break; default: throw new IllegalArgumentException( "Unexpected token of type : " + t.getType() + " , body : " + txt); } } element.attributes = attrs.toArray(EMPTY_ATTRS); element.properties = props.toArray(EMPTY_PROPS); element.children = comps.toArray(EMPTY_COMPS); element.connections = cons.toArray(EMPTY_CONS); } private void parseAttribute(List<AttributeElement> attrs, ComponentElement component, String body) throws Exception { String key = null; String value = null; int delim = body.indexOf(" "); if (delim > 1) { key = body.substring(1, delim); value = AttrUtils.unescape(body.substring(delim + 1)); } if (component instanceof RootElement && FORMAT_KEY.equals(key)) { try { format = Integer.parseInt(value); } catch (Exception ex) { Exceptions.printStackTrace(ex); } // don't set format as attribute? Being saved twice! return; } if (key != null && value != null) { AttributeElement a = new AttributeElement(); a.component = component; a.key = key; a.value = value; attrs.add(a); } else {/*?*/ } } private void parseProperty(List<PropertyElement> props, ComponentElement component, String property, Token[] tokens) throws Exception { if (tokens.length == 0) { throw new IllegalArgumentException("Empty tokens passed to parseProperty " + component + "." + property); } // PropertyElement p = new PropertyElement(); // p.address = ControlAddress.create(component, property); Argument[] args = new Argument[tokens.length]; Token t; for (int i = 0; i < args.length; i++) { t = tokens[i]; switch (t.getType()) { case PLAIN: case QUOTED: args[i] = PString.valueOf(t.getText()); break; case BRACED: // do proper evaluation of plain tokens for numbers, etc. args[i] = PString.valueOf(unescapeBraced(t.getText())); break; case SUBCOMMAND: args[i] = new SubCommandArgument(t.getText()); break; default: throw new IllegalArgumentException("Unexpected token type in parseProperty " + component.address + "." + property); } } PropertyElement p = new PropertyElement(); p.component = component; p.property = property; p.args = args; props.add(p); } private String unescapeBraced(String text) { if (format >= 2) { return text; } text = text.replace("\\{", "{"); text = text.replace("\\}", "}"); return text; } private void parseComponent(ComponentElement parent, List<ComponentElement> comps, Token[] tokens) throws Exception { if (tokens.length < 2 || tokens.length > 3) { throw new IllegalArgumentException("Unexpected number of tokens in parseComponent child of " + parent.address); } // next token should be relative component address ComponentAddress address = null; ComponentType type = null; Token t = tokens[0]; if (t.getType() == PLAIN && t.getText().startsWith(RELATIVE_ADDRESS_PREFIX)) { address = ComponentAddress.create(parent.address, t.getText().substring(RELATIVE_ADDRESS_PREFIX.length())); } t = tokens[1]; if (t.getType() == PLAIN) { type = ComponentType.create(t.getText()); } if (address == null || type == null) { throw new IllegalArgumentException("Invalid component creation line : " + address); } ComponentElement comp = new ComponentElement(); comp.address = address; comp.type = type; if (tokens.length == 2) { parseComponentBody(comp, null); } else if (tokens[2].getType() == BRACED) { parseComponentBody(comp, tokens[2].getText()); } else { throw new IllegalArgumentException("Invalid token at end of component line : " + address); } comps.add(comp); } private void parseConnection(ComponentElement parent, List<ConnectionElement> cons, Token[] tokens) { if (tokens.length != 2) { throw new IllegalArgumentException("Unexpected number of tokens in parseConnection of " + parent.address); } PortAddress p1 = parsePortAddress(parent.address, tokens[0]); PortAddress p2 = parsePortAddress(parent.address, tokens[1]); ConnectionElement con = new ConnectionElement(); con.container = parent; con.component1 = p1.getComponentAddress().getID(); con.port1 = p1.getID(); con.component2 = p2.getComponentAddress().getID(); con.port2 = p2.getID(); cons.add(con); } private PortAddress parsePortAddress(ComponentAddress context, Token token) { if (token.getType() == PLAIN) { String txt = token.getText(); if (txt.startsWith(RELATIVE_ADDRESS_PREFIX)) { return PortAddress.create(context + txt.substring(1)); } } throw new IllegalArgumentException("Invalid token in parsePortAddress() - context : " + context + " , token : " + token.getText()); } public static RootElement parse(String script) throws ParseException { if (script == null) { throw new NullPointerException(); } return new PXRParser(script).doParse(); } public static RootElement parseInContext(ComponentAddress context, String script) throws ParseException { if (context == null || script == null) { throw new NullPointerException(); } return new PXRParser(context, script).doParse(); } public static class ParseException extends Exception { public ParseException(Throwable cause) { super(cause); } } public static class Element { } public static class ComponentElement extends Element { public ComponentAddress address; public ComponentType type; public AttributeElement[] attributes; public PropertyElement[] properties; public ComponentElement[] children; public ConnectionElement[] connections; } public static class RootElement extends ComponentElement { } public static class AttributeElement extends Element { public ComponentElement component; public String key; public String value; } public static class PropertyElement extends Element { public ComponentElement component; public String property; public Argument[] args; } public static class ConnectionElement extends Element { public ComponentElement container; public String component1; public String port1; public String component2; public String port2; } }