/*
* 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.dgs;
import java.awt.Color;
import java.io.IOException;
import java.io.Reader;
import java.util.HashMap;
import java.util.LinkedList;
import org.graphstream.graph.implementations.AbstractElement.AttributeChangeEvent;
import org.graphstream.stream.SourceBase.ElementType;
import org.graphstream.stream.file.FileSourceDGS;
import org.graphstream.util.parser.ParseException;
import org.graphstream.util.parser.Parser;
// import org.graphstream.util.time.ISODateIO;
public class DGSParser implements Parser {
static enum Token {
AN, CN, DN, AE, CE, DE, CG, ST, CL, TF, EOF
}
protected static final int BUFFER_SIZE = 4096;
public static final int ARRAY_OPEN = '{';
public static final int ARRAY_CLOSE = '}';
public static final int MAP_OPEN = '[';
public static final int MAP_CLOSE = ']';
Reader reader;
int line, column;
int bufferCapacity, bufferPosition;
char[] buffer;
int[] pushback;
int pushbackOffset;
FileSourceDGS dgs;
String sourceId;
Token lastDirective;
// ISODateIO dateIO;
public DGSParser(FileSourceDGS dgs, Reader reader) {
this.dgs = dgs;
this.reader = reader;
bufferCapacity = 0;
buffer = new char[BUFFER_SIZE];
pushback = new int[10];
pushbackOffset = -1;
this.sourceId = String.format("<DGS stream %x>", System.nanoTime());
// try {
// dateIO = new ISODateIO();
// } catch (Exception e) {
// e.printStackTrace();
// }
}
/*
* (non-Javadoc)
*
* @see org.graphstream.util.parser.Parser#close()
*/
public void close() throws IOException {
reader.close();
}
/*
* (non-Javadoc)
*
* @see org.graphstream.util.parser.Parser#open()
*/
public void open() throws IOException, ParseException {
header();
}
/*
* (non-Javadoc)
*
* @see org.graphstream.util.parser.Parser#all()
*/
public void all() throws IOException, ParseException {
header();
while (next())
;
}
protected int nextChar() throws IOException {
int c;
if (pushbackOffset >= 0)
return pushback[pushbackOffset--];
if (bufferCapacity == 0 || bufferPosition >= bufferCapacity) {
bufferCapacity = reader.read(buffer, 0, BUFFER_SIZE);
bufferPosition = 0;
}
if (bufferCapacity <= 0)
return -1;
c = buffer[bufferPosition++];
//
// Handle special EOL
// - LF
// - CR
// - CR+LF
//
if (c == '\r') {
if (bufferPosition < bufferCapacity) {
if (buffer[bufferPosition] == '\n')
bufferPosition++;
} else {
c = nextChar();
if (c != '\n')
pushback(c);
}
c = '\n';
}
if (c == '\n') {
line++;
column = 0;
} else
column++;
return c;
}
protected void pushback(int c) throws IOException {
if (c < 0)
return;
if (pushbackOffset + 1 >= pushback.length)
throw new IOException("pushback buffer overflow");
pushback[++pushbackOffset] = c;
}
protected void skipLine() throws IOException {
int c;
while ((c = nextChar()) != '\n' && c >= 0)
;
}
protected void skipWhitespaces() throws IOException {
int c;
while ((c = nextChar()) == ' ' || c == '\t')
;
pushback(c);
}
protected void header() throws IOException, ParseException {
int[] dgs = new int[6];
for (int i = 0; i < 6; i++)
dgs[i] = nextChar();
if (dgs[0] != 'D' || dgs[1] != 'G' || dgs[2] != 'S')
throw parseException(String.format(
"bad magic header, 'DGS' expected, got '%c%c%c'", dgs[0],
dgs[1], dgs[2]));
if (dgs[3] != '0' || dgs[4] != '0' || dgs[5] < '0' || dgs[5] > '5')
throw parseException(String.format("bad version \"%c%c%c\"",
dgs[0], dgs[1], dgs[2]));
if (nextChar() != '\n')
throw parseException("end-of-line is missing");
skipLine();
}
/*
* (non-Javadoc)
*
* @see org.graphstream.util.parser.Parser#next()
*/
public boolean next() throws IOException, ParseException {
int c;
String nodeId;
String edgeId, source, target;
lastDirective = directive();
switch (lastDirective) {
case AN:
nodeId = id();
dgs.sendNodeAdded(sourceId, nodeId);
attributes(ElementType.NODE, nodeId);
break;
case CN:
nodeId = id();
attributes(ElementType.NODE, nodeId);
break;
case DN:
nodeId = id();
dgs.sendNodeRemoved(sourceId, nodeId);
break;
case AE:
edgeId = id();
source = id();
skipWhitespaces();
c = nextChar();
if (c != '<' && c != '>')
pushback(c);
target = id();
switch (c) {
case '>':
dgs.sendEdgeAdded(sourceId, edgeId, source, target, true);
break;
case '<':
dgs.sendEdgeAdded(sourceId, edgeId, target, source, true);
break;
default:
dgs.sendEdgeAdded(sourceId, edgeId, source, target, false);
break;
}
attributes(ElementType.EDGE, edgeId);
break;
case CE:
edgeId = id();
attributes(ElementType.EDGE, edgeId);
break;
case DE:
edgeId = id();
dgs.sendEdgeRemoved(sourceId, edgeId);
break;
case CG:
attributes(ElementType.GRAPH, null);
break;
case ST:
// TODO release 1.2 : read timestamp
// Version for 1.2 :
// --------------------------------
// long step;
// step = timestamp();
// sendStepBegins(sourceId, ste);
double step;
step = Double.valueOf(id());
dgs.sendStepBegins(sourceId, step);
break;
case CL:
dgs.sendGraphCleared(sourceId);
break;
case TF:
// TODO for release 1.2
// String tf;
// tf = string();
// try {
// dateIO.setFormat(tf);
// } catch (Exception e) {
// throw parseException("invalid time format \"%s\"", tf);
// }
break;
case EOF:
return false;
}
skipWhitespaces();
c = nextChar();
if (c == '#') {
skipLine();
return true;
}
if (c < 0)
return false;
if (c != '\n')
throw parseException("eol expected, got '%c'", c);
return true;
}
public boolean nextStep() throws IOException, ParseException {
boolean r;
Token next;
do {
r = next();
next = directive();
if (next != Token.EOF) {
pushback(next.name().charAt(1));
pushback(next.name().charAt(0));
}
} while (next != Token.ST && next != Token.EOF);
return r;
}
protected void attributes(ElementType type, String id) throws IOException,
ParseException {
int c;
skipWhitespaces();
while ((c = nextChar()) != '\n' && c != '#' && c >= 0) {
pushback(c);
attribute(type, id);
skipWhitespaces();
}
pushback(c);
}
protected void attribute(ElementType type, String elementId)
throws IOException, ParseException {
String key;
Object value = null;
int c;
AttributeChangeEvent ch = AttributeChangeEvent.CHANGE;
skipWhitespaces();
c = nextChar();
if (c == '+')
ch = AttributeChangeEvent.ADD;
else if (c == '-')
ch = AttributeChangeEvent.REMOVE;
else
pushback(c);
key = id();
if (key == null)
throw parseException("attribute key expected");
if (ch != AttributeChangeEvent.REMOVE) {
skipWhitespaces();
c = nextChar();
if (c == '=' || c == ':') {
skipWhitespaces();
value = value(true);
} else {
value = Boolean.TRUE;
pushback(c);
}
}
dgs.sendAttributeChangedEvent(sourceId, elementId, type, key, ch, null,
value);
}
protected Object value(boolean array) throws IOException, ParseException {
int c;
LinkedList<Object> l = null;
Object o;
do {
skipWhitespaces();
c = nextChar();
pushback(c);
switch (c) {
case '\'':
case '\"':
o = string();
break;
case '#':
o = color();
break;
case ARRAY_OPEN:
//
// Skip ARRAY_OPEN
nextChar();
//
skipWhitespaces();
o = value(true);
skipWhitespaces();
//
// Check if next char is ARRAY_CLOSE
if (nextChar() != ARRAY_CLOSE)
throw parseException("'%c' expected", ARRAY_CLOSE);
//
if (!o.getClass().isArray())
o = new Object[] { o };
break;
case MAP_OPEN:
o = map();
break;
default: {
String word = id();
if (word == null)
throw parseException("missing value");
if ((c >= '0' && c <= '9') || c == '-') {
try {
if (word.indexOf('.') > 0)
o = Double.valueOf(word);
else {
try {
o = Integer.valueOf(word);
} catch (NumberFormatException e) {
o = Long.valueOf(word);
}
}
} catch (NumberFormatException e) {
throw parseException("invalid number format '%s'", word);
}
} else {
if (word.equalsIgnoreCase("true"))
o = Boolean.TRUE;
else if (word.equalsIgnoreCase("false"))
o = Boolean.FALSE;
else
o = word;
}
break;
}
}
c = nextChar();
if (l == null && array && c == ',') {
l = new LinkedList<Object>();
l.add(o);
} else if (l != null)
l.add(o);
} while (array && c == ',');
pushback(c);
if (l == null)
return o;
return l.toArray();
}
protected Color color() throws IOException, ParseException {
int c;
int r, g, b, a;
StringBuilder hexa = new StringBuilder();
c = nextChar();
if (c != '#')
throw parseException("'#' expected");
for (int i = 0; i < 6; i++) {
c = nextChar();
if ((c >= 0 && c <= '9') || (c >= 'a' && c <= 'f')
|| (c >= 'A' && c <= 'F'))
hexa.appendCodePoint(c);
else
throw parseException("hexadecimal value expected");
}
r = Integer.parseInt(hexa.substring(0, 2), 16);
g = Integer.parseInt(hexa.substring(2, 4), 16);
b = Integer.parseInt(hexa.substring(4, 6), 16);
c = nextChar();
if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')
|| (c >= 'A' && c <= 'F')) {
hexa.appendCodePoint(c);
c = nextChar();
if ((c >= 0 && c <= '9') || (c >= 'a' && c <= 'f')
|| (c >= 'A' && c <= 'F'))
hexa.appendCodePoint(c);
else
throw parseException("hexadecimal value expected");
a = Integer.parseInt(hexa.substring(6, 8), 16);
} else {
a = 255;
pushback(c);
}
return new Color(r, g, b, a);
}
protected Object array() throws IOException, ParseException {
int c;
LinkedList<Object> array = new LinkedList<Object>();
c = nextChar();
if (c != ARRAY_OPEN)
throw parseException("'%c' expected", ARRAY_OPEN);
skipWhitespaces();
c = nextChar();
while (c != ARRAY_CLOSE) {
pushback(c);
array.add(value(false));
skipWhitespaces();
c = nextChar();
if (c != ARRAY_CLOSE && c != ',')
throw parseException("'%c' or ',' expected, got '%c'",
ARRAY_CLOSE, c);
if (c == ',') {
skipWhitespaces();
c = nextChar();
}
}
if (c != ARRAY_CLOSE)
throw parseException("'%c' expected", ARRAY_CLOSE);
return array.toArray();
}
protected Object map() throws IOException, ParseException {
int c;
HashMap<String, Object> map = new HashMap<String, Object>();
String key;
Object value;
c = nextChar();
if (c != MAP_OPEN)
throw parseException("'%c' expected", MAP_OPEN);
c = nextChar();
while (c != MAP_CLOSE) {
pushback(c);
key = id();
if (key == null)
throw parseException("id expected here, '%c'", c);
skipWhitespaces();
c = nextChar();
if (c == '=' || c == ':') {
skipWhitespaces();
value = value(false);
} else {
value = Boolean.TRUE;
pushback(c);
}
map.put(key, value);
skipWhitespaces();
c = nextChar();
if (c != MAP_CLOSE && c != ',')
throw parseException("'%c' or ',' expected, got '%c'",
MAP_CLOSE, c);
if (c == ',') {
skipWhitespaces();
c = nextChar();
}
}
if (c != MAP_CLOSE)
throw parseException("'%c' expected", MAP_CLOSE);
return map;
}
protected Token directive() throws IOException, ParseException {
int c1, c2;
//
// Skip comment and empty lines
//
do {
c1 = nextChar();
if (c1 == '#')
skipLine();
if (c1 < 0)
return Token.EOF;
} while (c1 == '#' || c1 == '\n');
c2 = nextChar();
if (c1 >= 'A' && c1 <= 'Z')
c1 -= 'A' - 'a';
if (c2 >= 'A' && c2 <= 'Z')
c2 -= 'A' - 'a';
switch (c1) {
case 'a':
if (c2 == 'n')
return Token.AN;
else if (c2 == 'e')
return Token.AE;
break;
case 'c':
switch (c2) {
case 'n':
return Token.CN;
case 'e':
return Token.CE;
case 'g':
return Token.CG;
case 'l':
return Token.CL;
}
break;
case 'd':
if (c2 == 'n')
return Token.DN;
else if (c2 == 'e')
return Token.DE;
break;
case 's':
if (c2 == 't')
return Token.ST;
break;
case 't':
if (c1 == 'f')
return Token.TF;
break;
}
throw parseException("unknown directive '%c%c'", c1, c2);
}
protected String string() throws IOException, ParseException {
int c, s;
StringBuilder builder;
boolean slash;
slash = false;
builder = new StringBuilder();
c = nextChar();
if (c != '\"' && c != '\'')
throw parseException("string expected");
s = c;
while ((c = nextChar()) != s || slash) {
if (slash && c != s)
builder.append("\\");
slash = c == '\\';
if (!slash) {
if (!Character.isValidCodePoint(c))
throw parseException("invalid code-point 0x%X", c);
builder.appendCodePoint(c);
}
}
return builder.toString();
}
protected String id() throws IOException, ParseException {
int c;
StringBuilder builder = new StringBuilder();
skipWhitespaces();
c = nextChar();
pushback(c);
if (c == '\"' || c == '\'') {
return string();
} else {
boolean stop = false;
while (!stop) {
c = nextChar();
switch (Character.getType(c)) {
case Character.LOWERCASE_LETTER:
case Character.UPPERCASE_LETTER:
case Character.DECIMAL_DIGIT_NUMBER:
break;
case Character.DASH_PUNCTUATION:
if (c != '-')
stop = true;
break;
case Character.MATH_SYMBOL:
if (c != '+')
stop = true;
break;
case Character.CONNECTOR_PUNCTUATION:
if (c != '_')
stop = true;
break;
case Character.OTHER_PUNCTUATION:
if (c != '.')
stop = true;
break;
default:
stop = true;
break;
}
if (!stop)
builder.appendCodePoint(c);
}
pushback(c);
}
if (builder.length() == 0)
return null;
return builder.toString();
}
/*
* protected long timestamp() throws IOException, ParseException { int c;
* String time;
*
* c = nextChar(); pushback(c);
*
* switch (c) { case '"': case '\'': time = string(); break; default:
* StringBuilder builder = new StringBuilder();
*
* while ((c = nextChar()) != '\n' && c != '"') builder.appendCodePoint(c);
*
* pushback(c); time = builder.toString(); break; }
*
* pushback(c); return dateIO.parse(time).getTimeInMillis(); }
*/
protected ParseException parseException(String message, Object... args) {
return new ParseException(String.format(String.format(
"parse error at (%d;%d) : %s", line, column, message), args));
}
}