/*******************************************************************************
* Copyright (c) 2006-2013 The RCP Company and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* The RCP Company - initial API and implementation
*******************************************************************************/
package com.rcpcompany.uibindings.internal.utils;
import java.io.IOException;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.EStructuralFeature;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.widgets.Display;
import com.rcpcompany.uibindings.Constants;
import com.rcpcompany.uibindings.utils.IBindingSpec;
import com.rcpcompany.uibindings.utils.IBindingSpec.BaseType;
import com.rcpcompany.uibindings.utils.IBindingSpec.SpecContext;
import com.rcpcompany.utils.logging.LogUtils;
/**
* Factory for {@link IBindingSpec}.
* <p>
* For unit conversions, it supports the following from <a
* href="http://www.w3schools.com/css/css_units.asp">the CSS2 standard</a>:
* <dl>
* <dt>px</dt>
* <dd>pixels (a dot on the computer screen)</dd>
* <dt>em</dt>
* <dd>1em is equal to the current font size. 2em means 2 times the size of the current font. E.g.,
* if an element is displayed with a font of 12 pt, then '2em' is 24 pt. The 'em' is a very useful
* unit in CSS, since it can adapt automatically to the font that the reader uses</dd>
* <dt>mm</dt>
* <dd>millimeter</dd>
* </dl>
*
* @author Tonny Madsen, The RCP Company
*/
public final class BindingSpecFactory {
private BindingSpecFactory() {
}
/**
* The aliases: alias -> canonical version.
*/
private static final Map<String, String> ALIASES = new HashMap<String, String>();
/**
* The types of all all defined arguments.
*/
private static final Map<String, Class<?>> ARGUMENT_TYPES = new HashMap<String, Class<?>>();
/**
* List of the arguments that supports a unit.
*/
private static final Map<String, Boolean> ARGUMENTS_WITH_UNITS = new HashMap<String, Boolean>();
/**
* The display used for unit conversions.
*/
private static final Display DISPLAY;
/**
* The font metrics used for the default font. Used for unit conversions.
*/
private static FontMetrics theFontMetrics;
/**
* The current font height used for unit conversions.
*/
private static float theFontHeight;
static {
ALIASES.put(IBindingSpec.WIDTH, Constants.ARG_WIDTH);
ARGUMENT_TYPES.put(Constants.ARG_WIDTH, Integer.class);
ARGUMENTS_WITH_UNITS.put(Constants.ARG_WIDTH, true);
ALIASES.put(IBindingSpec.HEIGHT, Constants.ARG_HEIGHT);
ARGUMENT_TYPES.put(Constants.ARG_HEIGHT, Integer.class);
ALIASES.put(IBindingSpec.DYNAMIC, Constants.ARG_DYNAMIC);
ARGUMENT_TYPES.put(IBindingSpec.WIDTH_WEIGHT, Integer.class);
ALIASES.put(IBindingSpec.ALIGNMENT, Constants.ARG_ALIGNMENT);
ARGUMENT_TYPES.put(Constants.ARG_ALIGNMENT, String.class);
ARGUMENT_TYPES.put(IBindingSpec.SCROLLBARS, String.class);
ARGUMENT_TYPES.put(IBindingSpec.MULTI, Boolean.class);
ARGUMENT_TYPES.put(IBindingSpec.COLLECT_MESSAGES, Boolean.class);
ARGUMENT_TYPES.put(Constants.ARG_READONLY, Boolean.class);
ARGUMENT_TYPES.put(Constants.ARG_DYNAMIC, Boolean.class);
ARGUMENT_TYPES.put(Constants.ARG_PASSWORD, Boolean.class);
ARGUMENT_TYPES.put(Constants.ARG_TOOL_TIP_TEXT, String.class);
ARGUMENT_TYPES.put(Constants.ARG_LABEL, String.class);
ARGUMENT_TYPES.put(Constants.ARG_TYPE, String.class);
ARGUMENT_TYPES.put(Constants.ARG_MESSAGE_FORMAT, String.class);
ARGUMENT_TYPES.put(Constants.ARG_UNIT, String.class);
// Compute and store a font metric
DISPLAY = Display.getDefault();
final GC gc = new GC(DISPLAY);
final Font f = JFaceResources.getDefaultFont();
theFontHeight = f.getFontData()[0].getHeight();
gc.setFont(f);
theFontMetrics = gc.getFontMetrics();
gc.dispose();
}
/**
* All calculated specs so far.
*/
private static final Map<EClass, Map<String, List<IBindingSpec>>> CALCULATED_SPECS = new HashMap<EClass, Map<String, List<IBindingSpec>>>();
/**
* Parses the specified spec based on the specified startType and returns the calculated spec
* list.
*
* @param startType the start type
* @param spec the specification
* @param context the context for the specification
* @return the spec list or <code>null</code> if the specification cannot be parsed
*/
public static List<IBindingSpec> parseSingleSpec(final EClass startType, final String spec,
final SpecContext context) {
Map<String, List<IBindingSpec>> typeSpecs = CALCULATED_SPECS.get(startType);
if (typeSpecs == null) {
typeSpecs = new HashMap<String, List<IBindingSpec>>();
CALCULATED_SPECS.put(startType, typeSpecs);
}
final List<IBindingSpec> oldSL = typeSpecs.get(spec);
if (oldSL != null) return oldSL;
final List<IBindingSpec> newSL = new ArrayList<IBindingSpec>();
try {
final IBindingSpecParserContext pc = new MyBindingSpecParserContext(newSL, context, spec, startType);
pc.nextToken();
while (!pc.isTokenType(StreamTokenizer.TT_EOF)) {
parseBaseSpec(pc);
if (pc.isTokenType('(')) {
parseSpecArguments(pc);
}
if (pc.isTokenType('.')) {
pc.nextToken();
continue;
}
if (pc.isTokenType(StreamTokenizer.TT_EOF)) {
break;
}
}
if (!pc.isTokenType(StreamTokenizer.TT_EOF)) {
pc.syntaxError("Spec ::= BaseSpec Arguments?", "<feature>");
}
if (pc.getLastSpec() != null) {
pc.getLastSpec().setLast(true);
}
/*
* All but the last spec must be a to-one
*/
for (final IBindingSpec s : newSL) {
final EStructuralFeature resultFeature = s.getResultFeature();
if (s.isLast() && pc.getSpecContext() == SpecContext.OBSERVABLE) {
if (!(resultFeature instanceof EReference)) {
pc.error("Last feature " + resultFeature.getContainerClass().getName() + "."
+ resultFeature.getName() + " is not reference");
}
break;
}
if (resultFeature != null && resultFeature.isMany()) {
pc.error("Feature " + resultFeature.getContainerClass().getName() + "." + resultFeature.getName()
+ "." + " must be to-one");
}
}
} catch (final IOException ex) {
LogUtils.error(startType, "", ex);
return null;
} catch (final RuntimeException ex) {
return null;
}
typeSpecs.put(spec, newSL);
return newSL;
}
/**
* Parses the base part of a specification.
* <p>
* Spec ::= BaseSpec Arguments?
*
* BaseSpec ::= ( Feature | Feature | ContainerSpec )
*
* @param pc the parse context
* @throws IOException from {@link StringTokenizer}
*/
private static void parseBaseSpec(final IBindingSpecParserContext pc) throws IOException {
pc.newSpecLevel();
if (pc.getSpecClass() == null) {
pc.error("New level not allowed unless previous level has a EClass type");
}
String featureName = null;
if (pc.isTokenType(StreamTokenizer.TT_WORD)) {
featureName = pc.getTokenString();
pc.nextToken();
final BaseType bt = BaseType.parse(featureName);
if (bt != null) {
pc.checkContext("Virtual feature " + bt + " only supported for tables", SpecContext.TABLE_COLUMN);
pc.addSpec(new MyBindingSpecVirtual(bt), null);
} else {
final EStructuralFeature feature = findFeature(pc.getSpec(), pc.getSpecClass(), featureName);
/*
* PARSE: optional MapSpec:=***'('<name>'='<value>':'<name>'}'
*/
if (pc.isTokenType('{')) {
parseMapSpec(pc, feature);
} else {
if (feature.getEType() instanceof EClass) {
pc.addSpec(new MyBindingSpecFeature(feature), (EClass) feature.getEType());
} else {
pc.addSpec(new MyBindingSpecFeature(feature), null);
}
}
}
} else if (pc.isTokenType('^')) {
parseContainerSpec(pc);
} else {
pc.syntaxError("BaseSpec ::= ( Feature | Feature | ContainerSpec )", "one of word or '^'");
}
}
/**
* Parses container.
* <p>
* Container ::= '^' ( | '^' '=' EClass )
*
* @param pc the parse context
* @throws IOException from {@link StreamTokenizer}
*/
private static void parseContainerSpec(IBindingSpecParserContext pc) throws IOException {
pc.nextToken();
if (pc.isTokenType('^')) {
pc.nextToken();
if (!pc.isTokenType('=')) {
pc.syntaxError("Container ::= '^' ( | '^'***'=' EClass )", "'='");
}
pc.nextToken();
if (!pc.isTokenType(StreamTokenizer.TT_WORD)) {
pc.syntaxError("Container ::= '^' ( | '^' '=' *** EClass )", "<EClass>");
}
final String containerClass = pc.getTokenString();
pc.nextToken();
pc.error("Not implemented");
} else {
for (final EReference ref : pc.getSpecClass().getEReferences()) {
if (ref.isContainer()) {
pc.addSpec(new MyBindingSpecFeature(ref), ref.getEReferenceType());
return;
}
}
pc.error(pc.getSpecClass() + " does not have a container.");
}
}
/**
* Parses MapSpec.
* <p>
* MapSpec ::= '('<name>'='<value>':'<name>'}'
*
* @param pc the parse context
* @param feature the feature
* @throws IOException
*/
private static void parseMapSpec(final IBindingSpecParserContext pc, final EStructuralFeature feature)
throws IOException {
pc.nextToken();
/*
* Feature must be to-many reference
*/
if (!(feature instanceof EReference)) {
pc.error("Feature " + feature.getContainerClass().getName() + "." + feature.getName() + " is not reference");
}
if (!feature.isMany()) {
pc.error("Feature " + feature.getContainerClass().getName() + "." + feature.getName() + " must be to-many");
}
if (!((EReference) feature).isContainment()) {
pc.error("Feature " + feature.getContainerClass().getName() + "." + feature.getName()
+ " must be containment");
}
/*
* Type of detail
*/
final EClass defailClass = (EClass) feature.getEType();
/*
* PARSE: MapSpec:='('***<name>'='<value>':'<name>'}'
*/
if (!pc.isTokenType(StreamTokenizer.TT_WORD)) {
pc.syntaxError("MapSpec:='('***<name>'='<value>':'<name>'}'", "<name>");
}
final EStructuralFeature keyFeature = findFeature(pc.getSpec(), defailClass, pc.getTokenString());
pc.nextToken();
/*
* PARSE: MapSpec:='('<name>***'='<value>':'<name>'}'
*/
if (pc.isTokenType('=')) {
pc.nextToken();
} else {
pc.syntaxError("MapSpec:='('<name>***'='<value>':'<name>'}'", "=");
}
/*
* PARSE: MapSpec:='('***<name>'='<value>':'<name>'}'
*/
Object keyValue = null;
switch (pc.getTokenType()) {
case StreamTokenizer.TT_NUMBER:
keyValue = pc.getTokenInteger();
pc.nextToken();
break;
case StreamTokenizer.TT_WORD:
keyValue = pc.getTokenString();
pc.nextToken();
break;
case '"':
case '\'':
keyValue = pc.getTokenString();
pc.nextToken();
break;
default:
pc.syntaxError("MapSpec:='('<name>'='***<value>':'<name>'}'", "one of integer, string or word");
}
/*
* PARSE: MapSpec:='('<name>'='<value>':'<name>***'}'
*/
if (pc.isTokenType(':')) {
pc.nextToken();
} else {
pc.syntaxError("MapSpec:='('<name>'='<value>***':'<name>'}'", "':'");
}
/*
* PARSE: MapSpec:='('<name>'='<value>':'***<name>'}'
*/
if (!pc.isTokenType(StreamTokenizer.TT_WORD)) {
pc.syntaxError("MapSpec:='('<name>'='<value>':***'<name>'}'", "'<name>'");
}
final EStructuralFeature valueFeature = findFeature(pc.getSpec(), defailClass, pc.getTokenString());
pc.nextToken();
if (keyFeature == valueFeature) {
pc.error("key and value features identical: " + keyFeature.getContainerClass().getName() + "."
+ keyFeature.getName());
}
if (valueFeature.getEType() instanceof EClass) {
pc.addSpec(new MyBindingSpecFeatureKeyValue(feature, keyFeature, keyValue, valueFeature),
(EClass) feature.getEType());
} else {
pc.addSpec(new MyBindingSpecFeatureKeyValue(feature, keyFeature, keyValue, valueFeature), null);
}
/*
* PARSE: MapSpec:='('<name>'='<value>':'<name>***'}'
*/
if (pc.isTokenType('}')) {
pc.nextToken();
} else {
pc.syntaxError("MapSpec:='('<name>'='<value>':'<name>***'}'", "'}'");
}
}
/**
* Parses an argument.
* <p>
* Arguments ::= '(' Argument ( ',' Argument )+ ')' Argument ::= Name ( '=' Value Unit? )?
*
* @param pc the parse context
* @throws IOException from {@link StreamTokenizer}
*/
private static void parseSpecArguments(final IBindingSpecParserContext pc) throws IOException {
pc.checkContext("Arguments are not supported", SpecContext.FORM_FIELD, SpecContext.TABLE_COLUMN);
pc.nextToken();
final Map<String, Object> arguments = pc.getLastSpec().getArguments();
while (pc.isTokenType(StreamTokenizer.TT_WORD)) {
String argName = pc.getTokenString();
pc.nextToken();
if (ALIASES.containsKey(argName)) {
argName = ALIASES.get(argName);
}
Object argValue = null;
String argUnit = null;
if (pc.isTokenType('=')) {
pc.nextToken();
argValue = pc.getTokenArgumentValue();
if (argValue == null) {
pc.syntaxError("Argument ::= Name ( '=' *** Value Unit? )?", "one of integer, string or word");
}
if (pc.isTokenType(StreamTokenizer.TT_WORD)) {
argUnit = pc.getTokenString();
pc.nextToken();
}
} else if ((pc.isTokenType(',')) || (pc.isTokenType(')'))) {
argValue = "true";
} else {
pc.syntaxError("Argument ::= Name ( *** '=' Value Unit? )?", "'='");
}
// Check the argument names and values
if (!ARGUMENT_TYPES.containsKey(argName)) {
for (final String nn : ARGUMENT_TYPES.keySet()) {
if (nn.equalsIgnoreCase(argName)) {
argName = nn;
break;
}
}
}
if (!ARGUMENT_TYPES.containsKey(argName)) {
pc.error("Unknown argument '" + argName + "'");
}
final Class<?> argClass = ARGUMENT_TYPES.get(argName);
if (argClass == Boolean.class) {
argValue = Boolean.parseBoolean("" + argValue);
} else if (!(argClass.isInstance(argValue))) {
pc.error("Argument '" + argName + "' takes an " + argClass.getSimpleName() + " as argument, got '"
+ argValue + "'");
}
if (ARGUMENTS_WITH_UNITS.get(argName) == Boolean.TRUE) {
final float factor;
if (argUnit == null) {
factor = 1;
} else if (argUnit.equals("px")) {
factor = 1;
} else if (argUnit.equals("em")) {
factor = theFontHeight;
} else if (argUnit.equals("mm")) {
factor = DISPLAY.getDPI().x / 25.4f;
} else if (argUnit.equals("dlu")) {
factor = theFontHeight / 4.0f;
} else {
factor = 0;
pc.error("Unit of argument '" + argName + "' can be 'em', 'dlu' or 'mm', got '" + argUnit + "'");
}
argValue = Math.round(((Integer) argValue) * factor);
} else {
if (argUnit != null) {
pc.error("Argument '" + argName + "' does not support units, got unit '" + argUnit + "'");
}
}
if (arguments.containsKey(argName)) {
pc.error("Argument '" + argName + "' specified multiple times");
}
arguments.put(argName, argValue);
// More arguments?
if (pc.isTokenType(',')) {
pc.nextToken();
} else if (pc.isTokenType(')')) {
break;
} else {
pc.syntaxError("Arguments ::= '(' Argument *** ( ',' Argument )+ ')'", "one of ',' or ')'");
}
}
// End of arguments
if (pc.isTokenType(')')) {
pc.nextToken();
} else {
pc.syntaxError("Arguments ::= '(' Argument ( ',' Argument )+ *** ')'", "<name> or ')'");
}
}
/**
* @param spec
* @param type
* @param featureName
* @return
*/
private static EStructuralFeature findFeature(String spec, EClass type, final String featureName) {
EStructuralFeature feature = type.getEStructuralFeature(featureName);
/*
* No direct match... Try ignoring case!
*/
if (feature == null) {
for (final EStructuralFeature f : type.getEAllStructuralFeatures()) {
if (f.getName().equalsIgnoreCase(featureName)) {
feature = f;
break;
}
}
}
if (feature == null) {
LogUtils.throwException(type, "In spec: '" + spec + "': " + type.getName() + "#" + featureName
+ " does not exist", null);
// Not reached!
}
return feature;
}
/**
* Creates and returns a new tokenizer for the specified string.
*
* @param spec the specification string
* @return the new tokenizer
*/
private static StreamTokenizer createTokenizer(String spec) {
final StreamTokenizer st = new StreamTokenizer(new StringReader(spec));
// st.commentChar('#');
st.lowerCaseMode(false);
st.parseNumbers();
st.quoteChar('"');
st.quoteChar('\'');
st.slashSlashComments(false);
st.slashStarComments(false);
st.ordinaryChar('.');
st.wordChars('_', '_');
return st;
}
/**
* Implementation of {@link IBindingSpecParserContext} used in
* {@link BindingSpecFactory#parseSingleSpec(EClass, String, SpecContext)}.
*/
private static final class MyBindingSpecParserContext implements IBindingSpecParserContext {
private final List<IBindingSpec> myNewSL;
private final SpecContext myContext;
private final String mySpec;
private final EClass myStartType;
final StreamTokenizer st;
private MyBindingSpecBase myLastSpec = null;
private EClass mySpecClass;
private MyBindingSpecParserContext(List<IBindingSpec> newSL, SpecContext context, String spec, EClass startType) {
myNewSL = newSL;
myContext = context;
mySpec = spec;
myStartType = startType;
st = createTokenizer(mySpec);
mySpecClass = myStartType;
}
@Override
public void newSpecLevel() {
myLastSpec = null;
}
@Override
public void addSpec(MyBindingSpecBase spec, EClass newSpecClass) {
myNewSL.add(spec);
myLastSpec = spec;
mySpecClass = newSpecClass;
}
@Override
public MyBindingSpecBase getLastSpec() {
return myLastSpec;
}
@Override
public EClass getSpecClass() {
return mySpecClass;
}
@Override
public void nextToken() throws IOException {
st.nextToken();
}
@Override
public int getTokenType() {
return st.ttype;
}
@Override
public String getTokenString() {
switch (st.ttype) {
case '"':
case '\'':
case StreamTokenizer.TT_WORD:
return st.sval;
default:
throw new IllegalArgumentException("Not a string token!");
}
}
@Override
public int getTokenInteger() {
if (st.ttype != StreamTokenizer.TT_NUMBER) throw new IllegalArgumentException("Not a number !");
return (int) st.nval;
}
@Override
public Object getTokenArgumentValue() throws IOException {
Object value = null;
switch (getTokenType()) {
case StreamTokenizer.TT_NUMBER:
value = getTokenInteger();
nextToken();
break;
case StreamTokenizer.TT_WORD:
value = getTokenString();
nextToken();
break;
case '"':
case '\'':
value = getTokenString();
nextToken();
break;
default:
break;
}
return value;
}
@Override
public boolean isTokenType(int token) {
return getTokenType() == token;
}
@Override
public String getSpec() {
return mySpec;
}
@Override
public SpecContext getSpecContext() {
return myContext;
}
@Override
public void checkContext(String message, SpecContext... legalContexts) {
for (final SpecContext c : legalContexts) {
if (c == getSpecContext()) return;
}
error(message);
}
@Override
public void error(String message) {
LogUtils.throwException(myStartType, "In spec: '" + getSpec() + "': " + message, null);
}
@Override
public void syntaxError(String bnf, String expectedToken) {
error("In " + bnf + ": expected " + expectedToken + ", got '" + st.toString() + "'");
}
}
protected static class MyBindingSpecFeature extends MyBindingSpecBase {
private final EStructuralFeature myFeature;
protected MyBindingSpecFeature(EStructuralFeature feature) {
myFeature = feature;
}
@Override
public BaseType getType() {
return BaseType.FEATURE;
}
@Override
public EStructuralFeature getFeature() {
return myFeature;
}
@Override
public EStructuralFeature getResultFeature() {
return getFeature();
}
}
protected static class MyBindingSpecFeatureKeyValue extends MyBindingSpecBase {
private final EStructuralFeature myFeature;
private final EStructuralFeature myKeyFeature;
private final Object myKeyValue;
private final EStructuralFeature myValueFeature;
protected MyBindingSpecFeatureKeyValue(EStructuralFeature feature, EStructuralFeature keyFeature,
Object keyValue, EStructuralFeature valueFeature) {
myFeature = feature;
myKeyFeature = keyFeature;
myKeyValue = keyValue;
myValueFeature = valueFeature;
}
@Override
public BaseType getType() {
return BaseType.KEY_VALUE;
}
@Override
public EStructuralFeature getFeature() {
return myFeature;
}
@Override
public EStructuralFeature getKeyFeature() {
return myKeyFeature;
}
@Override
public Object getKeyValue() {
return myKeyValue;
}
@Override
public EStructuralFeature getValueFeature() {
return myValueFeature;
}
@Override
public EStructuralFeature getResultFeature() {
return super.getValueFeature();
}
}
protected static class MyBindingSpecVirtual extends MyBindingSpecBase {
private final BaseType myType;
protected MyBindingSpecVirtual(BaseType type) {
myType = type;
}
@Override
public BaseType getType() {
return myType;
}
@Override
public EStructuralFeature getFeature() {
return null;
}
}
protected abstract static class MyBindingSpecBase implements IBindingSpec {
private Map<String, Object> myArguments;
private boolean myLast = false;
@Override
public Map<String, Object> getArguments() {
if (myArguments == null) {
myArguments = new HashMap<String, Object>();
}
return myArguments;
}
@Override
public EStructuralFeature getResultFeature() {
return null;
}
@Override
public EStructuralFeature getKeyFeature() {
return null;
}
@Override
public Object getKeyValue() {
return null;
}
@Override
public EStructuralFeature getValueFeature() {
return null;
}
@Override
public boolean isLast() {
return myLast;
}
public void setLast(boolean last) {
myLast = last;
}
}
/**
* Context used during parsing of a binding specification.
*/
public interface IBindingSpecParserContext {
/**
* Returns the context specified to
* {@link IBindingSpec.Factory#parseSingleSpec(EClass, String, SpecContext)}.
*
* @return the context
*/
SpecContext getSpecContext();
/**
* Adds a new specification record to the list of records for this current specification
* string
*
* @param spec the new record
* @param newSpecClass the resulting {@link EClass}
*/
void addSpec(MyBindingSpecBase spec, EClass newSpecClass);
/**
* Returns the class at the start of this spec record.
*
* @return the current class
*/
EClass getSpecClass();
/**
* Tests whether the next token is of the specified type.
* <p>
* See {@link StreamTokenizer#ttype} for further information.
*
* @param token the token to test
* @return <code>true</code> if the next token is the specified
*/
boolean isTokenType(int token);
/**
* The integer value of the current token if relevant.
*
* @return the integer value
*/
int getTokenInteger();
/**
* The string value of the current token if relevant.
*
* @return the string value
*/
String getTokenString();
/**
* The value of the current token if understood as an argument value.
*
* @return the value ({@link String} or {@link Integer}) or <code>null</code> if not found
*/
Object getTokenArgumentValue() throws IOException;
/**
* Starts a new specification level.
*/
void newSpecLevel();
/**
* The last specification record added.
*
* @return the last record
*/
MyBindingSpecBase getLastSpec();
/**
* Issues an error and returns null from
* {@link IBindingSpec.Factory#parseSingleSpec(EClass, String, SpecContext)}.
*
* @param message the messages of the error
*/
void error(String message);
/**
* Issues a syntax error and returns null from
* {@link IBindingSpec.Factory#parseSingleSpec(EClass, String, SpecContext)}.
*
* @param bnf the BNF currently being parsed
* @param expectedToken the expected token or tokens "one of..."
*/
void syntaxError(String bnf, String expectedToken);
/**
* Checks the the specification context is one of the specified...
*
* @param message the message to use for the error if not one of the specified contexts
* @param legalContexts the legal contexts
*/
void checkContext(String message, SpecContext... legalContexts);
/**
* Returns the current token type.
* <p>
* See {@link StreamTokenizer#ttype} for further information.
*
* @return the current token
*/
int getTokenType();
/**
* Moves to the next token.
*
* @throws IOException from {@link StringTokenizer}
*/
void nextToken() throws IOException;
/**
* The specification string being parsed.
*
* @return the specification string
*/
String getSpec();
}
}