// 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.diagram;
import java.awt.Color;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.sf.sdedit.Constants;
import net.sf.sdedit.config.Configuration;
import net.sf.sdedit.drawable.Arrow;
import net.sf.sdedit.drawable.Drawable;
import net.sf.sdedit.drawable.Fragment;
import net.sf.sdedit.drawable.Text;
import net.sf.sdedit.error.SemanticError;
import net.sf.sdedit.error.SyntaxError;
import net.sf.sdedit.message.Answer;
import net.sf.sdedit.message.BroadcastMessage;
import net.sf.sdedit.message.ForwardMessage;
import net.sf.sdedit.message.Message;
import net.sf.sdedit.util.Bijection;
/**
* This class encapsulates the data for and the process of the generation of a
* sequence diagram. That process consists - roughly spoken - of these steps:
*
* <ol>
* <li>Read object specifications and create lifelines for them</li>
* <li>For each message specification:</li>
* <ol>
* <li>Find the lifeline objects that correspond to the sender and the receiver
* of the message.</li>
* <li>If activating the sender implies sending answers, send these answers
* (which are taken from the answer message stack), so create arrows for them
* and add them to the PaintDevice. This is applied to all of the answers that
* lie above the answer with the sender as a receiver. </li>
* <li>Create a message object representing the message</li>
* <li>Add a message arrow to the PaintDevice</li>
* <li>Get the corresponding answer message and push it onto the answer message
* stack</li>
* </ol>
* <li>Pop all remaining answers from the stack and draw the arrows</li>
* </ol>
*
* When that process has finished, the image of the diagram display can be shown
* on the user interface.
*
* @sequence.diagram
*
* diag:Diagram ddp:DiagramDataProvider pd:PaintDevice /ll:Lifeline
* head:Drawable line:Drawable /msg:Message arrow:Drawable gui:GUI[p]
*
* [c:loop for all object specifications] diag:spec=ddp.read diag:ll.new
* diag:head=ll.getHead() diag:pd.add(ll) diag:line=ll.getView()
* diag:pd.add(line) [/c] [c:loop for all message specifications]
* diag:spec=ddp.read diag:msg.new diag:arrow=msg.getArrow() diag:pd.add(arrow)
* [/c] diag:pd.computeAxes pd:ll.setLeft(...)
* pd:arrow.setLeft(...),setWidth(...) gui:pd.draw(g2d) [c:loop for all drawable
* objects in clip] pd:arrow.draw(g2d) pd:line.draw(g2d) [/c]
*
* @author Markus Strauch
*/
public final class Diagram implements Constants {
/**
* Maps names of objects onto their root lifelines.
*/
private final Map<String, Lifeline> lifelineMap;
/**
* A list of root lifelines.
*/
private final List<Lifeline> lifelineList;
/**
* The current vertical position. For all messages that have already been
* read, the corresponding drawable objects' bottoms are above this
* position.
*/
private int verticalPosition;
/**
* We read the object and message specifications from a DiagramDataProvider.
*/
private final DiagramDataProvider provider;
/**
* The configuration object.
*/
private final Configuration conf;
/**
* A list of stacks, one for each thread, consisting of the answers that are
* yet to be sent.
*/
private final ArrayList<LinkedList<Message>> threadStacks;
/**
* first.get(i) is the lifeline belonging to the object that sends the first
* message on thread i.
*/
private final ArrayList<Lifeline> first;
/**
* The container for the drawable objects created for messages etc.
*/
private final PaintDevice paintDevice;
/**
* Maps a drawable object onto the state the DiagramDataProvider was in when
* the specification reflected by the drawable objects was read.
*/
private final Bijection<Drawable, Object> drawableBijection;
/**
* The number of the thread where the current message is processed.
*/
private int callerThread;
/**
* Flag denoting if the diagram is multithreaded.
*/
private final boolean threaded;
/**
* The list of thread states.
*/
private final ArrayList<String> threadStates;
/**
* Responsible for creating notes from data coming in from the
* DiagramDataProvider and for managing these notes.
*/
private final NoteManager noteManager;
/**
* Maps a lifeline name onto a map that maps a lifeline mnemonic onto the
* corresponding lifeline.
*/
private final Map<String, Map<String, Lifeline>> mnemonicMap;
private final FragmentManager fragmentManager;
private final MessageProcessor processor;
private boolean finished;
// These attributes are here for performance reasons
// The JIT cannot inline calls to the corresponding configuration
// get methods because the configuration object is synthesized
public final int arrowSize;
public final int messagePadding;
public final int subLifelineWidth;
public final int mainLifelineWidth;
public final int messageLabelSpace;
public final boolean returnArrowVisible;
public final Color[] threadColors;
/**
* Creates a new diagram that is to be generated based on the data delivered
* by the given <tt>DiagramDataProvider</tt>.
*
* @param configuration
* the configuration of the diagram
* @param provider
* for reading the object and message specifications
* @param paintDevice
* for storing and drawing the boxes, arrows etc.
*/
public Diagram(Configuration configuration, DiagramDataProvider provider,
PaintDevice paintDevice) {
arrowSize = configuration.getArrowSize();
messagePadding = configuration.getMessagePadding();
subLifelineWidth = configuration.getMessagePadding();
mainLifelineWidth = configuration.getMainLifelineWidth();
messageLabelSpace = configuration.getMessageLabelSpace();
returnArrowVisible = configuration.isReturnArrowVisible();
this.paintDevice = paintDevice;
lifelineMap = new HashMap<String, Lifeline>();
lifelineList = new ArrayList<Lifeline>();
conf = configuration;
paintDevice.setDiagram(this);
verticalPosition = 0;
first = new ArrayList<Lifeline>();
this.provider = provider;
provider.setDiagram(this);
threadStacks = new ArrayList<LinkedList<Message>>();
threadStates = new ArrayList<String>();
drawableBijection = new Bijection<Drawable, Object>();
this.threaded = conf.isThreaded();
if (!threaded) {
/* spawn the only single thread */
callerThread = spawnThread();
}
mnemonicMap = new HashMap<String, Map<String, Lifeline>>();
noteManager = new NoteManager(this);
fragmentManager = new FragmentManager(this);
processor = new MessageProcessor(this);
finished = false;
threadColors = new Color[] { configuration.getTc0(),
configuration.getTc1(), configuration.getTc2(),
configuration.getTc3(), configuration.getTc4(),
configuration.getTc5(), configuration.getTc6(),
configuration.getTc7(), configuration.getTc8(),
configuration.getTc9(), };
}
/**
* Generates the diagram, based on the data of the
* <tt>DiagramDataProvider</tt> passed to the constructor.
*
* @throws SyntaxError
* if a message or object specification is syntactically wrong
* @throws SemanticError
* if a message or object specification is semantically wrong
*/
public void generate() throws SemanticError, SyntaxError {
Fragment frame = null;
Text text = null;
String title = provider.getTitle();
String description[] = provider.getDescription();
if (description != null) {
text = new Text(description, paintDevice);
text.setTop(conf.getUpperMargin());
text.setLeft(conf.getLeftMargin());
verticalPosition = text.getBottom() + 3;
} else {
verticalPosition = conf.getUpperMargin();
}
if (title != null) {
frame = new Fragment(title, "", this);
frame.setTop(verticalPosition);
verticalPosition += frame.getLabelHeight() + 5;
}
readObjects();
if (lifelineList.isEmpty()) {
return;
}
paintDevice.reinitialize();
try {
for (Lifeline lifeline : lifelineList) {
paintDevice.addOtherDrawable(lifeline.getHead());
verticalPosition = Math.max(verticalPosition, lifeline
.getHead().getTop()
+ lifeline.getHead().getHeight());
}
for (Lifeline lifeline : lifelineList) {
lifeline.getView().setBottom(verticalPosition);
}
extendLifelines(conf.getInitialSpace());
for (Lifeline lifeline : lifelineList) {
if (lifeline.hasThread()) {
lifeline.setActive(true);
}
}
readMessages();
} finally {
fragmentManager.finishFragments();
if (getNumberOfLifelines() > 0) {
paintDevice.computeAxes(conf.getLeftMargin() + 6
+ getLifelineAt(0).getHead().getWidth() / 2);
paintDevice.computeBounds();
// fixes bug 2019730 (notes appear outside of diagram)
for (Lifeline lifeline : getAllLifelines()) {
noteManager.closeNote(lifeline.getName());
}
//
noteManager.computeArrowAssociations();
if (frame != null) {
frame.setLeft(conf.getLeftMargin());
frame.setRight(paintDevice.getWidth()
- conf.getRightMargin() + 6);
frame.setBottom(verticalPosition + 4);
paintDevice.addOtherDrawable(frame);
}
if (text != null) {
paintDevice.addOtherDrawable(text);
}
}
finished = true;
paintDevice.close();
}
}
public final boolean isFinished() {
return finished;
}
/**
* Reads lines in which objects are declared using the reader given, until
* an empty line is found. Creates lifelines for them. If all object
* declarations have been successfully read, draws them on the diagram.
*
* @throws SyntaxError
* if an object declaration is not well-formed
* @throws SemanticError
* if an object is declared twice
*/
private void readObjects() throws SyntaxError, SemanticError {
while (provider.advance()) {
addObject(provider.nextObject());
}
for (Lifeline l : provider.getLaterObjects()) {
addObject(l);
}
}
private void addObject(Lifeline lifeline) throws SemanticError {
if (lifeline == null) {
return;
}
if (lifeline.isActiveObject() && !threaded) {
throw new SemanticError(provider,
"v flag for active object cannot be set when multithreading is disabled");
}
if (provider.getState() != null) {
drawableBijection.add(lifeline.getHead(), provider.getState());
}
addLifeline(lifeline);
}
/**
* Reads lines in which messages exchanged between objects are described.
* Draws for each message a corresponding arrow (and before that, possibly
* several return arrows) on the diagram just after the message has been
* successfully read. Stops when there is nothing left to read.
*
* @param draw
* flag denoting if message arrows are to be drawn
* @throws SyntaxError
* if a description of a message is not well-formed
* @throws SemanticError
* if a message between objects that are not existing or that
* are not active is described
*/
private void readMessages() throws SyntaxError, SemanticError {
while (provider.advance()) {
if (fragmentManager.readFragments()) {
continue;
}
if (!noteManager.step()) {
readNextMessage();
}
fragmentManager.clearSectionLabel();
}
finish();
for (Lifeline line : getLifelines()) {
if (!line.isAlwaysActive()) {
line.terminate();
}
}
}
private void readNextMessage() throws SyntaxError, SemanticError {
MessageData data = provider.nextMessage();
noteManager.closeNote(data.getCaller());
String[] callees = data.getCallees();
if (callees.length == 1) {
throw new SyntaxError(provider,
"A broadcast message must have at least two receivers");
}
if (callees.length >= 2) {
if (data.isSpawnMessage()) {
throw new SyntaxError(provider,
"Broadcast messages are spawning by default");
}
Set<String> calleeSet = new HashSet<String>();
Lifeline[] allButLast = new Lifeline[callees.length - 1];
for (int i = 0; i < callees.length; i++) {
String callee = callees[i];
if (callee.length() == 0) {
throw new SyntaxError (provider,
"Malformed broadcast message");
}
if (!calleeSet.add(callee)) {
throw new SyntaxError(provider,
"Duplicate receiver: " + callee);
}
if (callee.equals(data.getCaller())) {
throw new SyntaxError(provider, "The sender "
+ callee + " cannot be a "
+ "receiver of the broadcast message");
}
noteManager.closeNote(callee);
MessageData part = new MessageData();
// TODO mnemonics
part.setCaller(data.getCaller());
part.setCallee(callee);
part.setLevel(data.getLevel());
part.setThread(data.getThread());
if (getLifeline(data.getCaller()) != null) {
if (!getLifeline(data.getCaller()).isAlwaysActive()) {
part.setSpawnMessage(true);
}
}
part.setReturnsInstantly(data.returnsInstantly());
if (i == 0) {
part.setNoteNumber(data.getNoteNumber());
part.setMessage(data.getMessage());
part.setBroadcastType(BroadcastMessage.FIRST);
} else if (i == callees.length - 1) {
part.setBroadcastType(BroadcastMessage.LAST);
} else {
part.setBroadcastType(BroadcastMessage.OTHER);
}
BroadcastMessage msg = (BroadcastMessage) processor
.processMessage(part);
if (i < callees.length - 1) {
allButLast[i] = msg.getCallee();
} else {
msg.setOtherCallees(allButLast);
}
processor.execute(msg);
}
} else {
noteManager.closeNote(data.getCallee());
ForwardMessage msg = processor.processMessage(data);
processor.execute(msg);
}
fragmentManager.clearLabels();
}
public int getNextFreeNoteNumber() {
return noteManager.getNextFreeNoteNumber();
}
public FragmentManager getFragmentManager() {
return fragmentManager;
}
/**
* Returns the state, represented by an Object, the
* <tt>DiagramDataProvider</tt> used by this Diagram was in when the data
* from which the given Drawable object has been created, was read.
*
* @param drawable
* a drawable corresponding to some data
* @return the state the DiagramDataProvider was in when the data has been
* provided
*/
public Object getStateForDrawable(Drawable drawable) {
if (drawable instanceof Arrow) {
Message msg = ((Arrow) drawable).getMessage();
if (msg instanceof Answer) {
drawable = ((Answer) msg).getForwardMessage().getArrow();
}
}
return drawableBijection.getImage(drawable);
}
public Drawable getDrawableForState(Object state) {
return drawableBijection.getPreImage(state);
}
/**
* Adds a new (root) lifeline for an object with the name and type given.
*/
private boolean addLifeline(Lifeline lifeline) throws SemanticError {
if (lifelineMap.get(lifeline.getName()) != null) {
throw new SemanticError(provider, lifeline.getName()
+ " already exists");
}
if (lifeline.hasThread()) {
if (!threaded) {
throw new SemanticError(
provider,
lifeline.getName()
+ " cannot have its own thread when multithreading is not enabled");
}
int thread = spawnThread();
first.set(thread, lifeline);
lifeline.setThread(thread);
}
lifelineList.add(lifeline);
lifelineMap.put(lifeline.getName(), lifeline);
return true;
}
public final void extendLifelines(final int amount) {
for (final Lifeline lifeline : getLifelines()) {
if (lifeline.isAlive()) {
for (final Lifeline line : lifeline.getAllLifelines()) {
line.getView().extend(amount);
}
}
}
verticalPosition += amount;
}
int getPositionOf(Lifeline lifeline) {
return lifelineList.indexOf(lifeline.getRoot());
}
public boolean isThreaded() {
return threaded;
}
public int getCallerThread() {
return callerThread;
}
public void setCallerThread(int callerThread) {
this.callerThread = callerThread;
}
public PaintDevice getPaintDevice() {
return paintDevice;
}
/**
* Returns the text handler that reads the text line by line and creates
* objects and message data from them.
*
* @return the text handler that reads the text line by line and creates
* messages and objects from them
*/
public DiagramDataProvider getDataProvider() {
return provider;
}
/**
* Returns the diagram configuration
*
* @return the diagram configuration
*/
public Configuration getConfiguration() {
return conf;
}
/**
* Returns a collection of the (not yet destroyed) lifelines appearing in
* the diagram.
*
* @return a collection of the (not yet destroyed) lifelines appearing in
* the diagram
*/
public Collection<Lifeline> getLifelines() {
return lifelineMap.values();
}
/**
* Returns a collection of all lifelines, whether visible or not, and
* whether already destroyed or not.
*
* @return a collection of all lifelines, whether visible or not, and
* whether already destroyed or not
*/
public Collection<Lifeline> getAllLifelines() {
return lifelineList;
}
/**
* Removes a lifeline, denoted by its name, from the diagram, including all
* of its sub lifelines.
*
* @param name
* the name of the object of which the lifeline is to be removed
*/
public void removeLifeline(String name) {
if (lifelineMap.remove(name) == null) {
throw new IllegalArgumentException("lifeline " + name
+ " should be removed, but does not exist");
}
}
LinkedList<Message> currentStack() {
return threadStacks.get(callerThread);
}
Lifeline firstCaller() {
return first.get(callerThread);
}
void setFirstCaller(Lifeline caller) {
first.set(callerThread, caller);
}
int spawnThread() {
int num = threadStacks.size();
threadStacks.add(new LinkedList<Message>());
first.add(null);
threadStates.add("new");
return num;
}
boolean noThreadIsSpawned() {
return threadStacks.isEmpty();
}
String threadState() {
return threadStates.get(callerThread);
}
void setThreadState(String state) {
threadStates.set(callerThread, state);
}
void deleteStack() {
threadStacks.set(callerThread, null);
first.set(callerThread, null);
}
void finish(int thread) {
LinkedList<Message> threadStack = threadStacks.get(thread);
while (threadStack != null && !threadStack.isEmpty()) {
Message answer = threadStack.removeLast();
sendAnswer(answer);
}
Lifeline firstLifeline = first.get(thread);
if (firstLifeline != null) {
firstLifeline.finish();
if (firstLifeline.getRoot() != firstLifeline) {
firstLifeline.dispose();
}
}
}
public void sendAnswer(Message answer) {
sendAnswer(answer, false);
}
public void sendAnswer(Message answer, boolean removeFromStack) {
if (removeFromStack) {
int thread = answer.getThread();
threadStacks.get(thread).removeLast();
}
noteManager.closeNote(answer.getCaller().getName());
noteManager.closeNote(answer.getCallee().getName());
answer.updateView();
}
/**
* All pending answers are sent back to the callers, all lifelines become
* inactive.
*/
public void finish() {
for (int t = 0; t < threadStacks.size(); t++) {
finish(t);
}
for (Lifeline line : getLifelines()) {
if (line != null && !(line.isAlwaysActive()) && line.isActive()) {
line.finish();
}
}
}
public Lifeline getLifelineAt(int position) {
return lifelineList.get(position);
}
public Lifeline getLifeline(String name) {
return lifelineMap.get(name);
}
public int getNumberOfLifelines() {
return lifelineList.size();
}
void setVerticalPosition(int verticalPosition) {
this.verticalPosition = verticalPosition;
}
public int getVerticalPosition() {
return verticalPosition;
}
public int getNumberOfThreads() {
return threadStacks.size();
}
void addToStateMap(Drawable drawable, Object state) {
drawableBijection.add(drawable, state);
}
public Lifeline getLifelineByMnemonic(String lifelineName, String mnemonic) {
Map<String, Lifeline> map = mnemonicMap.get(lifelineName);
return map == null ? null : map.get(mnemonic);
}
public void associateLifeline(String lifelineName, String mnemonic,
Lifeline lifeline) throws SemanticError {
Map<String, Lifeline> map = mnemonicMap.get(lifelineName);
if (map == null) {
map = new HashMap<String, Lifeline>();
mnemonicMap.put(lifelineName, map);
}
if (map.put(mnemonic, lifeline) != null) {
throw new SemanticError(provider, "The mnemonic \"" + mnemonic
+ "\" is already defined for the lifeline " + "\""
+ lifeline.getName());
}
}
public void associateMessage(int number, Message msg) {
noteManager.associateMessage(number, msg);
}
public void clearMnemonic(Lifeline lifeline) {
String mnemonic = lifeline.getMnemonic();
if (mnemonic == null) {
return;
}
Map<String, Lifeline> map = mnemonicMap.get(lifeline.getName());
map.remove(mnemonic);
}
public void toggleWaitingStatus(int thread) {
for (Lifeline lifeline : getLifelines(thread)) {
lifeline.toggleWaitingStatus();
}
}
private Set<Lifeline> getLifelines(int thread) {
Set<Lifeline> lifelines = new HashSet<Lifeline>();
Lifeline firstCaller = first.get(thread);
if (!firstCaller.isAlwaysActive()) {
lifelines.add(firstCaller);
}
for (Message msg : threadStacks.get(thread)) {
lifelines.add(msg.getCaller());
if (msg.getCallee() != null) {
lifelines.add(msg.getCallee());
}
}
return lifelines;
}
}