package com.baselet.element.sequence_aio.facet; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.baselet.control.enums.LineType; import com.baselet.element.sequence_aio.facet.StateInvariant.StateInvariantStyle; /** * Constructs a sequence diagram. * No modifications are allowed after the diagram was returned. * Events which affect a lifeline must be added after the affected lifelines. * */ public class SequenceDiagramBuilder { private static final Logger log = LoggerFactory.getLogger(SequenceDiagramBuilder.class); private final String DEFAULT_ID_PREFIX = "id"; private final SequenceDiagram dia; private final Map<Lifeline, LifelineState> currentLifelineState; /** if true no default ids are generated and every lifeline needs an id */ private boolean overrideDefaultIds; private final Map<String, Lifeline> ids; private final LinkedList<ActiveCombinedFragment> activeCombinedFragmentStack; /** ids on a lifeline for occurrenceSpecifications (message endpoint or execution specification start/end) */ private final Map<Lifeline, Map<String, OccurrenceSpecification>> lifelineLocalIds; /** stores all warnings which are found while the sequence diagram is built */ private final List<String> warnings; private int currentTick; private int lastMessageReceiveTick; private boolean diagramRetrieved; public SequenceDiagramBuilder() { overrideDefaultIds = false; ids = new HashMap<String, Lifeline>(); activeCombinedFragmentStack = new LinkedList<SequenceDiagramBuilder.ActiveCombinedFragment>(); lifelineLocalIds = new HashMap<Lifeline, Map<String, OccurrenceSpecification>>(); dia = new SequenceDiagram(); currentLifelineState = new HashMap<Lifeline, SequenceDiagramBuilder.LifelineState>(); warnings = new LinkedList<String>(); currentTick = 0; lastMessageReceiveTick = 0; diagramRetrieved = false; } public void setTitle(String title) { checkState(); dia.setTitle(title); } public void setText(String text) { checkState(); dia.setText(text); } /** * @param id of the lifeline which should be returned * @return the lifeline or null if no lifeline is associated with the given id */ public Lifeline getLifeline(String id) { return ids.get(id); } /** * @param id of the lifeline which should be returned * @return the lifeline * @throws SequenceDiagramException if no lifeline is associated with the given id */ Lifeline getLifelineException(String id) { if (!ids.containsKey(id)) { throw new SequenceDiagramException("No lifeline is associated with the id: '" + id + "'"); } return ids.get(id); } public Lifeline[] getLifelineInterval(String id1, String id2) { try { return getLifelineIntervalException(id1, id2); } catch (SequenceDiagramException e) { return new Lifeline[0]; } } public Lifeline[] getLifelineIntervalException(String id1, String id2) { Lifeline ll1 = getLifelineException(id1); Lifeline ll2 = getLifelineException(id2); if (ll1.getIndex() > ll2.getIndex()) { Lifeline tmp = ll1; ll1 = ll2; ll2 = tmp; } return Arrays.copyOfRange(dia.getLifelinesArray(), ll1.getIndex(), ll2.getIndex() + 1); } /** * * @param headText the text which is drawn in the head * @param id can be NULL, if none of reorder and idoverride option is active * @param headType the lifeline style * @param createdOnStart if false the lifeline will be created by the first message sent to this lifeline */ public void addLiveline(String headText, String id, Lifeline.LifelineHeadType headType, boolean createdOnStart, boolean execSpecFromStart) { checkState(); if (isOverrideDefaultIds() && id == null) { throw new SequenceDiagramException("If the override option is set to true then every lifeline needs an id!"); } Lifeline newLifeline = dia.addLiveline(headText, headType, createdOnStart, execSpecFromStart); if (id != null) { if (ids.containsKey(id)) { throw new SequenceDiagramException("There is already a lifeline which is associated with the id '" +id + "', please choose another identifier."); } ids.put(id, newLifeline); } if (!overrideDefaultIds) { if (ids.containsKey(DEFAULT_ID_PREFIX + dia.getLifelineCount())) { throw new SequenceDiagramException("There is already a lifeline which is associated with the default id '" + DEFAULT_ID_PREFIX + dia.getLifelineCount() + "',\nplease choose another identifier or add the option 'overrideIds=true'."); } ids.put(DEFAULT_ID_PREFIX + dia.getLifelineCount(), newLifeline); } lifelineLocalIds.put(newLifeline, new HashMap<String, OccurrenceSpecification>()); LifelineState newLifelineState = new LifelineState(); if (execSpecFromStart && createdOnStart) { newLifelineState.execSpecStartTickStack.addFirst(-1); } currentLifelineState.put(newLifeline, newLifelineState); } /** * * @param id of the lifeline * @param event e.g. message, invariant, activity */ private void addLifelineOccurrence(String id, LifelineOccurrence occurrence) { checkState(); try { getLifelineException(id).addLifelineOccurrenceAtTick(occurrence, currentTick); } catch (SequenceDiagramCheckedException ex) { throw new SequenceDiagramException("Error on lifeline '" + id + "': " + ex.getMessage(), ex); } } public void addTextOnLifeline(String lifelineId, String text) { addLifelineOccurrence(lifelineId, new TextOnLifeline(text)); } public void addStateInvariant(String lifelineId, String text, boolean drawAsState) { addLifelineOccurrence(lifelineId, new StateInvariant(text, drawAsState ? StateInvariantStyle.STATE : StateInvariantStyle.CURLY_BRACKETS)); } public void addCoregion(String id, boolean start) { checkState(); // check that every coregion should be closed and coregion should not overlap, but only add a warning and trust the user Lifeline lifeline = getLifelineException(id); LifelineState lifelineState = currentLifelineState.get(lifeline); if (lifelineState.coregionActive && start) { addWarning(id, "A new coregion was started while an old one was still active."); } else if (!lifelineState.coregionActive && !start) { addWarning(id, "A coregion was closed, but no coregion was active."); } try { lifeline.addLifelineOccurrenceAtTick(new Coregion(lifeline, currentTick, start), currentTick); lifelineState.coregionActive = start; } catch (SequenceDiagramCheckedException ex) { throw new SequenceDiagramException("Error on lifeline '" + id + "': " + ex.getMessage(), ex); } } public void destroyLifeline(String id) { checkState(); // check that every coregion should be closed and coregion should not overlap, but only add a warning and trust the user Lifeline lifeline = getLifelineException(id); if (lifeline.getDestroyed() != null) { addWarning(id, "The lifeline was already destroyed."); } else { lifeline.setDestroyed(currentTick); } } public void changeExecutionSpecification(String lifelineId, boolean on) { checkState(); // check that they don't collide i.e. 2 changes at the same tick Lifeline lifeline = getLifelineException(lifelineId); LifelineState lifelineState = currentLifelineState.get(lifeline); if (on) { if (lifelineState.execSpecStartTickStack.size() > 0 && lifelineState.execSpecStartTickStack.peek() == currentTick) { throw new SequenceDiagramException("On lifeline " + lifelineId + " two executionspecifications start at the same tick, this is not possible."); } if (lifelineState.lastEndOfExecSpec == currentTick) { throw new SequenceDiagramException("On lifeline " + lifelineId + " two executionspecifications overlap, this is not possible."); } if (!lifeline.isCreatedOnStart() && (lifeline.getCreated() == null || lifeline.getCreated() >= currentTick)) { throw new SequenceDiagramException("Error on lifeline '" + lifelineId + "': the lifeline can not contain executionspecifications before it is created."); } lifelineState.execSpecStartTickStack.addFirst(currentTick); } else { if (lifelineState.execSpecStartTickStack.size() == 0) { throw new SequenceDiagramException("On lifeline " + lifelineId + " a executionspecification was closed but no active executionspecification exists."); } int startTick = lifelineState.execSpecStartTickStack.poll(); if (startTick == currentTick) { throw new SequenceDiagramException("On lifeline " + lifelineId + " a executionspecification was closed too soon, every executionspecification needs to be at least 1 tick long."); } if (lifelineState.lastEndOfExecSpec == currentTick) { throw new SequenceDiagramException("On lifeline " + lifelineId + " two executionspecifications end at the same tick, this is not possible."); } lifelineState.lastEndOfExecSpec = currentTick; lifeline.addExecutionSpecification(new ExecutionSpecification(startTick, currentTick)); } } /** * @param fromId * @param toId * @param duration * @param text * @param lineType * @param arrowType * @param fromLocalId can be null, if not null the send end point will be associated with the given id * @param toLocalId can be null, if not null the receive end point will be associated with the given id */ public void addMessage(String fromId, String toId, int duration, String text, LineType lineType, Message.ArrowType arrowType, String fromLocalId, String toLocalId) { checkState(); // check that a self message has a duration > 0 if (fromId.equals(toId)) { if (duration < 1) { throw new SequenceDiagramException("The duration of a self message must be greater than 0, but was " + duration + "."); } } lastMessageReceiveTick = Math.max(lastMessageReceiveTick, currentTick + duration); Lifeline from = getLifelineException(fromId); checkLifelineSendMessage(from, fromId); Lifeline to = getLifelineException(toId); // check if the message doesn't end before the lifeline was created if (!to.isCreatedOnStart() && to.getCreated() != null && currentTick + duration <= to.getCreated()) { throw new SequenceDiagramException("A message can't end on a lifeline before this lifeline was created.\n" + "Please increase the messages duration by at least " + (to.getCreated() + 1 - (currentTick + duration)) + "."); } else if (currentTick + duration < 0) { throw new SequenceDiagramException("A message can't end on a lifeline before this lifeline was created.\n" + "Please increase the messages duration by at least " + -(currentTick + duration) + "."); } if (!to.isCreatedOnStart() && to.getCreated() == null) { to.setCreated(currentTick + duration); if (to.isExecSpecFromStart()) { currentLifelineState.get(to).execSpecStartTickStack.push(currentTick + duration); } } Message msg = new Message(from, to, duration, currentTick, text, arrowType, lineType); if (fromLocalId != null) { addOccurrenceSpecification(from, fromId, fromLocalId, msg.sendOccurrenceSpecification()); } if (toLocalId != null) { addOccurrenceSpecification(to, toId, toLocalId, msg.receiveOccurrenceSpecification()); } dia.addLifelineSpanningTickSpanningOccurrence(msg); } public void addLostMessage(String fromId, String text, LineType lineType, Message.ArrowType arrowType, String fromLocalId) { checkState(); Lifeline from = getLifelineException(fromId); checkLifelineSendMessage(from, fromId); LostOrFoundMessage msg = new LostOrFoundMessage(from, false, currentTick, text, arrowType, lineType); if (fromLocalId != null) { addOccurrenceSpecification(from, fromId, fromLocalId, msg.sendOccurrenceSpecification()); } addLifelineOccurrence(fromId, msg); } public void addFoundMessage(String toId, String text, LineType lineType, Message.ArrowType arrowType, String toLocalId) { checkState(); Lifeline to = getLifelineException(toId); if (!to.isCreatedOnStart()) { if (to.getCreated() == null || to.getCreated() >= currentTick) { throw new SequenceDiagramException("The lifeline " +toId + " was not yet created, therefore it is not possible to send a found message to it."); } } LostOrFoundMessage msg = new LostOrFoundMessage(to, true, currentTick, text, arrowType, lineType); if (toLocalId != null) { addOccurrenceSpecification(to, toId, toLocalId, msg.receiveOccurrenceSpecification()); } addLifelineOccurrence(toId, msg); } public void addReceiveGateMessage(String fromId, String text, LineType lineType, Message.ArrowType arrowType, String fromLocalId) { checkState(); Lifeline from = getLifelineException(fromId); checkLifelineSendMessage(from, fromId); GateMessage msg = GateMessage.createReceiveGateMessage(from, currentTick, text, arrowType, lineType, dia.getLifelinesArray()[0], dia.getLifelinesArray()[dia.getLifelinesArray().length - 1]); if (fromLocalId != null) { addOccurrenceSpecification(from, fromId, fromLocalId, msg.sendOccurrenceSpecification()); } dia.addLifelineSpanningTickSpanningOccurrence(msg); } public void addSendGateMessage(String toId, String text, LineType lineType, Message.ArrowType arrowType, String toLocalId) { checkState(); Lifeline to = getLifelineException(toId); GateMessage msg = GateMessage.createSendGateMessage(to, currentTick, text, arrowType, lineType, dia.getLifelinesArray()[0], dia.getLifelinesArray()[dia.getLifelinesArray().length - 1]); if (toLocalId != null) { addOccurrenceSpecification(to, toId, toLocalId, msg.receiveOccurrenceSpecification()); } if (!to.isCreatedOnStart() && to.getCreated() == null) { to.setCreated(currentTick); } dia.addLifelineSpanningTickSpanningOccurrence(msg); } private void checkLifelineSendMessage(Lifeline from, String id) { if (!from.isCreatedOnStart()) { if (from.getCreated() == null || from.getCreated() >= currentTick) { throw new SequenceDiagramException("The lifeline " + id + " was not yet created, therefore it is not possible to send a message from it."); } } } public void addGeneralOrdering(String earlierLifelineId, String earlierLifelineLocalId, String laterLifelineId, String laterLifelineLocalId) { checkState(); dia.addLifelineSpanningTickSpanningOccurrence(new GeneralOrdering( getLifelineOccurrenceSpecException(earlierLifelineId, earlierLifelineLocalId), getLifelineOccurrenceSpecException(laterLifelineId, laterLifelineLocalId), getLifelineIntervalException(earlierLifelineId, laterLifelineId))); } // private void addOccurrenceSpecification(String lifelineId, String localId, OccurrenceSpecification occurrence) { // addOccurrenceSpecification(getLifelineException(lifelineId), lifelineId, localId, occurrence); // } private void addOccurrenceSpecification(Lifeline lifeline, String lifelineId, String localId, OccurrenceSpecification occurrence) { if (lifelineLocalIds.get(lifeline).containsKey(localId)) { throw new SequenceDiagramException("The lifeline '" + lifelineId + "' has already a local id '" + localId + "', please choose another id."); } lifelineLocalIds.get(lifeline).put(localId, occurrence); } private OccurrenceSpecification getLifelineOccurrenceSpecException(String lifelineId, String localId) { Lifeline llifeline = getLifelineException(lifelineId); OccurrenceSpecification occurrenceSpec = lifelineLocalIds.get(llifeline).get(localId); if (occurrenceSpec == null) { throw new SequenceDiagramException("No lifeline occurrence with the id '" + localId + "' could be found on lifeline '" + lifelineId + "'."); } return occurrenceSpec; } public void addInteractionUse(String startId, String endId, String text) { checkState(); Lifeline[] lifelines = getLifelineIntervalException(startId, endId); dia.addLifelineSpanningTickSpanningOccurrence(new InteractionUse(currentTick, text, lifelines)); } public void addContinuation(String startId, String endId, String text) { checkState(); Lifeline[] lifelines = getLifelineIntervalException(startId, endId); dia.addLifelineSpanningTickSpanningOccurrence(new Continuation(currentTick, text, lifelines)); } /** * @param startId starting lifeline * @param endId ending lifeline * @param cfId the id of the combined Fragment, can be null * @param operator of the combined fragment */ public void beginCombinedFragment(String startId, String endId, String cfId, String operator) { checkState(); Lifeline[] lifelines; if (startId == null && endId == null) { lifelines = Arrays.<Lifeline> copyOf(dia.getLifelinesArray(), dia.getLifelinesArray().length); } else { lifelines = getLifelineIntervalException(startId, endId); } activeCombinedFragmentStack.push(new ActiveCombinedFragment(new CombinedFragment(lifelines, currentTick, operator), cfId)); activeCombinedFragmentStack.peek().activeOperand = new ActiveOperand(currentTick); } /** * @param cfId can be null, then the latest created combined fragment which was not closed is closed. * Otherwise the combined fragment associated with the given id is closed. */ public void endCombinedFragment(String cfId) { checkState(); if (activeCombinedFragmentStack.size() == 0) { throw new SequenceDiagramException("Error a combined fragment was closed, but no open one exists."); } ActiveCombinedFragment currentCombFrag = null; if (cfId == null) { currentCombFrag = activeCombinedFragmentStack.pop(); } else { ListIterator<ActiveCombinedFragment> activeCFIter = activeCombinedFragmentStack.listIterator(); while (activeCFIter.hasNext()) { currentCombFrag = activeCFIter.next(); if (cfId.equals(currentCombFrag.id)) { activeCFIter.remove(); break; } currentCombFrag = null; } } if (currentCombFrag == null) { throw new SequenceDiagramException("Error no combined fragment with id '" + cfId + "' was found."); } else { // TODO operand rewrite // if (currentCombFrag.activeOperand != null) { // throw new SequenceDiagramException("Error a combined fragment was closed, but there is still an open operand present."); // } currentCombFrag.combFrag.addOperand(currentCombFrag.activeOperand.startTick, currentTick); dia.addLifelineSpanningTickSpanningOccurrence(currentCombFrag.combFrag); } } public void endAndBeginOperand(String cfId) { checkState(); if (activeCombinedFragmentStack.size() == 0) { throw new SequenceDiagramException("Error a combined fragment was closed, but no open one exists."); } ActiveCombinedFragment currentCombFrag = null; if (cfId == null) { currentCombFrag = activeCombinedFragmentStack.peek(); } else { ListIterator<ActiveCombinedFragment> activeCFIter = activeCombinedFragmentStack.listIterator(); while (activeCFIter.hasNext()) { currentCombFrag = activeCFIter.next(); if (cfId.equals(currentCombFrag.id)) { break; } currentCombFrag = null; } } if (currentCombFrag == null) { throw new SequenceDiagramException("Error no combined fragment with id '" + cfId + "' was found."); } else { // TODO operand rewrite currentCombFrag.combFrag.addOperand(currentCombFrag.activeOperand.startTick, currentTick); currentCombFrag.activeOperand = new ActiveOperand(currentTick); } } // public void beginOperand() { // checkState(); // beginOperand(null, ""); // } /** * * @param text if empty or null no constraint will be generated * @param lifelineId if empty or null the first occurrence in the operand will determine the lifeline on which the constraint is placed */ // public void beginOperand(String text, String lifelineId) { // checkState(); // if (activeCombinedFragmentStack.size() == 0) { // throw new SequenceDiagramException("Error an operand must lie in a combined fragment, but no open one exists."); // } // ActiveCombinedFragment currentCombFrag = activeCombinedFragmentStack.peek(); // if (currentCombFrag.activeOperand != null) { // throw new SequenceDiagramException("Error a new operand was started, but there is still an open operand present."); // } // ActiveOperand operand; // if (text == null || text.isEmpty()) { // operand = new ActiveOperand(currentTick); // } // else { // operand = new ActiveOperand(text, getLifeline(lifelineId), currentTick); // } // currentCombFrag.activeOperand = operand; // } // public void endOperand() { // checkState(); // if (activeCombinedFragmentStack.size() == 0) { // throw new SequenceDiagramException("Error an operand must lie in a combined fragment, but no open one exists."); // } // ActiveCombinedFragment currentCombFrag = activeCombinedFragmentStack.peek(); // if (currentCombFrag.activeOperand == null) { // throw new SequenceDiagramException("Error a operand was closed, but there is no open operand."); // } // ActiveOperand operand = currentCombFrag.activeOperand; // currentCombFrag.activeOperand = null; // if (operand.constraint == null) { // currentCombFrag.combFrag.addOperand(operand.startTick, currentTick); // } // else { // Lifeline lifeline = operand.assoicatedLifeline; // if (lifeline == null) { // if it wasn't set place it on the first lifeline of the combined fragment // lifeline = currentCombFrag.combFrag.getFirstLifeline(); // } // try { // currentCombFrag.combFrag.addOperand(operand.startTick, currentTick, operand.constraint, lifeline); // } catch (SequenceDiagramCheckedException e) { // throw new SequenceDiagramException("Error while placing the interaction '" + operand.constraint // + "' constraint on the " + (lifeline.getIndex() + 1) // + " lifeline from the left.\n" + e.getMessage(), e); // } // } // } /** * advances a step down */ public void tick() { tick(1); } /** * * @param tickCount needs to be > 0 */ public void tick(int tickCount) { checkState(); if (tickCount < 1) { throw new IllegalArgumentException("The tickCount must be greater than 0."); } currentTick += tickCount; } public void setOverrideDefaultIds(boolean overrideDefaultIds) { checkState(); if (dia.getLifelineCount() > 0) { throw new SequenceDiagramException("The override ids option must be specified before any lifeline is specified."); } else { this.overrideDefaultIds = overrideDefaultIds; } } public boolean isOverrideDefaultIds() { return overrideDefaultIds; } /** * Should be called after the diagram is generated, because this last step can add further warnings. * @return the empty string if no warnings exists, otherwise a headline and the warnings (with \n as line delimiter) are returned */ public String getWarnings() { if (warnings.size() == 0) { return ""; } else { StringBuffer strBuffer = new StringBuffer(); strBuffer.append("Warnings:"); for (String w : warnings) { strBuffer.append('\n'); strBuffer.append(w); } return strBuffer.toString(); } } /** * Called as final step. * Can be called multiple times, but every time the same diagram (same reference) is returned. * If there exists a major issue with the diagram an exception is thrown, otherwise the diagram is returned * and if there are minor issues these are added to the warnings. * * @return the generated diagram * @throws SequenceDiagramException if the diagram could not be generated. * @see #getWarnings() */ public SequenceDiagram generateDiagram() { if (diagramRetrieved) { return dia; } else { // if a created later lifeline has no message it is draw at the start and a warning is added boolean createdLaterWarning = false; for (Lifeline ll : dia.getLifelines()) { if (!ll.isCreatedOnStart() && ll.getCreated() == null) { createdLaterWarning = true; ll.setCreatedOnStart(true); } } if (createdLaterWarning) { warnings.add("At least one lifeline was specified as created later, but didn't receive a message"); } // TODO for each operand condition add a LL occurence to the lifeline with the first message (or to the lowest index) // close all execution specifications and add a warning. boolean openExecSpecs; boolean foundOpenExecSpecs = false; do { openExecSpecs = false; for (Map.Entry<Lifeline, LifelineState> e : currentLifelineState.entrySet()) { if (e.getValue().execSpecStartTickStack.size() > 0) { if (e.getValue().lastEndOfExecSpec < currentTick) { e.getValue().lastEndOfExecSpec = currentTick; e.getKey().addExecutionSpecification( new ExecutionSpecification(e.getValue().execSpecStartTickStack.pop(), currentTick)); openExecSpecs = openExecSpecs || e.getValue().execSpecStartTickStack.size() > 0; foundOpenExecSpecs = true; } } } if (openExecSpecs) { currentTick++; } } while (openExecSpecs); if (foundOpenExecSpecs) { warnings.add("At least one executionspecification was not closed, any open executionspecification was closed at the end of the diagram."); } dia.setLastTick(Math.max(currentTick, lastMessageReceiveTick)); diagramRetrieved = true; return dia; } } /** * throws an exception if the diagram was already returned */ private void checkState() { if (diagramRetrieved) { throw new IllegalStateException("The final diagram was returned and therefore no more changes are allowed."); } } private void addWarning(String id, String text) { warnings.add("On lifeline '" + id + "': " + text); } private static class LifelineState { /** stores the last end of an execution specifications. */ int lastEndOfExecSpec = -1; // Starts at -1 since the first tick is 0 and therefore no overlap is possible LinkedList<Integer> execSpecStartTickStack = new LinkedList<Integer>(); boolean coregionActive = false; } private static class ActiveCombinedFragment { public ActiveCombinedFragment(CombinedFragment combFrag, String cfId) { super(); this.combFrag = combFrag; id = cfId; } String id; CombinedFragment combFrag; ActiveOperand activeOperand = null; } private static class ActiveOperand { public ActiveOperand(int startTick) { super(); this.startTick = startTick; } // public ActiveOperand(String constraint, Lifeline assoicatedLifeline, int startTick) { // super(); // this.constraint = constraint; // this.assoicatedLifeline = assoicatedLifeline; // this.startTick = startTick; // } // // String constraint = null; // if empty no constraint needed // Lifeline assoicatedLifeline = null; // lifeline on which the constraint should be drawn int startTick; } }