Java tutorial
// Diagram.java // See toplevel license.txt for copyright and license terms. package ded.model; import java.awt.Color; import java.awt.Dimension; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.StringReader; import java.io.Writer; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; import util.FlattenInputStream; import util.Util; import util.XParse; import util.awt.AWTJSONUtil; import util.json.JSONable; /** Complete diagram. */ public class Diagram implements JSONable { // ---------- constants ------------ /** Value of the "type" attribute in toplevel JSON object. This * should never be changed. */ public static final String jsonType = "Diagram Editor Diagram"; /** Value to write as the "version" attribute in toplevel JSON * object, and maximum value we can read there. This should * be bumped every time something is added or changed in the * file format that would cause an older version of this code * to be unable to read the current files. That includes * adding new enumerators to existing enumerations; although * the serialization code does not literally change, its * behavior does, because it then reads and writes a new * string value for that enumerator. Even adding a new field * should include a bump--even though the old code might be * able to read the file without choking, the semantics would * not be preserved. */ public static final int currentFileVersion = 13; // ---------- public data ------------ /** Size of window to display diagram. Some elements might not fit * in the current size. * * Currently, this is the size of the visible content area. The * surrounding window is generally larger, but that depends on * the window system. */ public Dimension windowSize; /** When true, the editor will paint the diagram file name in the * upper-left corner of the editing area, and also include it * when exporting to other file formats. Normally true. */ public boolean drawFileName; /** Entities, in display order. The last entity will appear on top * of all others. */ public ArrayList<Entity> entities; /** Inheritance nodes. */ public ArrayList<Inheritance> inheritances; /** Relations. */ public ArrayList<Relation> relations; /** Map from color names to Colors. */ public LinkedHashMap<String, Color> namedColors; // ----------- public methods ----------- public Diagram() { this.windowSize = new Dimension(800, 800); this.drawFileName = true; this.entities = new ArrayList<Entity>(); this.inheritances = new ArrayList<Inheritance>(); this.relations = new ArrayList<Relation>(); this.namedColors = makeDefaultColors(); } public static LinkedHashMap<String, Color> makeDefaultColors() { LinkedHashMap<String, Color> m = new LinkedHashMap<String, Color>(); // These colors are non-standard. I chose them manually, // based on factors like readability, garishness and // ability to differentiate from each other. One of them // is the same as the selection color, which introduces // some ambiguity, but it is a really nice color so I do // not want to lose it in either place. m.put("Gray", new Color(192, 192, 192)); m.put("White", Color.WHITE); m.put("Light Gray", new Color(224, 224, 224)); m.put("Orange", new Color(236, 125, 70)); m.put("Yellow", new Color(234, 236, 52)); m.put("Green", new Color(76, 222, 76)); m.put("Sky Blue", new Color(135, 193, 255)); // Selection color. m.put("Purple", new Color(161, 140, 237)); m.put("Pink", new Color(227, 120, 236)); m.put("Red", new Color(248, 50, 50)); // Very intense... return m; } public void selfCheck() { for (Relation r : this.relations) { r.globalSelfCheck(this); } for (Inheritance i : this.inheritances) { i.globalSelfCheck(this); } } // ------------------ serialization -------------------- @Override public JSONObject toJSON() { JSONObject o = new JSONObject(); try { o.put("type", jsonType); o.put("version", currentFileVersion); o.put("windowSize", AWTJSONUtil.dimensionToJSON(this.windowSize)); o.put("drawFileName", this.drawFileName); // Map from an entity to its position in the serialized // 'entities' array, so it can be referenced by inheritances // and relations. HashMap<Entity, Integer> entityToInteger = new HashMap<Entity, Integer>(); // Entities. JSONArray arr = new JSONArray(); int index = 0; for (Entity e : this.entities) { entityToInteger.put(e, index++); arr.put(e.toJSON()); } o.put("entities", arr); // Map from inheritance to serialized position. HashMap<Inheritance, Integer> inheritanceToInteger = new HashMap<Inheritance, Integer>(); // Inheritances. arr = new JSONArray(); index = 0; for (Inheritance inh : this.inheritances) { inheritanceToInteger.put(inh, index++); arr.put(inh.toJSON(entityToInteger)); } o.put("inheritances", arr); // Relations. arr = new JSONArray(); index = 0; for (Relation rel : this.relations) { arr.put(rel.toJSON(entityToInteger, inheritanceToInteger)); } o.put("relations", arr); } catch (JSONException e) { assert (false); } return o; } /** Deserialize from 'o'. */ public Diagram(JSONObject o) throws JSONException { String type = o.getString("type"); if (!type.equals(jsonType)) { throw new JSONException("unexpected file type: \"" + type + "\""); } int ver = (int) o.getLong("version"); if (ver < 1) { throw new JSONException("Invalid file version: " + ver + ". Valid version " + "numbers are and will always be positive."); } else if (ver > currentFileVersion) { throw new JSONException("The file has version " + ver + " but the largest version this program is capable of " + "reading is " + currentFileVersion + ". You need to get " + "a later version of the program in order to read " + "this file."); } this.windowSize = AWTJSONUtil.dimensionFromJSON(o.getJSONObject("windowSize")); this.namedColors = makeDefaultColors(); if (ver >= 3) { this.drawFileName = o.getBoolean("drawFileName"); } else { this.drawFileName = true; } // Make the lists now; this is particularly useful for handling // older file formats. this.entities = new ArrayList<Entity>(); this.inheritances = new ArrayList<Inheritance>(); this.relations = new ArrayList<Relation>(); // Map from serialized position to deserialized Entity. ArrayList<Entity> integerToEntity = new ArrayList<Entity>(); // Entities. JSONArray a = o.getJSONArray("entities"); for (int i = 0; i < a.length(); i++) { Entity e = new Entity(a.getJSONObject(i), ver); this.entities.add(e); integerToEntity.add(e); } if (ver >= 2) { // Map from serialized position to deserialized Inheritance. ArrayList<Inheritance> integerToInheritance = new ArrayList<Inheritance>(); // Inheritances. a = o.getJSONArray("inheritances"); for (int i = 0; i < a.length(); i++) { Inheritance inh = new Inheritance(a.getJSONObject(i), integerToEntity); this.inheritances.add(inh); integerToInheritance.add(inh); } // Relations. a = o.getJSONArray("relations"); for (int i = 0; i < a.length(); i++) { Relation rel = new Relation(a.getJSONObject(i), integerToEntity, integerToInheritance, ver); this.relations.add(rel); } } } /** Write this diagram to the specified file. */ public void saveToFile(String fname) throws Exception { JSONObject serialized = this.toJSON(); FileOutputStream fos = new FileOutputStream(fname); try { Writer w = new BufferedWriter(new OutputStreamWriter(fos, "UTF-8")); try { serialized.write(w, 2, 0); w.append('\n'); } finally { w.close(); fos = null; } } finally { if (fos != null) { fos.close(); } } } /** Read a Diagram from a file, expect the JSON format only. */ public static Diagram readFromFile(String fname) throws Exception { FileInputStream fis = new FileInputStream(fname); try { Reader r = new BufferedReader(new InputStreamReader(fis, "UTF-8")); try { return readFromReader(r); } finally { r.close(); fis = null; } } finally { if (fis != null) { fis.close(); } } } /** Read Diagram JSON out of 'r'. */ public static Diagram readFromReader(Reader r) throws Exception { // Parse the raw characters into a JSON tree. JSONObject obj = new JSONObject(new JSONTokener(r)); // Parse the JSON tree into a Diagram object graph. return new Diagram(obj); } /** Serialize as a JSON string. */ public String toJSONString() { return this.toJSON().toString(); } /** Deserialize a JSON string; may throw JSONException. */ public static Diagram parseJSONString(String json) throws JSONException { return new Diagram(new JSONObject(new JSONTokener(new StringReader(json)))); } /** Read a diagram from a file and return the new Diagram object. * This will auto-detect the ER or JSON file formats and read * the file appropriately. */ public static Diagram readFromFileAutodetect(String fname) throws Exception { // For compatibility with the C++ implementation, first attempt // to read it in the ER format. Diagram d = readFromERFile(fname); if (d != null) { return d; } else { // The file is not in the ER format. Proceed with reading // it as JSON. Exceptions will propagate out of // this method, as they indicate that the file *was* in the // ER format but some other problem occurred (or the file // could not be read at all, which is a problem no matter // what format we think the file is). } return readFromFile(fname); } // ------------------ legacy deserialization ------------------------- /** Read a Diagram from an ER file and return the new Diagram object. * Return null if the file is not in the ER format; throw an * exception for all other problems. */ public static Diagram readFromERFile(String fname) throws XParse, IOException { InputStream is = null; try { is = new FileInputStream(fname); return readFromERStream(is); } finally { if (is != null) { is.close(); } } } /** Read a Diagram from an ER InputStream. This will return null in * the one specific case where the file exists and is readable, but * the magic number is not present, meaning the file is probably * not in the ER format at all. */ public static Diagram readFromERStream(InputStream is) throws XParse, IOException { FlattenInputStream flat = new FlattenInputStream(is); // Magic number identifier for the file format. int magic = flat.readInt(); if (magic != 0x2B044C63) { // The file is not in the expected format. return null; } // File format version number. int ver = flat.readInt(); if (!(1 <= ver && ver <= 8)) { throw new XParse("ER file format version is " + ver + " but I only know how to read 1 through 8."); } flat.version = ver; return new Diagram(flat); } /** Read a Diagram from an ER FlattenInputStream */ public Diagram(FlattenInputStream flat) throws XParse, IOException { // Defaults if file does not specify. this.drawFileName = true; this.entities = new ArrayList<Entity>(); this.inheritances = new ArrayList<Inheritance>(); this.relations = new ArrayList<Relation>(); this.namedColors = makeDefaultColors(); if (flat.version >= 5) { this.windowSize = flat.readDimension(); } else { // Default size from C++ code. this.windowSize = new Dimension(400, 300); } // Entities { int numEntities = flat.readInt(); for (int i = 0; i < numEntities; i++) { Entity e = new Entity(flat); flat.noteOwner(e); this.entities.add(e); } } flat.checkpoint(0x64E2C40F); // Inheritances if (flat.version >= 7) { int numInheritances = flat.readInt(); for (int i = 0; i < numInheritances; i++) { Inheritance inh = new Inheritance(flat); flat.noteOwner(inh); this.inheritances.add(inh); } flat.checkpoint(0x144CB789); } // Relations { int numRelations = flat.readInt(); for (int i = 0; i < numRelations; i++) { Relation r = new Relation(flat); this.relations.add(r); } } flat.checkpoint(0x378264D9); this.selfCheck(); // In the ER format, I needed to add titles manually. But in // Ded, that is automatic. So, look for a title entity and // remove it. for (Entity e : this.entities) { if (e.loc.x == 0 && e.loc.y == 0 && e.attributes.equals(" ") && e.shape == EntityShape.ES_NO_SHAPE) { // Looks like a title; remove it. this.entities.remove(e); // Paranoia: make sure we didn't mess up the Diagram // by doing that. That would happen if there were a // Relation connected to the title. try { this.selfCheck(); } catch (AssertionError ae) { throw new RuntimeException( "Oops, I broke the file by removing the title element! " + "Complain to Scott. :)"); } // Cannot keep iterating, since we just modified the // collection we are iterating over. break; } } } // ------------------ data object boilerplate ------------------------ @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (this.getClass() == obj.getClass()) { Diagram d = (Diagram) obj; return this.windowSize.equals(d.windowSize) && this.drawFileName == d.drawFileName && this.entities.equals(d.entities) && this.inheritances.equals(d.inheritances) && this.relations.equals(d.relations) && this.namedColors.equals(d.namedColors); } return false; } @Override public int hashCode() { int h = 1; h = h * 31 + this.windowSize.hashCode(); h = h * 31 + (this.drawFileName ? 1 : 0); h = h * 31 + Util.collectionHashCode(this.entities); h = h * 31 + Util.collectionHashCode(this.inheritances); h = h * 31 + Util.collectionHashCode(this.relations); h = h * 31 + this.namedColors.hashCode(); return h; } @Override public String toString() { return this.toJSONString(); } } // EOF