/* Alloy Analyzer 4 -- Copyright (c) 2006-2009, Felix Chang
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
* (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
* OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package edu.mit.csail.sdg.alloy4viz;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import edu.mit.csail.sdg.alloy4.Util;
import edu.mit.csail.sdg.alloy4.XMLNode;
import edu.mit.csail.sdg.alloy4graph.DotColor;
import edu.mit.csail.sdg.alloy4graph.DotPalette;
import edu.mit.csail.sdg.alloy4graph.DotShape;
import edu.mit.csail.sdg.alloy4graph.DotStyle;
/** This utility class contains methods to read and write VizState customizations.
*
* <p><b>Thread Safety:</b> Can be called only by the AWT event thread.
*/
public final class StaticThemeReaderWriter {
/** Constructor is private, since this utility class never needs to be instantiated. */
private StaticThemeReaderWriter() { }
/** Read the XML file and merge its settings into an existing VizState object. */
public static void readAlloy(String filename, VizState theme) throws IOException {
File file = new File(filename);
try {
XMLNode elem = new XMLNode(file);
for(XMLNode sub: elem.getChildren("view")) parseView(sub,theme);
} catch(Throwable e) {
throw new IOException("The file \""+file.getPath()+"\" is not a valid XML file, or an error occurred in reading.");
}
}
/** Write the VizState's customizations into a new file (which will be overwritten if it exists). */
public static void writeAlloy(String filename, VizState theme) throws IOException {
PrintWriter bw = new PrintWriter(filename,"UTF-8");
bw.write("<?xml version=\"1.0\"?>\n<alloy>\n\n");
if (theme!=null) {
try {
writeView(bw, theme);
} catch(IOException ex) {
Util.close(bw);
throw new IOException("Error writing to the file \""+filename+"\"");
}
}
bw.write("\n</alloy>\n");
if (!Util.close(bw)) throw new IOException("Error writing to the file \""+filename+"\"");
}
/*============================================================================================*/
/** Does nothing if the element is malformed. */
private static void parseView(final XMLNode x, VizState now) {
/*
* <view orientation=".." nodetheme=".." edgetheme=".." hidePrivate="yes/no" hideMeta="yes/no" useOriginalAtomNames="yes/no" fontsize="12">
* <projection> .. </projection>
* <defaultnode../>
* <defaultedge../>
* 0 or more NODE or EDGE
* </view>
*/
if (!x.is("view")) return;
for(XMLNode xml:x) {
if (xml.is("projection")) {
now.deprojectAll();
for(AlloyType t:parseProjectionList(now,xml)) now.project(t);
}
}
if (has(x,"useOriginalAtomNames")) now.useOriginalName(getbool(x,"useOriginalAtomNames"));
if (has(x,"hidePrivate")) now.hidePrivate(getbool(x,"hidePrivate"));
if (has(x,"hideMeta")) now.hideMeta(getbool(x,"hideMeta"));
if (has(x,"fontsize")) now.setFontSize(getint(x,"fontsize"));
if (has(x,"nodetheme")) now.setNodePalette(parseDotPalette(x,"nodetheme"));
if (has(x,"edgetheme")) now.setEdgePalette(parseDotPalette(x,"edgetheme"));
for(XMLNode xml:x) {
if (xml.is("defaultnode")) parseNodeViz(xml, now, null);
else if (xml.is("defaultedge")) parseEdgeViz(xml, now, null);
else if (xml.is("node")) {
for(XMLNode sub:xml.getChildren("type")) {
AlloyType t=parseAlloyType(now,sub); if (t!=null) parseNodeViz(xml, now, t);
}
for(XMLNode sub:xml.getChildren("set")) {
AlloySet s=parseAlloySet(now,sub); if (s!=null) parseNodeViz(xml, now, s);
}
}
else if (xml.is("edge")) {
for(XMLNode sub:xml.getChildren("relation")) {
AlloyRelation r=parseAlloyRelation(now,sub); if (r!=null) parseEdgeViz(xml, now, r);
}
}
}
}
/*============================================================================================*/
/** Writes nothing if the argument is null. */
private static void writeView(PrintWriter out, VizState view) throws IOException {
if (view==null) return;
VizState defaultView=new VizState(view.getOriginalInstance());
out.write("<view");
writeDotPalette(out, "nodetheme", view.getNodePalette(), defaultView.getNodePalette());
writeDotPalette(out, "edgetheme", view.getEdgePalette(), defaultView.getEdgePalette());
if (view.useOriginalName()!=defaultView.useOriginalName()) {
out.write(" useOriginalAtomNames=\"");
out.write(view.useOriginalName() ? "yes" : "no");
out.write("\"");
}
if (view.hidePrivate()!=defaultView.hidePrivate()) {
out.write(" hidePrivate=\"");
out.write(view.hidePrivate() ? "yes" : "no");
out.write("\"");
}
if (view.hideMeta()!=defaultView.hideMeta()) {
out.write(" hideMeta=\"");
out.write(view.hideMeta() ? "yes" : "no");
out.write("\"");
}
if (view.getFontSize()!=defaultView.getFontSize()) {
out.write(" fontsize=\""+view.getFontSize()+"\"");
}
out.write(">\n");
if (view.getProjectedTypes().size()>0) writeProjectionList(out, view.getProjectedTypes());
out.write("\n<defaultnode" + writeNodeViz(view, defaultView, null));
out.write("/>\n\n<defaultedge" + writeEdgeViz(view, defaultView, null));
out.write("/>\n");
// === nodes ===
Set<AlloyNodeElement> types = new TreeSet<AlloyNodeElement>();
types.addAll(view.getOriginalModel().getTypes());
types.addAll(view.getCurrentModel().getTypes());
types.addAll(view.getOriginalModel().getSets());
types.addAll(view.getCurrentModel().getSets());
Map<String,Set<AlloyNodeElement>> viz2node=new TreeMap<String,Set<AlloyNodeElement>>();
for(AlloyNodeElement t:types) {
String str=writeNodeViz(view,defaultView,t);
Set<AlloyNodeElement> nodes=viz2node.get(str);
if (nodes==null) viz2node.put(str, nodes=new TreeSet<AlloyNodeElement>());
nodes.add(t);
}
for(Map.Entry<String,Set<AlloyNodeElement>> e:viz2node.entrySet()) {
out.write("\n<node"+e.getKey()+">\n");
for(AlloyNodeElement ts:e.getValue()) {
if (ts instanceof AlloyType) writeAlloyType(out,(AlloyType)ts);
else if (ts instanceof AlloySet) writeAlloySet(out,(AlloySet)ts);
}
out.write("</node>\n");
}
// === edges ===
Set<AlloyRelation> rels = new TreeSet<AlloyRelation>();
rels.addAll(view.getOriginalModel().getRelations());
rels.addAll(view.getCurrentModel().getRelations());
Map<String,Set<AlloyRelation>> viz2edge=new TreeMap<String,Set<AlloyRelation>>();
for(AlloyRelation r:rels) {
String str=writeEdgeViz(view,defaultView,r);
if (str.length()==0) continue;
Set<AlloyRelation> edges=viz2edge.get(str);
if (edges==null) viz2edge.put(str, edges=new TreeSet<AlloyRelation>());
edges.add(r);
}
for(Map.Entry<String,Set<AlloyRelation>> e:viz2edge.entrySet()) {
out.write("\n<edge"+e.getKey()+">\n");
for(AlloyRelation r:e.getValue()) writeAlloyRelation(out,r);
out.write("</edge>\n");
}
// === done ===
out.write("\n</view>\n");
}
/*============================================================================================*/
/** Return null if the element is malformed. */
private static AlloyType parseAlloyType(VizState now, XMLNode x) {
/* class AlloyType implements AlloyNodeElement {
* String name;
* }
* <type name="the type name"/>
*/
if (!x.is("type")) return null;
String name=x.getAttribute("name");
if (name.length()==0) return null; else return now.getCurrentModel().hasType(name);
}
/** Writes nothing if the argument is null. */
private static void writeAlloyType(PrintWriter out, AlloyType x) throws IOException {
if (x!=null) Util.encodeXMLs(out, " <type name=\"", x.getName(), "\"/>\n");
}
/*============================================================================================*/
/** Return null if the element is malformed. */
private static AlloySet parseAlloySet(VizState now, XMLNode x) {
/* class AlloySet implements AlloyNodeElement {
* String name;
* AlloyType type;
* }
* <set name="name" type="name"/>
*/
if (!x.is("set")) return null;
String name=x.getAttribute("name"), type=x.getAttribute("type");
if (name.length()==0 || type.length()==0) return null;
AlloyType t=now.getCurrentModel().hasType(type);
if (t==null) return null; else return now.getCurrentModel().hasSet(name, t);
}
/** Writes nothing if the argument is null. */
private static void writeAlloySet(PrintWriter out, AlloySet x) throws IOException {
if (x!=null) Util.encodeXMLs(out," <set name=\"",x.getName(),"\" type=\"",x.getType().getName(),"\"/>\n");
}
/*============================================================================================*/
/** Return null if the element is malformed. */
private static AlloyRelation parseAlloyRelation(VizState now, XMLNode x) {
/*
* <relation name="name">
* 2 or more <type name=".."/>
* </relation>
*/
List<AlloyType> ans=new ArrayList<AlloyType>();
if (!x.is("relation")) return null;
String name=x.getAttribute("name");
if (name.length()==0) return null;
for(XMLNode sub:x.getChildren("type")) {
String typename=sub.getAttribute("name");
if (typename.length()==0) return null;
AlloyType t = now.getCurrentModel().hasType(typename);
if (t==null) return null;
ans.add(t);
}
if (ans.size()<2) return null; else return now.getCurrentModel().hasRelation(name, ans);
}
/** Writes nothing if the argument is null. */
private static void writeAlloyRelation(PrintWriter out, AlloyRelation x) throws IOException {
if (x==null) return;
Util.encodeXMLs(out, " <relation name=\"", x.getName(), "\">");
for(AlloyType t:x.getTypes()) Util.encodeXMLs(out, " <type name=\"", t.getName(), "\"/>");
out.write(" </relation>\n");
}
/*============================================================================================*/
/** Always returns a nonnull (though possibly empty) set of AlloyType. */
private static Set<AlloyType> parseProjectionList(VizState now, XMLNode x) {
/*
* <projection>
* 0 or more <type name=".."/>
* </projection>
*/
Set<AlloyType> ans=new TreeSet<AlloyType>();
if (x.is("projection")) for(XMLNode sub:x.getChildren("type")) {
String name=sub.getAttribute("name");
if (name.length()==0) continue;
AlloyType t = now.getOriginalModel().hasType(name);
if (t!=null) ans.add(t);
}
return ans;
}
/** Writes an empty Projection tag if the argument is null or empty */
private static void writeProjectionList(PrintWriter out, Set<AlloyType> types) throws IOException {
if (types==null || types.size()==0) { out.write("\n<projection/>\n"); return; }
out.write("\n<projection>");
for(AlloyType t:types) Util.encodeXMLs(out, " <type name=\"", t.getName(), "\"/>");
out.write(" </projection>\n");
}
/*============================================================================================*/
/** Do nothing if the element is malformed; note: x can be null. */
private static void parseNodeViz(XMLNode xml, VizState view, AlloyNodeElement x) {
/*
* <node visible="inherit/yes/no" label=".." color=".." shape=".." style=".."
* showlabel="inherit/yes/no" showinattr="inherit/yes/no"
* hideunconnected="inherit/yes/no" nubmeratoms="inherit/yes/no">
* zero or more SET or TYPE
* </node>
*
* Each attribute, if omitted, means "no change".
* Note: BOOLEAN is tristate.
*/
if (has(xml,"visible")) view.nodeVisible.put (x, getbool(xml, "visible"));
if (has(xml,"hideunconnected")) view.hideUnconnected.put (x, getbool(xml, "hideunconnected"));
if (x==null || x instanceof AlloySet) {
AlloySet s=(AlloySet)x;
if (has(xml,"showlabel")) view.showAsLabel.put (s, getbool(xml, "showlabel"));
if (has(xml,"showinattr")) view.showAsAttr.put (s, getbool(xml, "showinattr"));
}
if (x==null || x instanceof AlloyType) {
AlloyType t=(AlloyType)x;
if (has(xml,"numberatoms")) view.number.put (t, getbool(xml, "numberatoms"));
}
if (has(xml,"style")) view.nodeStyle.put(x, parseDotStyle(xml));
if (has(xml,"color")) view.nodeColor.put(x, parseDotColor(xml));
if (has(xml,"shape")) view.shape .put(x, parseDotShape(xml));
if (has(xml,"label")) view.label .put(x, xml.getAttribute("label"));
}
/** Returns the String representation of an AlloyNodeElement's settings. */
private static String writeNodeViz(VizState view, VizState defaultView, AlloyNodeElement x) throws IOException {
StringWriter sw=new StringWriter();
PrintWriter out=new PrintWriter(sw);
writeBool(out, "visible", view.nodeVisible.get(x), defaultView.nodeVisible.get(x));
writeBool(out, "hideunconnected", view.hideUnconnected.get(x), defaultView.hideUnconnected.get(x));
if (x==null || x instanceof AlloySet) {
AlloySet s=(AlloySet)x;
writeBool(out, "showlabel", view.showAsLabel.get(s), defaultView.showAsLabel.get(s));
writeBool(out, "showinattr", view.showAsAttr.get(s), defaultView.showAsAttr.get(s));
}
if (x==null || x instanceof AlloyType) {
AlloyType t=(AlloyType)x;
writeBool(out, "numberatoms", view.number.get(t), defaultView.number.get(t));
}
writeDotStyle(out, view.nodeStyle.get(x), defaultView.nodeStyle.get(x));
writeDotShape(out, view.shape.get(x), defaultView.shape.get(x));
writeDotColor(out, view.nodeColor.get(x), defaultView.nodeColor.get(x));
if (x!=null && !view.label.get(x).equals(defaultView.label.get(x)))
Util.encodeXMLs(out, " label=\"", view.label.get(x), "\"");
if (out.checkError()) throw new IOException("PrintWriter IO Exception!");
return sw.toString();
}
/*============================================================================================*/
/** Do nothing if the element is malformed; note: x can be null. */
private static void parseEdgeViz(XMLNode xml, VizState view, AlloyRelation x) {
/*
* <edge visible="inherit/yes/no" label=".." color=".." style=".." weight=".." constraint=".."
* attribute="inherit/yes/no" merge="inherit/yes/no" layout="inherit/yes/no">
* zero or more RELATION
* </edge>
*
* Each attribute, if omitted, means "no change".
* Note: BOOLEAN is tristate.
*/
if (has(xml,"visible")) view.edgeVisible.put (x, getbool(xml,"visible"));
if (has(xml,"attribute")) view.attribute .put (x, getbool(xml,"attribute"));
if (has(xml,"merge")) view.mergeArrows.put (x, getbool(xml,"merge"));
if (has(xml,"layout")) view.layoutBack .put (x, getbool(xml,"layout"));
if (has(xml,"constraint")) view.constraint .put (x, getbool(xml,"constraint"));
if (has(xml,"style")) view.edgeStyle .put (x, parseDotStyle(xml));
if (has(xml,"color")) view.edgeColor .put (x, parseDotColor(xml));
if (has(xml,"weight")) view.weight .put (x, getint (xml,"weight"));
if (has(xml,"label")) view.label .put (x, xml.getAttribute("label"));
}
/** Returns the String representation of an AlloyRelation's settings. */
private static String writeEdgeViz(VizState view, VizState defaultView, AlloyRelation x) throws IOException {
StringWriter sw=new StringWriter();
PrintWriter out=new PrintWriter(sw);
writeDotColor(out, view.edgeColor.get(x), defaultView.edgeColor.get(x));
writeDotStyle(out, view.edgeStyle.get(x), defaultView.edgeStyle.get(x));
writeBool(out, "visible", view.edgeVisible.get(x), defaultView.edgeVisible.get(x));
writeBool(out, "merge", view.mergeArrows.get(x), defaultView.mergeArrows.get(x));
writeBool(out, "layout", view.layoutBack.get(x), defaultView.layoutBack.get(x));
writeBool(out, "attribute", view.attribute.get(x), defaultView.attribute.get(x));
writeBool(out, "constraint",view.constraint.get(x), defaultView.constraint.get(x));
if (view.weight.get(x) != defaultView.weight.get(x)) out.write(" weight=\"" + view.weight.get(x) + "\"");
if (x!=null && !view.label.get(x).equals(defaultView.label.get(x)))
Util.encodeXMLs(out, " label=\"", view.label.get(x), "\"");
if (out.checkError()) throw new IOException("PrintWriter IO Exception!");
return sw.toString();
}
/*============================================================================================*/
/** Returns null if the attribute doesn't exist, or is malformed. */
private static DotPalette parseDotPalette(XMLNode x, String key) {
return DotPalette.parse(x.getAttribute(key));
}
/** Writes nothing if value==defaultValue. */
private static void writeDotPalette(PrintWriter out, String key, DotPalette value, DotPalette defaultValue) throws IOException {
if (value!=defaultValue) Util.encodeXMLs(out, " "+key+"=\"", value==null?"inherit":value.toString(), "\"");
}
/*============================================================================================*/
/** Returns null if the attribute doesn't exist, or is malformed. */
private static DotColor parseDotColor(XMLNode x) { return DotColor.parse(x.getAttribute("color")); }
/** Writes nothing if value==defaultValue. */
private static void writeDotColor(PrintWriter out, DotColor value, DotColor defaultValue) throws IOException {
if (value!=defaultValue) Util.encodeXMLs(out, " color=\"", value==null?"inherit":value.toString(), "\"");
}
/*============================================================================================*/
/** Returns null if the attribute doesn't exist, or is malformed. */
private static DotShape parseDotShape(XMLNode x) { return DotShape.parse(x.getAttribute("shape")); }
/** Writes nothing if value==defaultValue. */
private static void writeDotShape(PrintWriter out, DotShape value, DotShape defaultValue) throws IOException {
if (value!=defaultValue) Util.encodeXMLs(out, " shape=\"", value==null?"inherit":value.toString(), "\"");
}
/*============================================================================================*/
/** Returns null if the attribute doesn't exist, or is malformed. */
private static DotStyle parseDotStyle(XMLNode x) { return DotStyle.parse(x.getAttribute("style")); }
/** Writes nothing if value==defaultValue. */
private static void writeDotStyle(PrintWriter out, DotStyle value, DotStyle defaultValue) throws IOException {
if (value!=defaultValue) Util.encodeXMLs(out, " style=\"", value==null?"inherit":value.toString(), "\"");
}
/*============================================================================================*/
/** Returns null if the attribute doesn't exist, or is malformed. */
private static Boolean getbool(XMLNode x, String attr) {
String value=x.getAttribute(attr);
if (value.equalsIgnoreCase("yes") || value.equalsIgnoreCase("true")) return Boolean.TRUE;
if (value.equalsIgnoreCase("no") || value.equalsIgnoreCase("false")) return Boolean.FALSE;
return null;
}
/** Writes nothing if the value is equal to the default value. */
private static void writeBool(PrintWriter out, String key, Boolean value, Boolean defaultValue) throws IOException {
if (value==null && defaultValue==null) return;
if (value!=null && defaultValue!=null && value.booleanValue()==defaultValue.booleanValue()) return;
out.write(' ');
out.write(key);
if (value==null) out.write("=\"inherit\""); else out.write(value ? "=\"yes\"":"=\"no\"");
}
/*============================================================================================*/
/** Returns true if the XML element has the given attribute. */
private static boolean has(XMLNode x, String attr) {
return x.getAttribute(attr,null)!=null;
}
/** Returns 0 if the attribute doesn't exist, or is malformed. */
private static int getint(XMLNode x, String attr) {
String value=x.getAttribute(attr);
int i;
try {
i=Integer.parseInt(value);
} catch(NumberFormatException ex) {
i=0;
}
return i;
}
}