/******************************************************************************* * Copyright (c) 2009 the CHISEL group and contributors. * 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: * Del Myers -- initial API and implementation *******************************************************************************/ package org.eclipse.zest.custom.sequence.widgets; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.eclipse.swt.SWT; /** * A helper class for quickly creating sequence charts without the need for a viewer * or label providers. Keeps an internal state of where in the chart the next message * will be placed. The user can question the builder for that state, and update it * as he or she pleases. The class will do its best to keep the chart consistent, but * some care must be taken when changing the state of the builder. * * @author Del Myers * */ public class SequenceChartBuilder { /** * The chart that we will build on. */ private UMLSequenceChart chart; /** * The activation that we are running on. */ private Activation currentActivation; /** * The index at which the next call/return will be made on the activation. */ private int index; /** * A mapping of message groups that have not yet gone out of scope. The will need to * be updated as the chart evolves. */ private Map<Activation, List<MessageGroup>> unscopedGroups; /** * The names of the lifelines that are used. */ private Map<String, Lifeline> lifelineNames; private boolean isRedrawing; /** * Creates a new sequence diagram on the given chart. The chart is cleared in order to * reset its state. * @param chart the chart to use. * @param start the name of the starting lifeline. */ public SequenceChartBuilder(UMLSequenceChart chart, String start) { lifelineNames = new HashMap<String, Lifeline>(); unscopedGroups = new HashMap<Activation, List<MessageGroup>>(); chart.clearChart(); Lifeline startLine = new Lifeline(chart); startLine.setData(start); startLine.setText(start); Activation root = new Activation(chart); root.setData("Start"); root.setText("Start"); root.setLifeline(startLine); lifelineNames.put(start, startLine); currentActivation = root; index = 0; chart.setRootActivation(root); this.chart = chart; isRedrawing = true; } /** * Turns off redrawing on the chart in order to avoid updates. */ public void turnOffRedraw() { if (isRedrawing) { chart.setRedraw(false); isRedrawing = false; } } /** * Turns on redrawing on the chart so that the chart will be updated. */ public void turnOnRedraw() { if (!isRedrawing) { chart.setRedraw(true); isRedrawing = true; } } /** * Returns the index at which the next call/return will be inserted. * @return the index at which the next call/return will be inserted. */ public int getIndex() { return index; } /** * Attempts to set the index of insertion to the given index. If the index is out-of-range, * it will be set to the nearest point that the chart will accept insertion. For convenience, * this number will be returned. * @param index the index to attempt to set the "cursor" to. * @return the index that was actually set. */ public int setIndex(int index) { Message[] messages = currentActivation.getMessages(); if (index > messages.length) { index = messages.length; } else if (index < 0) { index = 0; } this.index = index; return index; } /** * Creates a call with the given name at the current insertion index, to the * given life line. Does not follow the call to that lifeline, but rather increments * the index to insert another call. Message groups that have been created and * enclose the call have their sizes incremented. If there is no lifeline for the * given name, one is created. * @param callName the call to insert. * @param lifeline the lifeline to call. * @return the created call for convenience. */ public Call makeCall(String callName, String lifeline) { Lifeline line = lifelineNames.get(lifeline); if (line == null) { line = new Lifeline(chart); line.setText(lifeline); line.setData(lifeline); lifelineNames.put(lifeline, line); } Activation a = new Activation(chart); a.setText(callName); a.setData(callName); a.setLifeline(line); Call call = new Call(chart); call.setData(callName); call.setText(callName); currentActivation.addMessage(index, call, a); incrementGroups(); index++; return call; } /** * Sets the parent lifeline of the given lifeline to the new parent. * If the parent doesn't exist, then a new lifeline is created. * @param lifeline the name of the lifeline for which a container will be made. * The lifeline must already exist, or null will be returned. * @param parent the name of the new parent. * @return the parent lifeline for convenience, or null if the life line could * not be set. */ public Lifeline setContainer(String lifeline, String parent) { Lifeline line = lifelineNames.get(lifeline); if (line == null) { return null; } Lifeline parentLine = lifelineNames.get(parent); if (parentLine == null) { parentLine = new Lifeline(chart); parentLine.setText(parent); parentLine.setData(parent); lifelineNames.put(parent, parentLine); } parentLine.addChild(line); return parentLine; } private void incrementGroups() { MessageGroup[] groups = currentActivation.getMessageGroups(); for (MessageGroup g : groups) { if (g.getOffset() <= index && (g.getOffset() + g.getLength() >= index)) { if (index == g.getOffset() + g.getLength()) { //only increase the length if the group hasn't been closed. List<MessageGroup> contextGroups = unscopedGroups.get(currentActivation); if (contextGroups != null && contextGroups.contains(g)) { g.setRange(currentActivation, g.getOffset(), g.getLength()+1); } } else { g.setRange(currentActivation, g.getOffset(), g.getLength()+1); } } } } /** * Creates the given call and follows it to its destination. The insertion index is * reset to 0, and will begin on the target lifeline. * @param callName the call to insert. * @param lifeline the lifeline to call. * @return the created call for convenience. */ public Call followCall(String callName, String lifeline) { return followCall(makeCall(callName, lifeline)); } /** * Follows the given call to its destination, and sets the insertion index to 0. Returns * the call for convenience. * @param call the call to follow. * @return the call followed. */ public Call followCall(Call call) { currentActivation = call.getTarget(); index = 0; return call; } /** * Makes a return to the given activation if it is reachable from the current point. * This is used for fine-tuned control, and most often isn't necessary. It may be useful * when returning because of an exceptional case, however. Will fail and return null * if the activation isn't reachable. * @param to the activation to return to. * @return the created return, or null if the activation isn't reachable. */ public Return makeReturn(String returnName, Activation to) { Activation pointer = currentActivation; while (pointer != null) { if (pointer.getSourceCall() == null) { return null; } pointer = pointer.getSourceCall().getSource(); if (pointer == to) { Return r = new Return(chart); r.setLineStyle(SWT.LINE_DASH); r.setTargetStyle(Return.OPEN_ARROW); r.setText(returnName); r.setData(returnName); currentActivation.addMessage(index, r, pointer); incrementGroups(); index++; return r; } } return null; } /** * Makes a return from the current lifeline to the nearest point on the given lifeline. * @param returnName the name of the * @param lifeline * @return */ public Return makeReturn(String returnName, String lifeline) { Lifeline line = lifelineNames.get(lifeline); if (line == null) { return null; } Activation pointer = currentActivation; while (pointer != null) { if (pointer.getSourceCall() == null) { return null; } pointer = pointer.getSourceCall().getSource(); if (pointer.getLifeline() == line) { Return r = new Return(chart); r.setLineStyle(SWT.LINE_DASH); r.setTargetStyle(Return.OPEN_ARROW); r.setText(returnName); r.setData(returnName); currentActivation.addMessage(index, r, pointer); incrementGroups(); index++; return r; } } return null; } /** * Makes a return from the current location to the previous. * @param returnName the name to be displayed with the return. * @return the created return, if it could be created. null otherwise. */ public Return makeReturn(String returnName) { if (currentActivation.getSourceCall() == null) { return null; } if (currentActivation.getSourceCall().getSource() != null) { Activation pointer = currentActivation.getSourceCall().getSource(); Return r = new Return(chart); r.setLineStyle(SWT.LINE_DASH); r.setTargetStyle(Return.OPEN_ARROW); r.setText(returnName); r.setData(returnName); currentActivation.addMessage(index, r, pointer); incrementGroups(); index++; return r; } return null; } /** * Follows the given return to its destination, and resets the insertion index to immediately * after its return. The current position in the chart is irrelevent. The return will * be followed anyway. * @param r the return to follow. * @return the return followed. */ public Return followReturn(Return r) { if (r == null) { return null; } Activation pointer = r.getSource(); Activation target = r.getTarget(); Call m = pointer.getSourceCall(); while (m != null && m.getSource() != null && m.getSource() != target) { m = m.getSource().getSourceCall(); } if (m != null) { int i = 0; Message[] messages = target.getMessages(); while (i < messages.length) { if (messages[i]== m) { i++; break; } i++; } this.index = i; currentActivation = target; return r; } return null; } /** * Opens up a new group in the context of the current state of this builder. All subsequent * messages will be added inside this group until closeGroup() is called in the same context. * @param groupName the name of the group to open. * @return the group made. */ public MessageGroup openGroup(String groupName) { List<MessageGroup> groups = unscopedGroups.get(currentActivation); if (groups == null) { groups = new LinkedList<MessageGroup>(); unscopedGroups.put(currentActivation, groups); } MessageGroup group = new MessageGroup(chart); group.setText(groupName); group.setData(groupName); group.setRange(currentActivation, index, 0); groups.add(group); return group; } /** * Closes the last group opened in the context of the current location in the builder. * Subsequent messages will not be placed inside this group. * @return the group that was closed, or null if none. */ public MessageGroup closeGroup() { List<MessageGroup> groups = unscopedGroups.get(currentActivation); if (groups != null && groups.size() > 0) { return ((LinkedList<MessageGroup>)groups).removeLast(); } return null; } /** * Returns the activations that messages will be inserted on. * @return the the current activation that methods will be added to. */ public Activation getCurrentActivation() { return currentActivation; } /** * Resets the context to be on the given activation. The insertion index will be reset to the * end of that activation. * @param activation the activation to begin on. */ public void setContext(Activation activation) { this.currentActivation = activation; this.index = activation.getMessages().length; } }