FileSourceBase.java

/*
 * Copyright 2006 - 2013
 *     Stefan Balev     <stefan.balev@graphstream-project.org>
 *     Julien Baudry    <julien.baudry@graphstream-project.org>
 *     Antoine Dutot    <antoine.dutot@graphstream-project.org>
 *     Yoann Pign��      <yoann.pigne@graphstream-project.org>
 *     Guilhelm Savin   <guilhelm.savin@graphstream-project.org>
 * 
 * This file is part of GraphStream <http://graphstream-project.org>.
 * 
 * GraphStream is a library whose purpose is to handle static or dynamic
 * graph, create them from scratch, file or any source and display them.
 * 
 * This program is free software distributed under the terms of two licenses, the
 * CeCILL-C license that fits European law, and the GNU Lesser General Public
 * License. You can  use, modify and/ or redistribute the software under the terms
 * of the CeCILL-C license as circulated by CEA, CNRS and INRIA at the following
 * URL <http://www.cecill.info> or under the terms of the GNU LGPL as published by
 * the Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 * 
 * This program 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * The fact that you are presently reading this means that you have had
 * knowledge of the CeCILL-C and LGPL licenses and that you accept their terms.
 */
package org.graphstream.stream.file;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StreamTokenizer;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;

import org.graphstream.stream.SourceBase;
import org.graphstream.ui.geom.Point3;

/**
 * Base for various graph file input.
 * 
 * <p>
 * This class is a piece of crap. However it is still used in many places... :-(
 * TODO use a parser generator to replace it.
 * </p>
 * 
 * <p>
 * This class provides parsing utilities to help the creation of new graph
 * readers/parsers. It handles a stack of input files that allow to easily
 * implements "includes" (that is interrupting the parsing of a file to input
 * another one). It wraps stream tokenizers allowing to eat or get specific
 * token types easily.
 * </p>
 * 
 * <p>
 * It is well suited for graph formats using text (not binary), but not for XML
 * based files where a real XML parser would probably be better.
 * </p>
 */
public abstract class FileSourceBase extends SourceBase implements FileSource {
	// Attributes

	/**
	 * The quote character. Can be changed in descendants.
	 */
	protected int QUOTE_CHAR = '"';

	/**
	 * The comment character. Can be changed in descendants.
	 */
	protected int COMMENT_CHAR = '#';

	/**
	 * Is EOL significant?.
	 */
	protected boolean eol_is_significant = false;

	/**
	 * Stack of tokenizers/filenames. Each tokenizer is open on a file. When an
	 * include is found, the current tokenizer is pushed on the stack and a new
	 * one for the included file is created. Once the included file is parsed,
	 * the tokenizer is popped of the stack and the previous one is used.
	 */
	protected ArrayList<CurrentFile> tok_stack = new ArrayList<CurrentFile>();

	/**
	 * Current tokenizer.
	 */
	protected StreamTokenizer st;

	/**
	 * Current file name.
	 */
	protected String filename;

	/**
	 * Map of unknown attributes to corresponding classes.
	 */
	protected HashMap<String, String> attribute_classes = new HashMap<String, String>();

	// Constructors

	/**
	 * No-op constructor.
	 */
	protected FileSourceBase() {
	}

	/**
	 * Setup the reader End-Of-Line policy.
	 * 
	 * @param eol_is_significant
	 *            If true EOL will be returned as a token, else it is ignored.
	 */
	protected FileSourceBase(boolean eol_is_significant) {
		this.eol_is_significant = eol_is_significant;
	}

	/**
	 * Setup the reader End-Of-Line policy and specific comment and quote
	 * characters.
	 * 
	 * @param eol_is_significant
	 *            If true EOL will be returned as a token, else it is ignored.
	 * @param commentChar
	 *            Character used for one line comments.
	 * @param quoteChar
	 *            Character used to enclose quotations.
	 */
	protected FileSourceBase(boolean eol_is_significant, int commentChar, int quoteChar) {
		this.eol_is_significant = eol_is_significant;

		this.COMMENT_CHAR = commentChar;
		this.QUOTE_CHAR = quoteChar;
	}

	// Access

	// Command -- Complete modeField.

	public void readAll(String filename) throws IOException {
		begin(filename);
		while (nextEvents())
			;
		end();
	}

	public void readAll(URL url) throws IOException {
		begin(url);
		while (nextEvents())
			;
		end();
	}

	public void readAll(InputStream stream) throws IOException {
		begin(stream);
		while (nextEvents())
			;
		end();
	}

	public void readAll(Reader reader) throws IOException {
		begin(reader);
		while (nextEvents())
			;
		end();
	}

	// Commands -- By-event modeField.

	public void begin(String filename) throws IOException {
		pushTokenizer(filename);
	}

	public void begin(InputStream stream) throws IOException {
		pushTokenizer(stream);
	}

	public void begin(URL url) throws IOException {
		pushTokenizer(url);
	}

	public void begin(Reader reader) throws IOException {
		pushTokenizer(reader);
	}

	public abstract boolean nextEvents() throws IOException;

	public void end() throws IOException {
		popTokenizer();
	}

	// Command

	/**
	 * Declare that when <code>attribute</code> is found, the corresponding
	 * <code>attribute_class</code> must be instantiated and inserted in the
	 * current element being parsed. This is equivalent to the "map" keyword of
	 * the GML file. An attribute appears in a GML file as a name followed by a
	 * "[...]" block. The contents of this block defines sub-attributes that
	 * must map to public fields of the attribute. Only attributes that are not
	 * handled specifically by this parser can be added.
	 * 
	 * @param attribute
	 *            must name the attribute.
	 * @param attribute_class
	 *            must be the complete name of a Java class that will represent
	 *            the attribute.
	 */
	public void addAttributeClass(String attribute, String attribute_class) {
		attribute_classes.put(attribute, attribute_class);
	}

	// Command -- Parsing -- Include mechanism

	/**
	 * Include the content of a <code>file</code>. This pushes a new tokenizer
	 * on the input stack, calls the {@link #continueParsingInInclude()} method
	 * (that must be implemented to read the include contents) and when finished
	 * pops the tokenizer of the input stack.
	 */
	protected void include(String file) throws IOException {
		pushTokenizer(file);
		continueParsingInInclude();
		popTokenizer();
	}

	/**
	 * Must be implemented to read the content of an include. The current
	 * tokenizer will be set to the included file. When this method returns, the
	 * include file will be closed an parsing will continue where it was before
	 * inclusion.
	 */
	protected abstract void continueParsingInInclude() throws IOException;

	/**
	 * Push a tokenizer created from a file name on the file stack and make it
	 * current.
	 * 
	 * @param file
	 *            Name of the file used as source for the tokenizer.
	 */
	protected void pushTokenizer(String file) throws IOException {
		StreamTokenizer tok;
		CurrentFile cur;
		Reader reader;

		try {
			reader = createReaderFrom(file);
			tok = createTokenizer(reader); 
			
			cur = new CurrentFile(file, tok, reader);
		} catch (FileNotFoundException e) {
			throw new IOException("cannot read file '" + file
					+ "', not found: " + e.getMessage());
		}

		configureTokenizer(tok);
		tok_stack.add(cur);

		st = tok;
		filename = file;
	}

	/**
	 * Create a reader for by the tokenizer.
	 * 
	 * @param file
	 *            File name to be opened.
	 * @return a reader for the tokenizer.
	 * @throws FileNotFoundException
	 *             If the given file does not exist or un readable.
	 */
	protected Reader createReaderFrom(String file) throws FileNotFoundException {
		return new BufferedReader(new FileReader(file));
	}

	/**
	 * Create a stream that can be read by the tokenizer.
	 * 
	 * @param stream
	 *            Input stream to be open as a reader.
	 * @return a reader for the tokenizer.
	 */
	protected Reader createReaderFrom(InputStream stream) {
		return new BufferedReader(new InputStreamReader(stream));
	}
	
	
	/**
	 * Push a tokenizer created from a stream on the file stack and make it
	 * current.
	 * 
	 * @param url
	 *            The URL used as source for the tokenizer.
	 */
	protected void pushTokenizer(URL url) throws IOException {
		pushTokenizer(url.openStream(), url.toString());
	}

	/**
	 * Push a tokenizer created from a stream on the file stack and make it
	 * current.
	 * 
	 * @param stream
	 *            The stream used as source for the tokenizer.
	 */
	protected void pushTokenizer(InputStream stream) throws IOException {
		pushTokenizer(stream, "<?input-stream?>");
	}

	/**
	 * Push a tokenizer created from a stream on the file stack and make it
	 * current.
	 * 
	 * @param stream
	 *            The stream used as source for the tokenizer.
	 * @param name
	 *            The name of the input stream.
	 */
	protected void pushTokenizer(InputStream stream, String name)
			throws IOException {
		StreamTokenizer tok;
		CurrentFile cur;
		Reader reader;

		reader = createReaderFrom(stream);
		tok = createTokenizer(reader); 
		cur = new CurrentFile(name, tok, reader);

		configureTokenizer(tok);
		tok_stack.add(cur);

		st = tok;
		filename = name;
	}



	/**
	 * Push a tokenizer created from a reader on the file stack and make it
	 * current.
	 * 
	 * @param reader
	 *            The reader used as source for the tokenizer.
	 */
	protected void pushTokenizer(Reader reader) throws IOException {
		StreamTokenizer tok;
		CurrentFile cur;
		
		tok = createTokenizer(reader);
		cur = new CurrentFile("<?reader?>", tok,reader);
		configureTokenizer(tok);
		tok_stack.add(cur);

		st = tok;
		filename = "<?reader?>";

	}

	/**
	 * Create a tokenizer from an input source.
	 * 
	 * @param reader
	 *            The reader.
	 * @return The new tokenizer.
	 * @throws IOException
	 *             For any I/O error.
	 */
	private StreamTokenizer createTokenizer(Reader reader)
			throws IOException {
		return new StreamTokenizer(new BufferedReader(reader));
	}

	
	/**
	 * Method to override to configure the tokenizer behaviour. It is called
	 * each time a tokenizer is created (for the parsed file and all included
	 * files).
	 */
	protected void configureTokenizer(StreamTokenizer tok) throws IOException {
		if (COMMENT_CHAR > 0)
			tok.commentChar(COMMENT_CHAR);
		tok.quoteChar(QUOTE_CHAR);
		tok.eolIsSignificant(eol_is_significant);
		tok.wordChars('_', '_');
		tok.parseNumbers();
	}

	/**
	 * Remove the current tokenizer from the stack and restore the previous one
	 * (if any).
	 */
	protected void popTokenizer() throws IOException {
		int n = tok_stack.size();

		if (n <= 0)
			throw new RuntimeException("poped one too many tokenizer");

		n -= 1;

		CurrentFile  cur = tok_stack.remove(n);
		cur.reader.close();
		
		if (n > 0) {
			n -= 1;

			cur = tok_stack.get(n);

			st = cur.tok;
			filename = cur.file;
		}
	}

	// Low level parsing

	/**
	 * Push back the last read thing, so that it can be read anew. This allows
	 * to explore one token ahead, and if not corresponding to what is expected,
	 * go back.
	 */
	protected void pushBack() {
		st.pushBack();
	}

	/**
	 * Read EOF or report garbage at end of file.
	 */
	protected void eatEof() throws IOException {
		int tok = st.nextToken();

		if (tok != StreamTokenizer.TT_EOF)
			parseError("garbage at end of file, expecting EOF, " + gotWhat(tok));
	}

	/**
	 * Read EOL.
	 */
	protected void eatEol() throws IOException {
		int tok = st.nextToken();

		if (tok != StreamTokenizer.TT_EOL)
			parseError("expecting EOL, " + gotWhat(tok));
	}

	/**
	 * Read EOL or EOF.
	 * 
	 * @return The token read StreamTokenizer.TT_EOL or StreamTokenizer.TT_EOF.
	 */
	protected int eatEolOrEof() throws IOException {
		int tok = st.nextToken();

		if (tok != StreamTokenizer.TT_EOL && tok != StreamTokenizer.TT_EOF)
			parseError("expecting EOL or EOF, " + gotWhat(tok));

		return tok;
	}

	/**
	 * Read an expected <code>word</code> token or generate a parse error.
	 */
	protected void eatWord(String word) throws IOException {
		int tok = st.nextToken();

		if (tok != StreamTokenizer.TT_WORD)
			parseError("expecting `" + word + "', " + gotWhat(tok));

		if (!st.sval.equals(word))
			parseError("expecting `" + word + "' got `" + st.sval + "'");
	}

	/**
	 * Read an expected word among the given word list or generate a parse
	 * error.
	 * 
	 * @param words
	 *            The expected words.
	 */
	protected void eatWords(String... words) throws IOException {
		int tok = st.nextToken();

		if (tok != StreamTokenizer.TT_WORD)
			parseError("expecting one of `[" + Arrays.toString(words) + "]', " + gotWhat(tok));

		boolean found = false;

		for (String word : words) {
			if (st.sval.equals(word)) {
				found = true;
				break;
			}
		}

		if (!found)
			parseError("expecting one of `[" + Arrays.toString(words) + "]', got `" + st.sval + "'");
	}

	/**
	 * Eat either a word or another, and return the eated one.
	 * 
	 * @param word1
	 *            The first word to eat.
	 * @param word2
	 *            The alternative word to eat.
	 * @return The word eaten.
	 */
	protected String eatOneOfTwoWords(String word1, String word2)
			throws IOException {
		int tok = st.nextToken();

		if (tok != StreamTokenizer.TT_WORD)
			parseError("expecting `" + word1 + "' or  `" + word2 + "', "
					+ gotWhat(tok));

		if (st.sval.equals(word1))
			return word1;

		if (st.sval.equals(word2))
			return word2;

		parseError("expecting `" + word1 + "' or `" + word2 + "' got `"
				+ st.sval + "'");
		return null;
	}

	/**
	 * Eat the expected symbol or generate a parse error.
	 */
	protected void eatSymbol(char symbol) throws IOException {
		int tok = st.nextToken();

		if (tok != symbol)
			parseError("expecting symbol `" + symbol + "', " + gotWhat(tok));
	}

	/**
	 * Eat one of the list of expected <code>symbols</code> or generate a parse
	 * error none of <code>symbols</code> can be found.
	 */
	protected int eatSymbols(String symbols) throws IOException {
		int tok = st.nextToken();
		int n = symbols.length();
		boolean f = false;

		for (int i = 0; i < n; ++i) {
			if (tok == symbols.charAt(i)) {
				f = true;
				i = n;
			}
		}

		if (!f)
			parseError("expecting one of symbols `" + symbols + "', "
					+ gotWhat(tok));
		
		return tok;
	}

	/**
	 * Eat the expected <code>word</code> or push back what was read so that it
	 * can be read anew.
	 */
	protected void eatWordOrPushbak(String word) throws IOException {
		int tok = st.nextToken();

		if (tok != StreamTokenizer.TT_WORD)
			pushBack();

		if (!st.sval.equals(word))
			pushBack();
	}

	/**
	 * Eat the expected <code>symbol</code> or push back what was read so that
	 * it can be read anew.
	 */
	protected void eatSymbolOrPushback(char symbol) throws IOException {
		int tok = st.nextToken();

		if (tok != symbol)
			pushBack();
	}

	/**
	 * Eat all until an EOL is found. The EOL is also eaten. This works only if
	 * EOL is significant (else it does nothing).
	 */
	protected void eatAllUntilEol() throws IOException {
		if (!eol_is_significant)
			return;

		int tok = st.nextToken();

		if (tok == StreamTokenizer.TT_EOF)
			return;

		while ((tok != StreamTokenizer.TT_EOL)
				&& (tok != StreamTokenizer.TT_EOF)) {
			tok = st.nextToken();
		}
	}

	/**
	 * Eat all availables EOLs.
	 */
	protected void eatAllEols() throws IOException {
		if (!eol_is_significant)
			return;

		int tok = st.nextToken();

		while (tok == StreamTokenizer.TT_EOL)
			tok = st.nextToken();

		pushBack();
	}

	/**
	 * Read a word or generate a parse error.
	 */
	protected String getWord() throws IOException {
		int tok = st.nextToken();

		if (tok != StreamTokenizer.TT_WORD)
			parseError("expecting a word, " + gotWhat(tok));

		return st.sval;
	}

	/**
	 * Get a symbol.
	 */
	protected char getSymbol() throws IOException {
		int tok = st.nextToken();

		if (tok > 0 && tok != StreamTokenizer.TT_WORD
				&& tok != StreamTokenizer.TT_NUMBER
				&& tok != StreamTokenizer.TT_EOL
				&& tok != StreamTokenizer.TT_EOF && tok != QUOTE_CHAR
				&& tok != COMMENT_CHAR) {
			return (char) tok;
		}

		parseError("expecting a symbol, " + gotWhat(tok));
		return (char) 0; // Never reached.
	}

	/**
	 * Get a symbol or push back what was read so that it can be read anew. If
	 * no symbol is found, 0 is returned.
	 */
	protected char getSymbolOrPushback() throws IOException {
		int tok = st.nextToken();

		if (tok > 0 && tok != StreamTokenizer.TT_WORD
				&& tok != StreamTokenizer.TT_NUMBER
				&& tok != StreamTokenizer.TT_EOL
				&& tok != StreamTokenizer.TT_EOF && tok != QUOTE_CHAR
				&& tok != COMMENT_CHAR) {
			return (char) tok;
		}

		pushBack();

		return (char) 0;
	}

	/**
	 * Read a string constant (between quotes) or generate a parse error. Return
	 * the content of the string without the quotes.
	 */
	protected String getString() throws IOException {
		int tok = st.nextToken();

		if (tok != QUOTE_CHAR)
			parseError("expecting a string constant, " + gotWhat(tok));

		return st.sval;
	}

	/**
	 * Read a word or number or generate a parse error. If it is a number it is
	 * converted to a string before being returned.
	 */
	protected String getWordOrNumber() throws IOException {
		int tok = st.nextToken();

		if (tok != StreamTokenizer.TT_WORD && tok != StreamTokenizer.TT_NUMBER)
			parseError("expecting a word or number, " + gotWhat(tok));

		if (tok == StreamTokenizer.TT_NUMBER) {
			// If st.nval is an integer, as it is stored into a double,
			// toString() will transform it by automatically adding ".0", we
			// prevent this. The tokenizer does not allow to read integers.

			if ((st.nval - ((int) st.nval)) == 0)
				return Integer.toString((int) st.nval);
			else
				return Double.toString(st.nval);
		} else {
			return st.sval;
		}
	}

	/**
	 * Read a string or number or generate a parse error. If it is a number it
	 * is converted to a string before being returned.
	 */
	protected String getStringOrNumber() throws IOException {
		int tok = st.nextToken();

		if (tok != QUOTE_CHAR && tok != StreamTokenizer.TT_NUMBER)
			parseError("expecting a string constant or a number, "
					+ gotWhat(tok));

		if (tok == StreamTokenizer.TT_NUMBER) {
			if ((st.nval - ((int) st.nval)) == 0)
				return Integer.toString((int) st.nval);
			else
				return Double.toString(st.nval);
		} else {
			return st.sval;
		}
	}

	/**
	 * Read a string or number or pushback and return null. If it is a number it
	 * is converted to a string before being returned.
	 */
	protected String getStringOrWordOrNumberOrPushback() throws IOException {
		int tok = st.nextToken();

		if (tok == StreamTokenizer.TT_EOL || tok == StreamTokenizer.TT_EOF) {
			pushBack();
			return null;
		}

		if (tok == StreamTokenizer.TT_NUMBER) {
			if ((st.nval - ((int) st.nval)) == 0)
				return Integer.toString((int) st.nval);
			else
				return Double.toString(st.nval);
		} else if (tok == StreamTokenizer.TT_WORD || tok == QUOTE_CHAR) {
			return st.sval;
		} else {
			pushBack();
			return null;
		}
	}

	/**
	 * Read a string or number or generate a parse error. If it is a number it
	 * is converted to a string before being returned.
	 */
	protected String getStringOrWordOrNumber() throws IOException {
		int tok = st.nextToken();

		if (tok == StreamTokenizer.TT_EOL || tok == StreamTokenizer.TT_EOF)
			parseError("expecting word, string or number, " + gotWhat(tok));

		if (tok == StreamTokenizer.TT_NUMBER) {
			if ((st.nval - ((int) st.nval)) == 0)
				return Integer.toString((int) st.nval);
			else
				return Double.toString(st.nval);
		} else {
			return st.sval;
		}
	}

	/**
	 * Read a string or number or generate a parse error. The returned value is
	 * converted to a Number of a String depending on its type.
	 */
	protected Object getStringOrWordOrNumberO() throws IOException {
		int tok = st.nextToken();

		if (tok == StreamTokenizer.TT_EOL || tok == StreamTokenizer.TT_EOF)
			parseError("expecting word, string or number, " + gotWhat(tok));

		if (tok == StreamTokenizer.TT_NUMBER) {
			return st.nval;
		} else {
			return st.sval;
		}
	}

	/**
	 * Read a string or number or generate a parse error. The returned value is
	 * converted to a Number of a String depending on its type.
	 */
	protected Object getStringOrWordOrSymbolOrNumberO() throws IOException {
		int tok = st.nextToken();

		if (tok == StreamTokenizer.TT_EOL || tok == StreamTokenizer.TT_EOF)
			parseError("expecting word, string or number, " + gotWhat(tok));

		if (tok == StreamTokenizer.TT_NUMBER) {
			return st.nval;
		} else if (tok == StreamTokenizer.TT_WORD) {
			return st.sval;
		} else
			return Character.toString((char) tok);
	}

	/**
	 * Read a word or string or generate a parse error.
	 */
	protected String getWordOrString() throws IOException {
		int tok = st.nextToken();

		if (tok == StreamTokenizer.TT_WORD || tok == QUOTE_CHAR)
			return st.sval;

		parseError("expecting a word or string, " + gotWhat(tok));
		return null;
	}

	/**
	 * Read a word or symbol or generate a parse error.
	 */
	protected String getWordOrSymbol() throws IOException {
		int tok = st.nextToken();

		if (tok == StreamTokenizer.TT_NUMBER || tok == QUOTE_CHAR
				|| tok == StreamTokenizer.TT_EOF)
			parseError("expecting a word or symbol, " + gotWhat(tok));

		if (tok == StreamTokenizer.TT_WORD)
			return st.sval;
		else
			return Character.toString((char) tok);
	}

	/**
	 * Read a word or symbol or push back the read thing so that it is readable
	 * anew. In the second case, null is returned.
	 */
	protected String getWordOrSymbolOrPushback() throws IOException {
		int tok = st.nextToken();

		if (tok == StreamTokenizer.TT_NUMBER || tok == QUOTE_CHAR
				|| tok == StreamTokenizer.TT_EOF) {
			pushBack();
			return null;
		}

		if (tok == StreamTokenizer.TT_WORD)
			return st.sval;
		else
			return Character.toString((char) tok);
	}

	/**
	 * Read a word or symbol or string or generate a parse error.
	 */
	protected String getWordOrSymbolOrString() throws IOException {
		int tok = st.nextToken();

		if (tok == StreamTokenizer.TT_NUMBER || tok == StreamTokenizer.TT_EOF)
			parseError("expecting a word, symbol or string, " + gotWhat(tok));

		if (tok == QUOTE_CHAR)
			return st.sval;

		if (tok == StreamTokenizer.TT_WORD)
			return st.sval;
		else
			return Character.toString((char) tok);
	}

	/**
	 * Read a word or symbol or string or number or generate a parse error.
	 */
	protected String getAllExceptedEof() throws IOException {
		int tok = st.nextToken();

		if (tok == StreamTokenizer.TT_EOF)
			parseError("expecting all excepted EOF, " + gotWhat(tok));

		if (tok == StreamTokenizer.TT_NUMBER || tok == StreamTokenizer.TT_EOF) {
			if ((st.nval - ((int) st.nval)) == 0)
				return Integer.toString((int) st.nval);
			else
				return Double.toString(st.nval);
		}

		if (tok == QUOTE_CHAR)
			return st.sval;

		if (tok == StreamTokenizer.TT_WORD)
			return st.sval;
		else
			return Character.toString((char) tok);
	}

	/**
	 * Read a word, a symbol or EOF, or generate a parse error. If this is EOF,
	 * the string "EOF" is returned.
	 */
	protected String getWordOrSymbolOrEof() throws IOException {
		int tok = st.nextToken();

		if (tok == StreamTokenizer.TT_NUMBER || tok == QUOTE_CHAR)
			parseError("expecting a word or symbol, " + gotWhat(tok));

		if (tok == StreamTokenizer.TT_WORD)
			return st.sval;
		else if (tok == StreamTokenizer.TT_EOF)
			return "EOF";
		else
			return Character.toString((char) tok);
	}

	/**
	 * Read a word or symbol or string or EOL/EOF or generate a parse error. If
	 * EOL is read the "EOL" string is returned. If EOF is read the "EOF" string
	 * is returned.
	 * 
	 * @return A string.
	 */
	protected String getWordOrSymbolOrStringOrEolOrEof() throws IOException {
		int tok = st.nextToken();

		if (tok == StreamTokenizer.TT_NUMBER)
			parseError("expecting a word, symbol or string, " + gotWhat(tok));

		if (tok == QUOTE_CHAR)
			return st.sval;

		if (tok == StreamTokenizer.TT_WORD)
			return st.sval;

		if (tok == StreamTokenizer.TT_EOF)
			return "EOF";

		if (tok == StreamTokenizer.TT_EOL)
			return "EOL";

		return Character.toString((char) tok);
	}

	/**
	 * Read a word or number or string or EOL/EOF or generate a parse error. If
	 * EOL is read the "EOL" string is returned. If EOF is read the "EOF" string
	 * is returned. If a number is returned, it is converted to a string as
	 * follows: if it is an integer, only the integer part is converted to a
	 * string without dot or comma and no leading zeros. If it is a float the
	 * fractional part is also converted and the dot is used as separator.
	 * 
	 * @return A string.
	 */
	protected String getWordOrNumberOrStringOrEolOrEof() throws IOException {
		int tok = st.nextToken();

		if (tok == StreamTokenizer.TT_NUMBER) {
			if (st.nval - ((int) st.nval) != 0)
				return Double.toString(st.nval);

			return Integer.toString((int) st.nval);
		}

		if (tok == QUOTE_CHAR)
			return st.sval;

		if (tok == StreamTokenizer.TT_WORD)
			return st.sval;

		if (tok == StreamTokenizer.TT_EOF)
			return "EOF";

		if (tok == StreamTokenizer.TT_EOL)
			return "EOL";

		parseError("expecting a word, a number, a string, EOL or EOF, "
				+ gotWhat(tok));
		return null; // Never happen, parseError throws unconditionally an
						// exception.
	}

	/**
	 * Read a word or string or EOL/EOF or generate a parse error. If EOL is
	 * read the "EOL" string is returned. If EOF is read the "EOF" string is
	 * returned.
	 * 
	 * @return A string.
	 */
	protected String getWordOrStringOrEolOrEof() throws IOException {
		int tok = st.nextToken();

		if (tok == StreamTokenizer.TT_WORD)
			return st.sval;

		if (tok == QUOTE_CHAR)
			return st.sval;

		if (tok == StreamTokenizer.TT_EOL)
			return "EOL";

		if (tok == StreamTokenizer.TT_EOF)
			return "EOF";

		parseError("expecting a word, a string, EOL or EOF, " + gotWhat(tok));
		return null; // Never happen, parseError throws unconditionally an
						// exception.
	}

	// Order: Word | String | Symbol | Number | Eol | Eof

	/**
	 * Read a word or number or string or EOL/EOF or generate a parse error. If
	 * EOL is read the "EOL" string is returned. If EOF is read the "EOF" string
	 * is returned. If a number is returned, it is converted to a string as
	 * follows: if it is an integer, only the integer part is converted to a
	 * string without dot or comma and no leading zeros. If it is a float the
	 * fractional part is also converted and the dot is used as separator.
	 * 
	 * @return A string.
	 */
	protected String getWordOrSymbolOrNumberOrStringOrEolOrEof()
			throws IOException {
		int tok = st.nextToken();

		if (tok == StreamTokenizer.TT_NUMBER) {
			if (st.nval - ((int) st.nval) != 0)
				return Double.toString(st.nval);

			return Integer.toString((int) st.nval);
		}

		if (tok == QUOTE_CHAR)
			return st.sval;

		if (tok == StreamTokenizer.TT_WORD)
			return st.sval;

		if (tok == StreamTokenizer.TT_EOF)
			return "EOF";

		if (tok == StreamTokenizer.TT_EOL)
			return "EOL";

		return Character.toString((char) tok);
	}

	/**
	 * Read a number or generate a parse error.
	 */
	protected double getNumber() throws IOException {
		int tok = st.nextToken();

		if (tok != StreamTokenizer.TT_NUMBER)
			parseError("expecting a number, " + gotWhat(tok));

		return st.nval;
	}

	/**
	 * Read a number (possibly with an exponent) or generate a parse error.
	 */
	protected double getNumberExp() throws IOException {
		int tok = st.nextToken();

		if (tok != StreamTokenizer.TT_NUMBER)
			parseError("expecting a number, " + gotWhat(tok));

		double nb = st.nval;

		tok = st.nextToken();

		if (tok == StreamTokenizer.TT_WORD
				&& (st.sval.startsWith("e-") || st.sval.startsWith("e+"))) {
			double exp = Double.parseDouble(st.sval.substring(2));
			return Math.pow(nb, exp);
		} else {
			st.pushBack();
		}

		return nb;
	}

	/**
	 * Return a string containing "got " then the content of the current
	 * <code>token</code>.
	 */
	protected String gotWhat(int token) {
		switch (token) {
		case StreamTokenizer.TT_NUMBER:
			return "got number `" + st.nval + "'";
		case StreamTokenizer.TT_WORD:
			return "got word `" + st.sval + "'";
		case StreamTokenizer.TT_EOF:
			return "got EOF";
		default:
			if (token == QUOTE_CHAR)
				return "got string constant `" + st.sval + "'";
			else
				return "unknown symbol `" + token + "' (" + ((char) token)
						+ ")";
		}
	}

	/**
	 * Generate a parse error.
	 */
	protected void parseError(String message) throws IOException {
		throw new IOException("parse error: " + filename + ": " + st.lineno()
				+ ": " + message);
	}

	// Access

	/**
	 * True if the <code>string</code> represents a truth statement ("1",
	 * "true", "yes", "on").
	 */
	protected boolean isTrue(String string) {
		string = string.toLowerCase();

		if (string.equals("1"))
			return true;
		if (string.equals("true"))
			return true;
		if (string.equals("yes"))
			return true;
		if (string.equals("on"))
			return true;

		return false;
	}

	/**
	 * True if the <code>string</code> represents a false statement ("0",
	 * "false", "no", "off").
	 */
	protected boolean isFalse(String string) {
		string = string.toLowerCase();

		if (string.equals("0"))
			return true;
		if (string.equals("false"))
			return true;
		if (string.equals("no"))
			return true;
		if (string.equals("off"))
			return true;

		return false;
	}

	/**
	 * Uses {@link #isTrue(String)} and {@link #isFalse(String)} to determine if
	 * <code>value</code> is a truth value and return the corresponding boolean.
	 * 
	 * @throws NumberFormatException
	 *             if the <code>value</code> is not a truth value.
	 */
	protected boolean getBoolean(String value) throws NumberFormatException {
		if (isTrue(value))
			return true;
		if (isFalse(value))
			return false;
		throw new NumberFormatException("not a truth value `" + value + "'");
	}

	/**
	 * Try to transform <code>value</code> into a double.
	 * 
	 * @throws NumberFormatException
	 *             if the <code>value</code> is not a double.
	 */
	protected double getReal(String value) throws NumberFormatException {
		return Double.parseDouble(value);
	}

	/**
	 * Try to transform <code>value</code> into a long.
	 * 
	 * @throws NumberFormatException
	 *             if the <code>value</code> is not a long.
	 */
	protected long getInteger(String value) throws NumberFormatException {
		return Long.parseLong(value);
	}

	/**
	 * Get a number triplet with numbers separated by comas and return a new
	 * point for it. For example "0,1,2".
	 */
	protected Point3 getPoint3(String value) throws NumberFormatException {
		int p0 = value.indexOf(',');
		int p1 = value.indexOf(',', p0 + 1);

		if (p0 > 0 && p1 > 0) {
			String n0, n1, n2;
			float v0, v1, v2;

			n0 = value.substring(0, p0);
			n1 = value.substring(p0 + 1, p1);
			n2 = value.substring(p1 + 1);

			v0 = Float.parseFloat(n0);
			v1 = Float.parseFloat(n1);
			v2 = Float.parseFloat(n2);

			return new Point3(v0, v1, v2);
		}

		throw new NumberFormatException("value '" + value
				+ "' not in a valid point3 format");
	}

	/*
	 * Get a number triplet with numbers separated by comas and return new
	 * bounds for it. For example "0,1,2".
	protected Bounds3 getBounds3(String value) throws NumberFormatException {
		int p0 = value.indexOf(',');
		int p1 = value.indexOf(',', p0 + 1);

		if (p0 > 0 && p1 > 0) {
			String n0, n1, n2;
			float v0, v1, v2;

			n0 = value.substring(0, p0);
			n1 = value.substring(p0 + 1, p1);
			n2 = value.substring(p1 + 1);

			v0 = Float.parseFloat(n0);
			v1 = Float.parseFloat(n1);
			v2 = Float.parseFloat(n2);

			return new Bounds3(v0, v1, v2);
		}

		throw new NumberFormatException("value '" + value
				+ "' not in a valid point3 format");
	}
	 */

	// Nested classes

	/**
	 * Currently processed file.
	 * <p>
	 * The graph reader base can process includes in files, and handles a stack
	 * of files.
	 * </p>
	 * 
	 */
	protected static class CurrentFile {
		/**
		 * The file name.
		 */
		public String file;

		/**
		 * The stream tokenizer.
		 */
		public StreamTokenizer tok;
		
		public Reader reader;

		public CurrentFile(String f, StreamTokenizer t, Reader reader) {
			file = f;
			tok = t;
			this.reader=reader;
		}
	}
}