From d92ca6a07798a4645f3655f92c9e6813c47121b2 Mon Sep 17 00:00:00 2001 From: Kamil Mierzejewski Date: Wed, 5 Jan 2022 09:47:50 +0100 Subject: [PATCH] Xmi export for Sequence Diagrams --- .../sequencediagram/SequenceDiagram.java | 4 + .../plantuml/xmi/SequenceDiagramXmiMaker.java | 85 ++++++ .../plantuml/xmi/XmiSequenceDiagram.java | 46 ++++ .../plantuml/xmi/XmiSequenceDiagramArgo.java | 132 +++++++++ .../xmi/XmiSequenceDiagramStandard.java | 253 ++++++++++++++++++ 5 files changed, 520 insertions(+) create mode 100644 src/net/sourceforge/plantuml/xmi/SequenceDiagramXmiMaker.java create mode 100644 src/net/sourceforge/plantuml/xmi/XmiSequenceDiagram.java create mode 100644 src/net/sourceforge/plantuml/xmi/XmiSequenceDiagramArgo.java create mode 100644 src/net/sourceforge/plantuml/xmi/XmiSequenceDiagramStandard.java diff --git a/src/net/sourceforge/plantuml/sequencediagram/SequenceDiagram.java b/src/net/sourceforge/plantuml/sequencediagram/SequenceDiagram.java index d39b20288..275bde7d7 100644 --- a/src/net/sourceforge/plantuml/sequencediagram/SequenceDiagram.java +++ b/src/net/sourceforge/plantuml/sequencediagram/SequenceDiagram.java @@ -71,6 +71,7 @@ import net.sourceforge.plantuml.skin.rose.Rose; import net.sourceforge.plantuml.style.ClockwiseTopRightBottomLeft; import net.sourceforge.plantuml.ugraphic.ImageBuilder; import net.sourceforge.plantuml.ugraphic.color.HColor; +import net.sourceforge.plantuml.xmi.SequenceDiagramXmiMaker; public class SequenceDiagram extends UmlDiagram { @@ -255,6 +256,9 @@ public class SequenceDiagram extends UmlDiagram { if (fileFormat == FileFormat.ATXT || fileFormat == FileFormat.UTXT) return new SequenceDiagramTxtMaker(this, fileFormat); + if (fileFormat.name().startsWith("XMI")) + return new SequenceDiagramXmiMaker(this, fileFormat); + if (modeTeoz()) return new SequenceDiagramFileMakerTeoz(this, skin2, fileFormatOption, index); diff --git a/src/net/sourceforge/plantuml/xmi/SequenceDiagramXmiMaker.java b/src/net/sourceforge/plantuml/xmi/SequenceDiagramXmiMaker.java new file mode 100644 index 000000000..50453e5b9 --- /dev/null +++ b/src/net/sourceforge/plantuml/xmi/SequenceDiagramXmiMaker.java @@ -0,0 +1,85 @@ +package net.sourceforge.plantuml.xmi; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.io.IOException; +import java.io.OutputStream; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import org.w3c.dom.Document; + +import net.sourceforge.plantuml.FileFormat; +import net.sourceforge.plantuml.api.ImageDataSimple; +import net.sourceforge.plantuml.core.ImageData; +import net.sourceforge.plantuml.sequencediagram.SequenceDiagram; +import net.sourceforge.plantuml.sequencediagram.graphic.FileMaker; +import net.sourceforge.plantuml.xml.XmlFactories; + +public final class SequenceDiagramXmiMaker implements FileMaker { + + private final SequenceDiagram diagram; + private final FileFormat fileFormat; + + public SequenceDiagramXmiMaker(SequenceDiagram sequenceDiagram, FileFormat fileFormat) { + this.diagram = sequenceDiagram; + this.fileFormat = fileFormat; + } + + @Override + public ImageData createOne(OutputStream os, int index, boolean isWithMetadata) throws IOException { + DocumentBuilder builder; + ImageData imageData = new ImageDataSimple(0, 0); + try { + builder = XmlFactories.newDocumentBuilder(); + } catch (ParserConfigurationException e) { + e.printStackTrace(); + return imageData; + } + Document document = builder.newDocument(); + document.setXmlVersion("1.0"); + document.setXmlStandalone(true); + + XmiSequenceDiagram xmi; + if (fileFormat == FileFormat.XMI_ARGO) + xmi = new XmiSequenceDiagramArgo(diagram, document); + else + xmi = new XmiSequenceDiagramStandard(diagram, document); + + xmi.build(); + + try { + writeDocument(document, os); + } catch (TransformerException | ParserConfigurationException e) { + e.printStackTrace(); + } + return imageData; + } + + + @Override + public int getNbPages() { + return 1; + } + + private void writeDocument(Document document, OutputStream os) + throws TransformerException, ParserConfigurationException { + final Source source = new DOMSource(document); + + final Result resultat = new StreamResult(os); + + final Transformer transformer = XmlFactories.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty(OutputKeys.ENCODING, UTF_8.name()); + transformer.transform(source, resultat); + } + +} diff --git a/src/net/sourceforge/plantuml/xmi/XmiSequenceDiagram.java b/src/net/sourceforge/plantuml/xmi/XmiSequenceDiagram.java new file mode 100644 index 000000000..622ba227c --- /dev/null +++ b/src/net/sourceforge/plantuml/xmi/XmiSequenceDiagram.java @@ -0,0 +1,46 @@ +package net.sourceforge.plantuml.xmi; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import net.sourceforge.plantuml.cucadiagram.Display; +import net.sourceforge.plantuml.sequencediagram.SequenceDiagram; + +public abstract class XmiSequenceDiagram { + + protected final SequenceDiagram diagram; + + public abstract void build(); + + protected final Document document; + + public XmiSequenceDiagram(SequenceDiagram diagram, Document document) { + super(); + this.diagram = diagram; + this.document = document; + } + + protected Element createElement(String tag, String[][] attributes) { + return setAttributes(document.createElement(tag), attributes); + } + + protected Element setAttribute(Element element, String name, String value) { + element.setAttribute(name, value); + return element; + } + + protected Element setAttributes(Element element, String[][] attributes) { + for (String[] attr : attributes) { + element.setAttribute(attr[0], attr[1]); + } + return element; + } + + protected String getDisplayString(Display display) { + return String.join("\n", display.asList()); + } + + protected String getXmiId(String tag, Object object) { + return Integer.toHexString(tag.hashCode()) + "_" + Integer.toHexString(object.hashCode()); + } +} \ No newline at end of file diff --git a/src/net/sourceforge/plantuml/xmi/XmiSequenceDiagramArgo.java b/src/net/sourceforge/plantuml/xmi/XmiSequenceDiagramArgo.java new file mode 100644 index 000000000..88c5abe64 --- /dev/null +++ b/src/net/sourceforge/plantuml/xmi/XmiSequenceDiagramArgo.java @@ -0,0 +1,132 @@ +package net.sourceforge.plantuml.xmi; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import net.sourceforge.plantuml.sequencediagram.Event; +import net.sourceforge.plantuml.sequencediagram.Message; +import net.sourceforge.plantuml.sequencediagram.Participant; +import net.sourceforge.plantuml.sequencediagram.SequenceDiagram; +import net.sourceforge.plantuml.version.Version; + +public class XmiSequenceDiagramArgo extends XmiSequenceDiagram { + public XmiSequenceDiagramArgo(SequenceDiagram diagram, Document document) { + super(diagram, document); + } + + @Override + public void build() { + final Element xmi = document.createElement("XMI"); + xmi.setAttribute("xmi.version", "1.2"); + xmi.setAttribute("xmlns:UML", "href://org.omg/UML/1.3"); + document.appendChild(xmi); + + final Element header = document.createElement("XMI.header"); + xmi.appendChild(header); + + final Element metamodel = document.createElement("XMI.metamodel"); + metamodel.setAttribute("xmi.name", "UML"); + metamodel.setAttribute("xmi.version", "1.4"); + header.appendChild(metamodel); + + final Element content = document.createElement("XMI.content"); + xmi.appendChild(content); + + // + final Element model = createElement(diagram, "UML:Model"); + model.setAttribute("name", "PlantUML " + Version.versionString()); + content.appendChild(model); + + Element ownedElement = document.createElement("UML:Namespace.ownedElement"); + model.appendChild(ownedElement); + ownedElement.appendChild(createCollaborationElement()); + + for (Participant participant : diagram.participants()) { + ownedElement.appendChild(createActorElement(participant)); + } + } + + private Element createActorElement(Participant participant) { + Element actor = createElement(participant, "UML:Actor"); + actor.setAttribute("name", String.join(" ", participant.getDisplay(false).asList())); + return actor; + } + + private Node createCollaborationElement() { + Element collaboration = document.createElement("UML:Collaboration"); + Element ownedElement = document.createElement("UML:Namespace.ownedElement"); + + for (Participant participant : diagram.participants()) { + ownedElement.appendChild(createClassifierRole(participant)); + } + + collaboration.appendChild(ownedElement); + + Node messages = collaboration.appendChild((document.createElement("UML:Collaboration.interaction"))) + .appendChild(document.createElement("UML:Interaction")) + .appendChild(document.createElement("UML:Interaction.message")); + + Message prevMessage = null; + for (Event event : diagram.events()) { + if (event instanceof Message) { + Message message = (Message) event; + messages.appendChild(createMessage(message, prevMessage)); + ownedElement.appendChild(createSendAction(message)); + prevMessage = message; + } + } + + return collaboration; + } + + private Node createSendAction(Message message) { + Element sendAction = createElement(message, "UML:SendAction"); + sendAction.appendChild(document.createElement("UML:Action.script")).appendChild(createElement( + "UML:ActionExpression", new String[][] { + {"xmi.id", getXmiId("UML:ActionExpression", message)}, + {"body", getDisplayString(message.getLabel()) } + })); + return sendAction; + } + + private Element createElement(Object object, String tag) { + return createElement(tag, + new String[][] { {"xmi.id", getXmiId("UML:ActionExpression", object)}}); + } + + private Node createRef(String tag, Object target) { + Element role = document.createElement(tag); + role.setAttribute("xmi.idref", getXmiId(tag, target)); + return role; + } + + private Element createClassifierRole(Participant participant) { + Element classifierRole = createElement(participant, "UML:ClassifierRole"); + + classifierRole.setAttribute("name", participant.getCode()); + classifierRole.appendChild(document.createElement("UML:ClassifierRole.base")) + .appendChild(createRef("UML:Actor", participant)); + return classifierRole; + } + + private Element createMessage(Message message, Message prevMessage) { + Element messageElement = createElement(message, "UML:Message"); + messageElement.appendChild(document.createElement("UML:Message.sender")) + .appendChild(createRef("UML:ClassifierRole", message.getParticipant1())); + messageElement.appendChild(document.createElement("UML:Message.receiver")) + .appendChild(createRef("UML:ClassifierRole", message.getParticipant2())); + messageElement.appendChild(document.createElement("UML:Message.action")) + .appendChild(createRef("UML:SendAction", message)); + + if (prevMessage != null) { + messageElement.appendChild(document.createElement("UML:Message.predecessor")) + .appendChild(createRef("UML:Message", prevMessage)); + } + + return messageElement; + } + +} diff --git a/src/net/sourceforge/plantuml/xmi/XmiSequenceDiagramStandard.java b/src/net/sourceforge/plantuml/xmi/XmiSequenceDiagramStandard.java new file mode 100644 index 000000000..f78dfeaa0 --- /dev/null +++ b/src/net/sourceforge/plantuml/xmi/XmiSequenceDiagramStandard.java @@ -0,0 +1,253 @@ +package net.sourceforge.plantuml.xmi; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Stack; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +import net.sourceforge.plantuml.sequencediagram.AbstractMessage; +import net.sourceforge.plantuml.sequencediagram.Event; +import net.sourceforge.plantuml.sequencediagram.Grouping; +import net.sourceforge.plantuml.sequencediagram.GroupingType; +import net.sourceforge.plantuml.sequencediagram.LifeEvent; +import net.sourceforge.plantuml.sequencediagram.LifeEventType; +import net.sourceforge.plantuml.sequencediagram.Message; +import net.sourceforge.plantuml.sequencediagram.MessageExo; +import net.sourceforge.plantuml.sequencediagram.Note; +import net.sourceforge.plantuml.sequencediagram.NotePosition; +import net.sourceforge.plantuml.sequencediagram.Participant; +import net.sourceforge.plantuml.sequencediagram.SequenceDiagram; + +public class XmiSequenceDiagramStandard extends XmiSequenceDiagram { + + private Stack> covered; + private HashMap> activeParticipants; + + public XmiSequenceDiagramStandard(SequenceDiagram diagram, Document document) { + super(diagram, document); + + covered = new Stack<>(); + covered.push(new HashSet()); + + activeParticipants = new HashMap<>(); + } + + @Override + public void build() { + Node packagedElement = document.appendChild(createElement("uml:Model", new String[][] { + {"xmlns:uml", "http://www.omg.org/spec/UML/20110701"}, + {"xmlns:xmi", "http://schema.omg.org/spec/XMI/2.1"}, + {"xmi:version","2.1"}, + {"xmi:id",getXmiId("uml:Model", diagram)} + })).appendChild(createUmlElement(diagram, "packagedElement", "Interaction")); + packagedElement.appendChild(createUmlElement(diagram, "nestedClassifier", "Collaboration")); + + for (Participant participant : diagram.participants()) { + packagedElement.appendChild(createElement(participant, "lifeline", new String[][] { + {"name", getDisplayString(participant.getDisplay(false))} + })); + } + + Node currentFragment = packagedElement; + for (Event event : diagram.events()) { + if (event instanceof Note) { + buildNoteEvent(packagedElement, (Note) event); + } else if (event instanceof LifeEvent) { + buildLifeEvent(packagedElement, (LifeEvent)event); + } else if (event instanceof AbstractMessage) { + buildMessage(packagedElement, currentFragment, (AbstractMessage) event); + } else if (event instanceof Grouping) { + currentFragment = buildGrouping(currentFragment, (Grouping) event); + } + } + } + + private void buildLifeEvent(Node packagedElement, LifeEvent event) { + if (event.getType() == LifeEventType.ACTIVATE) { + Element execution = createUmlElement(event, "fragment", "BehaviorExecutionSpecification"); + execution.setAttribute("covered", getXmiId("lifeline", event.getParticipant())); + execution.setAttribute("start", getLifeEventOccurrenceId(event)); + packagedElement.appendChild(execution); + activeParticipants.putIfAbsent(event.getParticipant(), new Stack()); + activeParticipants.get(event.getParticipant()).push(execution); + } else if (event.getType() == LifeEventType.DEACTIVATE) { + activeParticipants.get(event.getParticipant()).pop().setAttribute("finish", getLifeEventOccurrenceId(event)); + } + } + + private Participant getReceiver(AbstractMessage message) { + if (message instanceof Message) { + return message.getParticipant2(); + } else if (message instanceof MessageExo) { + MessageExo exo = (MessageExo) message; + if (exo.getType().toString().startsWith("FROM_")) { + return message.getParticipant1(); + } + } + return null; + } + + private String getLifeEventOccurrenceId(LifeEvent event) { + if (event.getParticipant() == getReceiver(event.getMessage())) + { + return getXmiId("receiveEvent", event.getMessage()); + } else { + return getXmiId("sendEvent", event.getMessage()); + } + } + + private void buildMessage(Node packagedElement, Node currentFragment, AbstractMessage message) { + if (message instanceof Message) { + buildMessage(packagedElement, currentFragment, (Message) message); + } + else if (message instanceof MessageExo) { + buildMessageExo(packagedElement, currentFragment, (MessageExo) message); + } + + if (message.getParticipant1() != null) { + covered.peek().add(getXmiId("lifeline", message.getParticipant1())); + } + if (message.getParticipant2() != null) { + covered.peek().add(getXmiId("lifeline", message.getParticipant2())); + } + } + + private void buildNoteEvent(Node packagedElement, Note note) { + HashSet annotated = new HashSet(); + + if (note.getParticipant() != null) { + annotated.add(getXmiId("lifeline", note.getParticipant())); + } + + if (note.getParticipant2() != null) { + annotated.add(getXmiId("lifeline", note.getParticipant2())); + } + + buildNote(packagedElement, note, annotated); + } + + private HashSet getAnnotatedElements(Note note, Message message) { + HashSet annotated = new HashSet(); + int p1 = getParticipantNumber(message.getParticipant1()); + int p2 = getParticipantNumber(message.getParticipant2()); + NotePosition senderPosition = p1 < p2 ? NotePosition.LEFT : NotePosition.RIGHT; + + if (note.getPosition() == senderPosition) { + annotated.add(getXmiId("sendEvent", message)); + annotated.add(getXmiId("lifeline", message.getParticipant1())); + } else { + annotated.add(getXmiId("receiveEvent", message)); + annotated.add(getXmiId("lifeline", message.getParticipant2())); + } + return annotated; + } + + private void buildNote(Node packagedElement, Note note, HashSet annotated) { + Element comment = createUmlElement(note, "ownedComment", "Comment"); + if (!annotated.isEmpty()) { + comment.setAttribute("annotatedElement", String.join(" ", annotated)); + } + comment.appendChild(document.createElement("body")).appendChild( + document.createTextNode(getDisplayString(note.getStrings()))); + packagedElement.appendChild(comment); + } + + private int getParticipantNumber(Participant participant) { + return Arrays.asList(diagram.participants().toArray()).indexOf(participant); + } + + private void buildMessageExo(Node packagedElement, Node currentFragment, MessageExo message) { + String messageEvent = message.getType().toString().startsWith("TO_") ? "sendEvent" : "receiveEvent"; + String messageEventId = getXmiId(messageEvent, message); + currentFragment.appendChild(createMessageOccurrence(message, messageEvent, message.getParticipant1())); + packagedElement.appendChild( + setAttributes(createUmlElement(message, "message", "Message"), new String[][]{ + {"name", getDisplayString(message.getLabel())}, + {messageEvent, messageEventId}})); + + HashSet annotated = new HashSet(); + annotated.add(messageEventId); + annotated.add(getXmiId("lifeline", message.getParticipant1())); + for (Note note : message.getNoteOnMessages()) { + buildNote(packagedElement, note, annotated); + } + } + + private void buildMessage(Node packagedElement, Node currentFragment, Message message) { + currentFragment.appendChild(createMessageOccurrence(message, "sendEvent", message.getParticipant1())); + currentFragment.appendChild(createMessageOccurrence(message, "receiveEvent", message.getParticipant2())); + packagedElement.appendChild( + setAttributes(createUmlElement(message, "message", "Message"), new String[][]{ + {"name", getDisplayString(message.getLabel())}, + {"receiveEvent", getXmiId("receiveEvent", message)}, + {"sendEvent", getXmiId("sendEvent", message)}})); + + for (Note note : message.getNoteOnMessages()) { + buildNote(packagedElement, note, getAnnotatedElements(note, message)); + } + } + + private Node buildGrouping(Node currentFragment, Grouping grouping) { + if (grouping.getType() == GroupingType.START) + { + Element group = createUmlElement(grouping, "fragment", "CombinedFragment"); + group.setAttribute("interactionOperator", grouping.getTitle()); + currentFragment.appendChild(group); + + currentFragment = group.appendChild(createElement(grouping, "operand")); + currentFragment.appendChild(createGuardElement(grouping)); + covered.push(new HashSet()); + } + else if(grouping.getType() == GroupingType.ELSE) { + currentFragment = currentFragment.getParentNode().appendChild(createElement(grouping, "operand")); + currentFragment.appendChild(createGuardElement(grouping)); + } + else if(grouping.getType() == GroupingType.END){ + Node coveredAttr = document.createAttribute("covered"); + HashSet topCovered = covered.pop(); + coveredAttr.setTextContent(String.join(" ", topCovered)); + currentFragment.getParentNode().getAttributes().setNamedItem(coveredAttr); + covered.peek().addAll(topCovered); + currentFragment = currentFragment.getParentNode().getParentNode(); + } + return currentFragment; + } + + private Node createGuardElement(Grouping grouping) { + Node guard = createElement(grouping,"guard"); + guard.appendChild(setAttribute(createUmlElement(grouping, "specification", "LiteralString"), + "value", grouping.getComment())); + return guard; + } + + + private Element createElement(Object object, String tag) { + return createElement(tag, new String[][]{{"xmi:id", getXmiId(tag, object)}}); + } + + private Element createElement(Object object, String tag, String[][] attributes) { + return setAttributes(createElement(tag, new String[][]{ + {"xmi:id", getXmiId(tag, object)} + }), attributes); + } + + private Element createUmlElement(Object object, String tag, String type) { + return createElement(tag, new String[][]{ + {"xmi:type", "uml:"+type}, + {"xmi:id", getXmiId(type, object)} + }); + } + + private Node createMessageOccurrence(Object event, String type, Participant participant) { + return setAttributes(createUmlElement(event, "fragment", "MessageOccurrenceSpecification"), + new String[][] { + {"xmi:id", getXmiId(type, event)}, + {"covered", getXmiId("lifeline", participant)}, + {"message", getXmiId("Message", event)}, + }); + } +}