content: /* 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, "hideSkolem"))
              now.hideSkolem(getbool(x, "hideSkolem"));
          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.hideSkolem() != defaultView.hideSkolem()) {
              out.write(" hideSkolem=\"");
              out.write(view.hideSkolem() ? "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;
      }
  }
  