// Copyright (c) 2006 - 2008, Markus Strauch.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
package net.sf.sdedit.text;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import net.sf.sdedit.diagram.Diagram;
import net.sf.sdedit.diagram.DiagramDataProvider;
import net.sf.sdedit.diagram.Lifeline;
import net.sf.sdedit.diagram.MessageData;
import net.sf.sdedit.drawable.Note;
import net.sf.sdedit.error.SyntaxError;
import net.sf.sdedit.util.Grep;
import net.sf.sdedit.util.Pair;
/**
* A <tt>DiagramDataProvider</tt> implementation, reading a diagram
* specification from a single string.
*
* @author Markus Strauch
*
*/
public class TextHandler implements DiagramDataProvider {
private BufferedReader reader;
private String text;
private String rawLine;
private String currentLine;
private int lineBegin;
private int lineEnd;
/* -1 = init, 0 = objects, 1 = messages */
private int section;
private Diagram diagram;
private int lineNumber;
private String title;
private String[] description;
private Map<Lifeline, String> annotations;
private boolean parseProperties;
/**
* Creates a new <tt>TextHandler</tt> for the given text.
*
* @param text
* a diagram specification
*
*/
public TextHandler(String text) {
section = -1;
lineBegin = 0;
lineEnd = -1;
lineNumber = 0;
this.text = text;
annotations = new HashMap<Lifeline, String>();
reset();
}
/**
* Sets the diagram instance that corresponds to the specification read by
* this <tt>TextHandler</tt>. This method is called inside
* {@linkplain Diagram#generate()}.
*
* @param diagram
* the diagram that corresponds to the specification read by this
* <tt>TextHandler</tt>
*/
public void setDiagram(Diagram diagram) {
this.diagram = diagram;
parseProperties = diagram.getConfiguration().isAllowMessageProperties();
}
/**
* Returns the index of the first position of the current line in the
* specification.
*
* @return the index of the first position of the current line in the
* specification
*/
public int getLineBegin() {
return lineBegin;
}
public Object getState() {
return lineBegin;
}
public int getLineNumber() {
return lineNumber;
}
public String getText() {
return text;
}
/**
* Returns the index of the last position of the current line in the
* specification string.
*
* @return the index of the last position of the current line in the
* specification string
*/
public int getLineEnd() {
return lineEnd;
}
/**
* Returns the line that is currently read.
*
* @return the line that is currently read
*/
public String getCurrentLine() {
return currentLine;
}
/**
* Resets the text handler so objects and messages can be read once again.
*/
private void reset() {
// TODO this is a chance for optimization
String[] titleString = Grep.parse("(?s).*#!\\[([^\n\r]*?)\\].*", text);
if (titleString == null) {
title = null;
} else {
title = titleString[0];
}
String[] descString = Grep.parse("(?s).*#!>>(.*)#!<<.*", text);
if (descString == null) {
description = null;
} else {
description = descString[0].trim().split("\n");
}
for (int i = 0; description != null && i < description.length; i++) {
description[i] = description[i].trim();
if (!description[i].startsWith("#!")) {
description = null;
} else {
description[i] = description[i].replaceFirst("#!", "");
}
}
reader = new BufferedReader(new StringReader(text));
section = -1;
lineBegin = 0;
lineEnd = -1;
currentLine = null;
rawLine = null;
annotations.clear();
}
/**
* @see net.sf.sdedit.diagram.DiagramDataProvider#getTitle()
*/
public String getTitle() {
return title;
}
public String[] getDescription() {
return description;
}
/**
* Reads the next line in the specification. If another line is present, the
* next object reflecting the current line will be returned by a call to
* {@linkplain #nextObject()}, {@linkplain #nextMessage()},
* {@linkplain #getNote()}, {@linkplain #openFragment()} or
* {@linkplain #getEventAssociation()}.
*
* @return flag denoting if another object or message line could be read
*/
public boolean advance() {
return advance(true);
}
private boolean advance(boolean ignoreEmptyLines) {
if (reader == null) {
throw new IllegalStateException("null reader");
}
try {
if (!reader.ready()) {
section = 1;
return false;
}
String line;
do {
lineBegin = lineEnd + 2;
rawLine = line = reader.readLine();
lineNumber++;
if (line == null) {
section = 1;
return false;
}
lineEnd = lineBegin + line.length() - 1;
if (ignoreEmptyLines) {
line = line.trim();
}
if (section == -1 && (line.equals("") || line.startsWith("#"))) {
continue;
}
section = Math.max(0, section);
if (section == 0) {
int cmt = line.indexOf("#");
if (cmt >= 0) {
line = line.substring(0, cmt).trim();
}
}
if (section == 0 && line.equals("")) {
section = 1;
return false;
}
} while (ignoreEmptyLines
&& (line.equals("") || line.startsWith("#")));
currentLine = line;
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public String openFragment() {
if (section == 0) {
throw new IllegalStateException("not all objects have been read");
}
if (currentLine == null) {
throw new IllegalStateException("nothing to read");
}
if (currentLine.startsWith("//")) {
System.err
.println("Warning: Comments starting with // are deprecated. Use [c:<type> <text>]...[/c].");
return currentLine.substring(2).trim();
}
if (currentLine.endsWith("]") && currentLine.startsWith("[c")) {
return currentLine;
}
return null;
}
public String getFragmentSeparator() {
if (section == 0) {
throw new IllegalStateException("not all objects have been read");
}
if (currentLine == null) {
throw new IllegalStateException("nothing to read");
}
if (currentLine.startsWith("--")) {
return currentLine.substring(2);
}
return null;
}
public boolean closeFragment() {
if (section == 0) {
throw new IllegalStateException("not all objects have been read");
}
if (currentLine == null) {
throw new IllegalStateException("nothing to read");
}
return currentLine.equals("\\\\") || currentLine.endsWith("]")
&& currentLine.startsWith("[/c");
}
/**
* Returns the {@linkplain MessageData} object made from the current line.
*
* @return the {@linkplain MessageData} object made from the current line
* @throws SyntaxError
* if the next message cannot be parsed
*/
public MessageData nextMessage() throws SyntaxError {
if (section == 0) {
throw new IllegalStateException("not all objects have been read");
}
if (currentLine == null) {
throw new IllegalStateException("nothing to read");
}
MessageData data;
try {
data = new TextBasedMessageData(currentLine);
} catch (SyntaxError e) {
e.setProvider(this);
throw e;
}
currentLine = null;
return data;
}
public Collection<Lifeline> getLaterObjects() throws SyntaxError {
ArrayList<Lifeline> laterObjects = new ArrayList<Lifeline>();
if (!reader.markSupported()) {
System.out.println("WARNING: reader does not support marking");
return laterObjects;
}
String oldCurrentLine = currentLine;
String oldRawLine = rawLine;
StringBuffer oldLines = new StringBuffer();
try {
String line;
do {
line = reader.readLine();
if (line != null) {
if (line.startsWith("#newobj ")) {
currentLine = line.substring(8);
rawLine = currentLine;
laterObjects.add(nextObjectIntern());
}
oldLines.append(line);
oldLines.append('\n');
}
} while (line != null);
reader.close();
reader = new BufferedReader(new StringReader(oldLines.toString()));
} catch (IOException io) {
io.printStackTrace();
}
currentLine = oldCurrentLine;
rawLine = oldRawLine;
return laterObjects;
}
/**
* Returns the {@linkplain Lifeline} object made from the current line}.
*
* @return the {@linkplain Lifeline} object made from the current line
*
* @throws SyntaxError
* if the next object declaration is not well-formed
*/
public Lifeline nextObject() throws SyntaxError {
if (section == 1) {
throw new IllegalStateException(
"reading objects has already been finished");
}
return nextObjectIntern();
}
private Lifeline nextObjectIntern() throws SyntaxError {
if (currentLine == null) {
throw new IllegalStateException("nothing to read");
}
if (currentLine.indexOf(':') == -1) {
throw new SyntaxError(this,
"not a valid object declaration - ':' missing");
}
String[] parts = Grep.parse(
"(\\/?.+?):([^\\[\\]]+?)\\s*(\\[.*?\\]|)\\s*(\".*\"|)",
currentLine);
if (parts == null || parts.length != 4) {
String msg;
if (currentLine.indexOf('.') >= 0) {
msg = "not a valid object declaration, perhaps you forgot to "
+ "enter an empty line before the message section";
} else {
msg = "not a valid object declaration";
}
throw new SyntaxError(this, msg);
}
currentLine = null;
String name = parts[0];
String type = parts[1];
String flags = parts[2];
String label = parts[3];
if (!label.equals("")) {
label = label.substring(1, label.length() - 1);
}
boolean anon = flags.indexOf('a') >= 0;
boolean role = flags.indexOf('r') >= 0;
boolean active = flags.indexOf('v') >= 0;
boolean process = flags.indexOf('p') >= 0;
boolean hasThread = flags.indexOf('t') >= 0;
boolean autoDestroy = flags.indexOf('x') >= 0;
boolean external = flags.indexOf('e') >= 0;
Lifeline lifeline;
if (external && !process) {
throw new SyntaxError(this, "only processes can be declared external");
}
if (name.startsWith("/")) {
if (type.equals("Actor") || process) {
throw new SyntaxError(this, "processes and actors must be visible");
}
if (hasThread) {
throw new SyntaxError(this,
"invisible objects cannot have their own thread");
}
lifeline = new Lifeline(name.substring(1), type, label, false,
anon, role, active, false, false, autoDestroy, external, diagram);
} else {
if (type.equals("Actor")) {
if (anon) {
throw new SyntaxError(this, "actors cannot be anonymous");
}
}
if ((type.equals("Actor") || process) && hasThread) {
throw new SyntaxError(this,
"actors cannot have their own thread");
}
if ((type.equals("Actor") || process) && autoDestroy) {
throw new SyntaxError(this,
"actors cannot be (automatically) destroyed");
}
lifeline = new Lifeline(parts[0], parts[1], label, true, anon,
role, active, process, hasThread, autoDestroy, external, diagram);
}
int cmt = rawLine.indexOf("#!");
if (cmt >= 0 && cmt + 2 < rawLine.length() - 1) {
String annotation = rawLine.substring(cmt + 2).trim();
annotations.put(lifeline, annotation);
}
return lifeline;
}
public String getAnnotation(Lifeline lifeline) {
String name = annotations.get(lifeline);
return name;
}
/**
* If there is a note specified at the current line and the subsequent
* lines, a {@linkplain Note} representation is returned, otherwise
* <tt>null</tt>
*
* @return a note, if one is specified at the current position in text,
* otherwise <tt>null</tt>
*/
public Note getNote() throws SyntaxError {
if (section == 0) {
throw new IllegalStateException("not all objects have been read");
}
if (currentLine == null) {
throw new IllegalStateException("nothing to read");
}
String[] parts = Grep.parse("\\s*(\\*|\\+)(\\d+)\\s*(.+)", currentLine);
if (parts == null) {
return null;
}
boolean consuming = parts[0].equals("+");
int number = -1;
try {
number = Integer.parseInt(parts[1]);
} catch (NumberFormatException nfe) {
/* empty */
}
if (number < 0) {
throw new SyntaxError(this, "bad note number: " + parts[1]);
}
String obj = parts[2];
Lifeline line = diagram.getLifeline(obj);
if (line == null) {
throw new SyntaxError(this, obj + " does not exist");
}
int oldBegin = lineBegin;
int oldEnd = lineEnd;
line = line.getRightmost();
List<String> desc = new LinkedList<String>();
boolean more;
do {
if (!advance(false)) {
lineBegin = oldBegin;
lineEnd = oldEnd;
throw new SyntaxError(this, "The note is not closed.");
}
more = !currentLine.trim().equals(parts[0] + parts[1]);
} while (more && desc.add(currentLine));
if (desc.size() == 0) {
lineBegin = oldBegin;
lineEnd = oldEnd;
throw new SyntaxError(this, "The note is empty.");
}
String[] noteText = desc.toArray(new String[0]);
URI link = null;
if (noteText.length == 1) {
String linkString = noteText[0].trim();
if (linkString.startsWith("link:")) {
try {
linkString = linkString.substring(5).trim();
link = new URI(linkString);
if (link.getPath() == null) {
throw new SyntaxError(this, "Empty path in URI: "
+ linkString);
}
noteText[0] = link.getPath();
} catch (URISyntaxException e) {
throw new SyntaxError(this, "Bad URI syntax: "
+ e.getMessage());
}
}
}
Note note = new Note(line, number, noteText, consuming);
note.setLink(link);
return note;
}
/**
* If the current line specifies an association of a note to the current
* vertical position of a lifeline, this method returns a pair consisting of
* the lifeline and the note number.
*
* @return a pair of a lifeline and a note number, if the current line
* specifies and association between the note and the lifeline,
* otherwise <tt>null</tt>
*/
public Pair<Lifeline, Integer> getEventAssociation() throws SyntaxError {
if (section == 0) {
throw new IllegalStateException("not all objects have been read");
}
if (currentLine == null) {
throw new IllegalStateException("nothing to read");
}
String[] parts = Grep.parse("\\((\\d+)\\)\\s*(\\w+)", currentLine);
if (parts == null) {
return null;
}
int number = Integer.parseInt(parts[0]);
String obj = parts[1];
Lifeline line = diagram.getLifeline(obj);
if (line == null) {
throw new SyntaxError(this, obj + " does not exist");
}
return new Pair<Lifeline, Integer>(line, number);
}
}