// $Id: PGMLStackParser.java 16474 2008-12-30 22:09:28Z tfmorris $
// Copyright (c) 2005-2008 The Regents of the University of California. All
// Rights Reserved. Permission to use, copy, modify, and distribute this
// software and its documentation without fee, and without a written
// agreement is hereby granted, provided that the above copyright notice
// and this paragraph appear in all copies. This software program and
// documentation are copyrighted by The Regents of the University of
// California. The software program and documentation are supplied "AS
// IS", without any accompanying services from The Regents. The Regents
// does not warrant that the operation of the program will be
// uninterrupted or error-free. The end-user understands that the program
// was developed for research purposes and is advised not to rely
// exclusively on the program for any reason. IN NO EVENT SHALL THE
// UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
// SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS,
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT,
// UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
package org.argouml.persistence;
import java.awt.Rectangle;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import org.apache.log4j.Logger;
import org.argouml.uml.diagram.ArgoDiagram;
import org.argouml.uml.diagram.AttributesCompartmentContainer;
import org.argouml.uml.diagram.DiagramSettings;
import org.argouml.uml.diagram.ExtensionsCompartmentContainer;
import org.argouml.uml.diagram.OperationsCompartmentContainer;
import org.argouml.uml.diagram.PathContainer;
import org.argouml.uml.diagram.StereotypeContainer;
import org.argouml.uml.diagram.VisibilityContainer;
import org.argouml.uml.diagram.ui.FigEdgeModelElement;
import org.argouml.uml.diagram.ui.FigEdgePort;
import org.tigris.gef.base.Diagram;
import org.tigris.gef.persistence.pgml.Container;
import org.tigris.gef.persistence.pgml.FigEdgeHandler;
import org.tigris.gef.persistence.pgml.FigGroupHandler;
import org.tigris.gef.persistence.pgml.HandlerStack;
import org.tigris.gef.presentation.Fig;
import org.tigris.gef.presentation.FigEdge;
import org.tigris.gef.presentation.FigGroup;
import org.tigris.gef.presentation.FigNode;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
// TODO: Move to Diagram subsystem?
/**
* The PGML Parser. <p>
*
* This replaces much of the identically named class from GEF.
*/
class PGMLStackParser
extends org.tigris.gef.persistence.pgml.PGMLStackParser {
private static final Logger LOG = Logger.getLogger(PGMLStackParser.class);
private List<EdgeData> figEdges = new ArrayList<EdgeData>(50);
private LinkedHashMap<FigEdge, Object> modelElementsByFigEdge =
new LinkedHashMap<FigEdge, Object>(50);
private DiagramSettings diagramSettings;
/**
* Constructor.
* @param modelElementsByUuid a map of model elements indexed
* by a unique string identifier.
* @deprecated for 0.27.2 by tfmorris. Use
* {@link #PGMLStackParser(Map, DiagramSettings)}/
*/
@Deprecated
public PGMLStackParser(Map modelElementsByUuid) {
super(modelElementsByUuid);
addTranslations();
}
// TODO: Use stylesheet to convert or wait till we use Fig
// factories in diagram subsystem.
// What is the last version that used FigNote?
private void addTranslations() {
addTranslation("org.argouml.uml.diagram.ui.FigNote",
"org.argouml.uml.diagram.static_structure.ui.FigComment");
addTranslation("org.argouml.uml.diagram.static_structure.ui.FigNote",
"org.argouml.uml.diagram.static_structure.ui.FigComment");
addTranslation("org.argouml.uml.diagram.state.ui.FigState",
"org.argouml.uml.diagram.state.ui.FigSimpleState");
addTranslation("org.argouml.uml.diagram.ui.FigCommentPort",
"org.argouml.uml.diagram.ui.FigEdgePort");
addTranslation("org.tigris.gef.presentation.FigText",
"org.argouml.uml.diagram.ui.ArgoFigText");
addTranslation("org.tigris.gef.presentation.FigLine",
"org.argouml.gefext.ArgoFigLine");
addTranslation("org.tigris.gef.presentation.FigPoly",
"org.argouml.gefext.ArgoFigPoly");
addTranslation("org.tigris.gef.presentation.FigCircle",
"org.argouml.gefext.ArgoFigCircle");
addTranslation("org.tigris.gef.presentation.FigRect",
"org.argouml.gefext.ArgoFigRect");
addTranslation("org.tigris.gef.presentation.FigRRect",
"org.argouml.gefext.ArgoFigRRect");
addTranslation(
"org.argouml.uml.diagram.deployment.ui.FigMNodeInstance",
"org.argouml.uml.diagram.deployment.ui.FigNodeInstance");
addTranslation("org.argouml.uml.diagram.ui.FigRealization",
"org.argouml.uml.diagram.ui.FigAbstraction");
}
/**
* Construct a PGML parser with the given HREF/Object map and default
* diagram settings.
*
* @param modelElementsByUuid map of HREF ids to objects used to associate
* Figs with their owning model elements
* @param defaultSettings default diagram settings to use for newly created
* diagram and its contained Figs
*/
public PGMLStackParser(Map<String, Object> modelElementsByUuid,
DiagramSettings defaultSettings) {
super(modelElementsByUuid);
addTranslations();
// Create a new diagram wide settings block which is backed by
// the project-wide defaults that we were passed
diagramSettings = new DiagramSettings(defaultSettings);
}
/*
* @see org.tigris.gef.persistence.pgml.HandlerFactory#getHandler(
* HandlerStack, Object, String, String, String, Attributes)
*/
@Override
public DefaultHandler getHandler(HandlerStack stack,
Object container,
String uri,
String localname,
String qname,
Attributes attributes)
throws SAXException {
String href = attributes.getValue("href");
Object owner = null;
if (href != null) {
owner = findOwner(href);
if (owner == null) {
LOG.warn("Found href of "
+ href
+ " with no matching element in model");
return null;
}
}
// Ignore non-private elements within FigNode groups
if (container instanceof FigGroupHandler) {
FigGroup group = ((FigGroupHandler) container).getFigGroup();
if (group instanceof FigNode && !qname.equals("private")) {
return null;
}
}
// Handle ItemUID in container contents
if (qname.equals("private") && (container instanceof Container)) {
return new PrivateHandler(this, (Container) container);
}
DefaultHandler handler =
super.getHandler(stack, container, uri, localname, qname,
attributes);
if (handler instanceof FigEdgeHandler) {
return new org.argouml.persistence.FigEdgeHandler(
this, ((FigEdgeHandler) handler).getFigEdge());
}
return handler;
}
/*
* @see org.tigris.gef.persistence.pgml.PGMLStackParser#setAttrs(
* org.tigris.gef.presentation.Fig, org.xml.sax.Attributes)
*/
@Override
protected final void setAttrs(Fig f, Attributes attrList)
throws SAXException {
if (f instanceof FigGroup) {
FigGroup group = (FigGroup) f;
String clsNameBounds = attrList.getValue("description");
if (clsNameBounds != null) {
StringTokenizer st =
new StringTokenizer(clsNameBounds, ",;[] ");
// Discard class name, x y w h
if (st.hasMoreElements()) {
st.nextToken();
}
if (st.hasMoreElements()) {
st.nextToken();
}
if (st.hasMoreElements()) {
st.nextToken();
}
if (st.hasMoreElements()) {
st.nextToken();
}
if (st.hasMoreElements()) {
st.nextToken();
}
Map<String, String> attributeMap = interpretStyle(st);
setStyleAttributes(group, attributeMap);
}
}
// TODO: Attempt to move the following code to GEF
String name = attrList.getValue("name");
if (name != null && !name.equals("")) {
registerFig(f, name);
}
setCommonAttrs(f, attrList);
final String href = attrList.getValue("href");
if (href != null && !href.equals("")) {
Object modelElement = findOwner(href);
if (modelElement == null) {
LOG.error("Can't find href of " + href);
throw new SAXException("Found href of " + href
+ " with no matching element in model");
}
// The owner should always have already been set in the constructor
if (f.getOwner() != modelElement) {
// Assign nodes immediately but edges later. See issue 4310.
if (f instanceof FigEdge) {
modelElementsByFigEdge.put((FigEdge) f, modelElement);
} else {
f.setOwner(modelElement);
}
} else {
LOG.debug("Ignoring href on " + f.getClass().getName()
+ " as it's already set");
}
}
}
/**
* The StringTokenizer is expected to be positioned at the start of a string
* of style identifiers in the format name=value;name=value;name=value....
* Each name value pair is interpreted and the Fig configured accordingly.
* The value is optional and will default to a value applicable for its
* name.
* The current applicable names are operationsVisible and attributesVisible
* and are used to show or hide the compartments within Class and Interface.
* The default values are true.
* @param st The StrinkTokenizer positioned at the first style identifier
* @return a map of attributes
*/
private Map<String, String> interpretStyle(StringTokenizer st) {
Map<String, String> map = new HashMap<String, String>();
String name;
String value;
while (st.hasMoreElements()) {
String namevaluepair = st.nextToken();
int equalsPos = namevaluepair.indexOf('=');
if (equalsPos < 0) {
name = namevaluepair;
value = "true";
} else {
name = namevaluepair.substring(0, equalsPos);
value = namevaluepair.substring(equalsPos + 1);
}
map.put(name, value);
}
return map;
}
/**
* Set the fig style attributes. This should move into
* the render factories as described in issue 859.
* @param fig the fig to style.
* @param attributeMap a map of name value pairs
*/
private void setStyleAttributes(Fig fig, Map<String, String> attributeMap) {
for (Map.Entry<String, String> entry : attributeMap.entrySet()) {
final String name = entry.getKey();
final String value = entry.getValue();
if ("operationsVisible".equals(name)) {
((OperationsCompartmentContainer) fig)
.setOperationsVisible(value.equalsIgnoreCase("true"));
} else if ("attributesVisible".equals(name)) {
((AttributesCompartmentContainer) fig)
.setAttributesVisible(value.equalsIgnoreCase("true"));
} else if ("stereotypeVisible".equals(name)) {
((StereotypeContainer) fig)
.setStereotypeVisible(value.equalsIgnoreCase("true"));
} else if ("visibilityVisible".equals(name)) {
((VisibilityContainer) fig)
.setVisibilityVisible(value.equalsIgnoreCase("true"));
} else if ("pathVisible".equals(name)) {
((PathContainer) fig)
.setPathVisible(value.equalsIgnoreCase("true"));
} else if ("extensionPointVisible".equals(name)) {
((ExtensionsCompartmentContainer) fig)
.setExtensionPointVisible(value.equalsIgnoreCase("true"));
}
}
}
/**
* Read and parse the input stream to create a new diagram and return it.
*
* @param is the input stream
* @param closeStream true to close the stream when parsing is complete
* @return the diagram created as a result of the parse
* @throws SAXException
*/
public ArgoDiagram readArgoDiagram(InputStream is, boolean closeStream)
throws SAXException {
return (ArgoDiagram) readDiagram(is, closeStream);
}
@Override
public Diagram readDiagram(InputStream is, boolean closeStream)
throws SAXException {
// TODO: we really want to be able replace the initial content handler
// which is passed to SAX, but we can't do this without cloning a
// whole bunch of code because it's private in the super class.
Diagram d = super.readDiagram(is, closeStream);
attachEdges(d);
return d;
}
/**
* This is called when all nodes and edges have been read and placed on
* the diagram. This method then attaches the edges to the correct node,
* including the nodes contained within edges allowing edge to edge
* connections for comment edges, association classes and dependencies.
* @param d the Diagram
*/
private void attachEdges(Diagram d) {
for (EdgeData edgeData : figEdges) {
final FigEdge edge = edgeData.getFigEdge();
Object modelElement = modelElementsByFigEdge.get(edge);
if (modelElement != null) {
if (edge.getOwner() == null) {
edge.setOwner(modelElement);
}
}
}
for (EdgeData edgeData : figEdges) {
final FigEdge edge = edgeData.getFigEdge();
Fig sourcePortFig = findFig(edgeData.getSourcePortFigId());
Fig destPortFig = findFig(edgeData.getDestPortFigId());
final FigNode sourceFigNode =
getFigNode(edgeData.getSourceFigNodeId());
final FigNode destFigNode =
getFigNode(edgeData.getDestFigNodeId());
if (sourceFigNode instanceof FigEdgePort) {
sourcePortFig = sourceFigNode;
}
if (destFigNode instanceof FigEdgePort) {
destPortFig = destFigNode;
}
if (sourcePortFig == null && sourceFigNode != null) {
sourcePortFig = getPortFig(sourceFigNode);
}
if (destPortFig == null && destFigNode != null) {
destPortFig = getPortFig(destFigNode);
}
if (sourcePortFig == null
|| destPortFig == null
|| sourceFigNode == null
|| destFigNode == null) {
LOG.error("Can't find nodes for FigEdge: "
+ edge.getId() + ":"
+ edge.toString());
edge.removeFromDiagram();
} else {
edge.setSourcePortFig(sourcePortFig);
edge.setDestPortFig(destPortFig);
edge.setSourceFigNode(sourceFigNode);
edge.setDestFigNode(destFigNode);
}
}
// Once all edges are connected do a compute route on each to make
// sure that annotations and the edge port is positioned correctly
// Only do this after all edges are connected as compute route
// requires all edges to be connected to nodes.
// TODO: It would be nice not to have to do this and restore annotation
// positions instead.
for (Object edge : d.getLayer().getContentsEdgesOnly()) {
FigEdge figEdge = (FigEdge) edge;
figEdge.computeRouteImpl();
}
}
// TODO: Move to GEF
/**
* Store data of a FigEdge together with the id's of nodes to connect to
* @param figEdge The FigEdge
* @param sourcePortFigId The id of the source port
* @param destPortFigId The id of the destination port
* @param sourceFigNodeId The id of the source node
* @param destFigNodeId The id of the destination node
*/
public void addFigEdge(
final FigEdge figEdge,
final String sourcePortFigId,
final String destPortFigId,
final String sourceFigNodeId,
final String destFigNodeId) {
figEdges.add(new EdgeData(figEdge, sourcePortFigId, destPortFigId,
sourceFigNodeId, destFigNodeId));
}
// TODO: Move to GEF
/**
* Get the FigNode that the fig id represents.
*
* @param figId (In the form Figx.y.z)
* @return the FigNode with the given id
* @throws IllegalStateException
* if the figId supplied is not of a FigNode
*/
private FigNode getFigNode(String figId) throws IllegalStateException {
if (figId.contains(".")) {
// If the id does not look like a top-level Fig then we can assume
// that this is an id of a FigEdgePort inside some FigEdge.
// So extract the FigEdgePort from the FigEdge and return that as
// the FigNode.
figId = figId.substring(0, figId.indexOf('.'));
FigEdgeModelElement edge = (FigEdgeModelElement) findFig(figId);
if (edge == null) {
throw new IllegalStateException(
"Can't find a FigNode with id " + figId);
}
edge.makeEdgePort();
return edge.getEdgePort();
} else {
// If there is no dot then this must be a top level Fig and can be
// assumed to be a FigNode.
Fig f = findFig(figId);
if (f instanceof FigNode) {
return (FigNode) f;
} else {
LOG.error("FigID " + figId + " is not a node, edge ignored");
return null;
}
}
}
// TODO: Move to GEF
/**
* Get the Fig from the FigNode that is the port.
*
* @param figNode the FigNode
* @return the Fig that is the port on the given FigNode
*/
private Fig getPortFig(FigNode figNode) {
if (figNode instanceof FigEdgePort) {
// TODO: Can we just do this every time, no need for else - Bob
return figNode;
} else {
return (Fig) figNode.getPortFigs().get(0);
}
}
// TODO: Move to GEF
/**
* The data from an edge extracted from the PGML before we can guarantee
* all the nodes have been constructed. This stores the FigEdge and the
* id's of the nodes to connect to later.
* If the nodes are not known then the ports are returned instead.
*/
private class EdgeData {
private final FigEdge figEdge;
private final String sourcePortFigId;
private final String destPortFigId;
private final String sourceFigNodeId;
private final String destFigNodeId;
/**
* Constructor
* @param edge The FigEdge
* @param sourcePortId The id of the source port
* @param destPortId The id of the destination port
* @param sourceNodeId The id of the source node
* @param destNodeId The id of the destination node
*/
public EdgeData(FigEdge edge, String sourcePortId,
String destPortId, String sourceNodeId, String destNodeId) {
if (sourcePortId == null || destPortId == null) {
throw new IllegalArgumentException(
"source port and dest port must not be null"
+ " source = " + sourcePortId
+ " dest = " + destPortId
+ " figEdge = " + edge);
}
this.figEdge = edge;
this.sourcePortFigId = sourcePortId;
this.destPortFigId = destPortId;
this.sourceFigNodeId =
sourceNodeId != null ? sourceNodeId : sourcePortId;
this.destFigNodeId =
destNodeId != null ? destNodeId : destPortId;
}
/**
* Get the id of the destination FigNode
* @return the id
*/
public String getDestFigNodeId() {
return destFigNodeId;
}
/**
* Get the id of the destination port
* @return the id
*/
public String getDestPortFigId() {
return destPortFigId;
}
/**
* Get the FigEdge
* @return the FigEdge
*/
public FigEdge getFigEdge() {
return figEdge;
}
/**
* Get the id of the source FigNode
* @return the id
*/
public String getSourceFigNodeId() {
return sourceFigNodeId;
}
/**
* Get the id of the source port
* @return the id
*/
public String getSourcePortFigId() {
return sourcePortFigId;
}
}
/**
* Construct a new instance of the named Fig with the owner represented
* by the given href and the bounds parsed from the PGML file. We look
* for constructors of the form Fig(Object owner, Rectangle
* bounds, DiagramSettings settings) which is typically used for subclasses
* of FigNodeModelElement, then Fig(Object owner, DiagramSettings settings)
* which is used for subclasses of FigEdgeModelElement.
* <p>
* If we fail to find any of the constructors that we know about, we'll
* call GEF's version of this method to see if it can find a constructor.
*
* @param className fully qualified name of class to instantiate
* @param href string representing UUID of owning element
* @param bounds position and size of figure
* @return
* @throws SAXException
* @see org.tigris.gef.persistence.pgml.PGMLStackParser#constructFig(java.lang.String, java.lang.String, java.awt.Rectangle)
*/
@Override
protected Fig constructFig(String className, String href, Rectangle bounds)
throws SAXException {
Fig f = null;
try {
Class figClass = Class.forName(className);
for (Constructor constructor : figClass.getConstructors()) {
Class[] parameterTypes = constructor.getParameterTypes();
// FigNodeModelElements should match here
if (parameterTypes.length == 3
&& parameterTypes[0].equals(Object.class)
&& parameterTypes[1].equals(Rectangle.class)
&& parameterTypes[2].equals(DiagramSettings.class)
) {
Object parameters[] = new Object[3];
Object owner = null;
if (href != null) {
owner = findOwner(href);
}
parameters[0] = owner;
parameters[1] = bounds;
parameters[2] =
((ArgoDiagram) getDiagram()).getDiagramSettings();
f = (Fig) constructor.newInstance(parameters);
}
// FigEdgeModelElements should match here (they have no bounds)
if (parameterTypes.length == 2
&& parameterTypes[0].equals(Object.class)
&& parameterTypes[1].equals(DiagramSettings.class)
) {
Object parameters[] = new Object[2];
Object owner = null;
if (href != null) {
owner = findOwner(href);
}
parameters[0] = owner;
parameters[1] =
((ArgoDiagram) getDiagram()).getDiagramSettings();
f = (Fig) constructor.newInstance(parameters);
}
}
} catch (ClassNotFoundException e) {
throw new SAXException(e);
} catch (IllegalAccessException e) {
throw new SAXException(e);
} catch (InstantiationException e) {
throw new SAXException(e);
} catch (InvocationTargetException e) {
throw new SAXException(e);
}
// Fall back to GEF's handling if we couldn't find an appropriate
// constructor
if (f == null) {
// TODO: Convert this to a warning or error when all the Figs
// have been upgraded.
LOG.debug("No ArgoUML constructor found for " + className
+ " falling back to GEF's default constructors");
f = super.constructFig(className, href, bounds);
}
return f;
}
/**
* Save the newly created Diagram for use by the parser. We take the
* opportunity to attach our default diagram settings to it so we'll have
* them if needed when constructing Figs.
* <p>
* Diagrams are created in GEF's PGMLHandler.initDiagram() which is private
* and can't be overridden. Initialization sequence is:<ul>
* <li>load diagram class using name in PGML file
* <li>instantiate using 0-arg constructor
* <li>invoke this method (setDiagram(<newDiagramInstance))
* <li>invoke diagram's initialize(Object owner) method
* <li>diagram.setName()
* <li>diagram.setScale()
* <li>diagram.setShowSingleMultiplicity()
* (?!why does GEF care about multiplicity?!)
* </ul>
* @param diagram the new diagram
* @see org.tigris.gef.persistence.pgml.PGMLStackParser#setDiagram(org.tigris.gef.base.Diagram)
*/
@Override
public void setDiagram(Diagram diagram) {
// TODO: We could generalize this to initialize more stuff if needed
((ArgoDiagram) diagram).setDiagramSettings(getDiagramSettings());
super.setDiagram(diagram);
}
public DiagramSettings getDiagramSettings() {
return diagramSettings;
}
}