From bc464ba42bb231b3529522a9da6ebfb9199c3830 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Wed, 27 Oct 2021 13:00:55 +0200 Subject: [PATCH] Removed useless admin stuff, changed to save files as JSON, added image scaling for exports. --- README.md | 5 + design/main_interface.PNG | Bin 9943 -> 8865 bytes design/sample_export.png | Bin 1797 -> 14895 bytes pom.xml | 12 +- .../erme/EntityRelationMappingEditor.java | 21 +-- .../control/actions/ExportToImageAction.java | 30 +++- .../actions/HtmlDocumentViewerAction.java | 3 + .../erme/control/actions/LoadAction.java | 25 +-- .../erme/control/actions/SaveAction.java | 31 ++-- .../nl/andrewlalis/erme/model/Attribute.java | 5 +- .../andrewlalis/erme/model/MappingModel.java | 149 ++++++++++++++++-- .../nl/andrewlalis/erme/model/Relation.java | 11 +- .../java/nl/andrewlalis/erme/util/Hash.java | 50 ------ .../andrewlalis/erme/view/DiagramPanel.java | 5 +- .../nl/andrewlalis/erme/view/EditorFrame.java | 17 +- .../andrewlalis/erme/view/EditorMenuBar.java | 5 +- src/main/resources/admin_hash.txt | 1 - src/main/resources/html/instructions.html | 2 +- src/main/resources/icon.png | Bin 0 -> 4167 bytes 19 files changed, 247 insertions(+), 125 deletions(-) delete mode 100644 src/main/java/nl/andrewlalis/erme/util/Hash.java delete mode 100644 src/main/resources/admin_hash.txt create mode 100644 src/main/resources/icon.png diff --git a/README.md b/README.md index bed16db..f055f68 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # Entity-Relation Mapping Editor A simple UI for editing entity-relation mapping diagrams. +## Usage +This program is distributed as an executable **jar** file. You can find the latest release [here](https://github.com/andrewlalis/EntityRelationMappingEditor/releases). You will need Java installed on your computer (version 8 or higher). [You can install an OpenJDK version of Java here.](https://adoptium.net/) + +Simply double-click on the jar file, or execute `java -jar ` from the command line (where `` is replaced with the path to your actual jar file). + ![window screenshot](https://raw.githubusercontent.com/andrewlalis/EntityRelationMappingEditor/main/design/main_interface.PNG) ## How to Use diff --git a/design/main_interface.PNG b/design/main_interface.PNG index bca62de234cbaf9d05d67025c12431b607eb75f5..79e16ab5b494f3b096a504b414f039bff5fef41f 100644 GIT binary patch literal 8865 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 A7ytkO literal 9943 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 diff --git a/design/sample_export.png b/design/sample_export.png index 96e323f2106cb039cb7ea1103e188b374b7a1bf4..360f25cf8038ad99c3e82b411d1e00030ad522e0 100644 GIT binary patch literal 14895 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 DDFuJO literal 1797 zcmZ9Ndr*@38ppvS+FBNBs~e?ZDjKGCXj=>~|PN7|ci>2>4Lb#%7*GCQUgZ4-Sm23o_y= zuU<)PLDvjKKWC4f;b&oUcI0cUU97chI257AQV*qiGn#I>A)8j9t<;1X6R_0?fND=wAE6)?Zgx|9~ zl!>G8O8CIYVIagFG!Kz+4^d+=SW2@61yRHYLXto=q30VaNOuHHFdrjXN0rO(Plc9u zc(&Be2qiCBwM!+Piyq1O=h{W|#GLWAK2;t8^}11rguZ`%W>?J_pkh9~^@~o>#)df? zE%aPoTzbn`6MS1H-w=+e|BcTq6s|4cf+uDZp3!xIU%hzjWc9A!_e&hhE`5>S6TjEL zjn?S-E*s1wsQlONTzE;9XY~c?zW{WuN5b+Pp+Lptx_qv&}rx3&Abb- zCSNIqex#^v>Ul}QMsfJ?jAD*U8hBdGVDC1!@9-_*&_zi}r}LS!+gP(GW(GglHHc%l zwr_D({@U+&QmcoEEKW3LzVl&^<5G`l2_*F5l>VtlSyDaBhgNPy9mg#ZB_j$u0X!iMQ!2xp^xf$5+Y&I zZPI9Ry%PSZBR<*$jXg+({l8bx*eu7~vDF{%`hI6-$UYDB1q4thQ@|U4I}5e&YUfOI z+s(E=xXE{Mj5NO6T5=uMZ@zz@RO$#XccvfcbxExL_*IKFw@IU$P=AaV%~bg_gP5YDo07kbco;4*R&)R>z!5C4V`410>Y9mlx=Inx>f7w5~HDHamD|clQK|es~CXcEw zr+CfZP?z9?9|aDJqHaq!4rqIRk`ryP{xo%jCya7SO`q09XNT2BM0(U+JLfci(!Eb2 zksbYHc4!4U2=SC>oWG)KPm`cNdn7%A43TU_>SyEw)y04VmI9ICwtrB--i-zv{vg)y zGJoZ677|hd1>?;25eaRYP%U~>tU|o6HWjNRsiq*Kdo}Bm*(Z~xQ39d9A_ z;LLp;5j1kS9%y+_-m+C?nf$Oy?ytdFU789}=9Z>1T1g8FMFW`#*2^ZK?}z)oy*yH} zC8@&656=-JpBx&#W1a}9Du0j#!YRL=62BT;r&3;4ZO05Sa1!I%^Ys=?%W7e&F15(4Zshz4m|lrZ@AaJMXV; z_^0wfmAA6W3_dT8aosaC#YkN`EYQ02WTQL98BE4~uMI=jf^+`9)9d#ueByX?{W(`8 zQ)nc^DpmzGhcv%7ZDV20pbjwf{gKgIY;WF{;MeYn3EqW{PS2lrJR{Lf%ZL_K-S%}s zZPJDGv!}{CnCZ=zN6^M*T1*NeI>Oo9s=xifa95sqk)Wp4DzM1&Ow#l!ethm39!a=#xG+MQ_lAE!!Ujx6Y siOe5w@FRka@8BSNw9EmU<-ELwe4K}g5nhnl.andrewlalis EntityRelationMappingEditor - 1.5.0 + 1.6.0 @@ -50,13 +50,19 @@ com.formdev flatlaf - 1.0-rc3 + 1.6.1 org.projectlombok lombok - 1.18.16 + 1.18.22 provided + + + com.fasterxml.jackson.core + jackson-databind + 2.13.0 + \ No newline at end of file diff --git a/src/main/java/nl/andrewlalis/erme/EntityRelationMappingEditor.java b/src/main/java/nl/andrewlalis/erme/EntityRelationMappingEditor.java index 75fcb5f..faaf929 100644 --- a/src/main/java/nl/andrewlalis/erme/EntityRelationMappingEditor.java +++ b/src/main/java/nl/andrewlalis/erme/EntityRelationMappingEditor.java @@ -1,31 +1,16 @@ package nl.andrewlalis.erme; import com.formdev.flatlaf.FlatLightLaf; -import nl.andrewlalis.erme.util.Hash; import nl.andrewlalis.erme.view.EditorFrame; -import java.nio.charset.StandardCharsets; - public class EntityRelationMappingEditor { - public static final String VERSION = "1.5.0"; + public static final String VERSION = "1.6.0"; public static void main(String[] args) { - if (!FlatLightLaf.install()) { + if (!FlatLightLaf.setup()) { System.err.println("Could not install FlatLight Look and Feel."); } - final boolean includeAdminActions = shouldIncludeAdminActions(args); - if (includeAdminActions) { - System.out.println("Admin actions have been enabled."); - } - EditorFrame frame = new EditorFrame(includeAdminActions); + EditorFrame frame = new EditorFrame(); frame.setVisible(true); } - - private static boolean shouldIncludeAdminActions(String[] args) { - if (args.length < 1) { - return false; - } - byte[] pw = args[0].getBytes(StandardCharsets.UTF_8); - return Hash.matches(pw, "admin_hash.txt"); - } } diff --git a/src/main/java/nl/andrewlalis/erme/control/actions/ExportToImageAction.java b/src/main/java/nl/andrewlalis/erme/control/actions/ExportToImageAction.java index 9531830..c106ebb 100644 --- a/src/main/java/nl/andrewlalis/erme/control/actions/ExportToImageAction.java +++ b/src/main/java/nl/andrewlalis/erme/control/actions/ExportToImageAction.java @@ -4,7 +4,6 @@ import lombok.Setter; import nl.andrewlalis.erme.model.MappingModel; import nl.andrewlalis.erme.model.Relation; import nl.andrewlalis.erme.view.DiagramPanel; -import nl.andrewlalis.erme.view.view_models.AttributeViewModel; import nl.andrewlalis.erme.view.view_models.MappingModelViewModel; import javax.imageio.ImageIO; @@ -77,9 +76,18 @@ public class ExportToImageAction extends AbstractAction { } else { chosenFile = new File(chosenFile.getParent(), chosenFile.getName() + '.' + extension); } + String input = JOptionPane.showInputDialog(this.diagramPanel, "Choose a scale for the image.", "3.0"); + float scale; + try { + scale = Float.parseFloat(input); + if (scale <= 0.0f || scale > 64.0f) throw new IllegalArgumentException(); + } catch (Exception ex) { + JOptionPane.showMessageDialog(this.diagramPanel, "Invalid scale value. Should be a positive number less than 64.", "Invalid Scale", JOptionPane.WARNING_MESSAGE); + return; + } try { long start = System.currentTimeMillis(); - BufferedImage render = this.renderModel(); + BufferedImage render = this.renderModel(scale); double durationSeconds = (System.currentTimeMillis() - start) / 1000.0; ImageIO.write(render, extension, chosenFile); prefs.put(LAST_EXPORT_LOCATION_KEY, chosenFile.getAbsolutePath()); @@ -97,12 +105,19 @@ public class ExportToImageAction extends AbstractAction { } } - private BufferedImage renderModel() { + /** + * Renders the mapping model to an image with the given resolution. + * @param scale The scale to use. Should be greater than zero. + * @return The image which was rendered. + */ + private BufferedImage renderModel(float scale) { // Prepare a tiny sample image that we can use to determine the bounds of the model in a graphics context. BufferedImage bufferedImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); Graphics2D g2d = bufferedImage.createGraphics(); DiagramPanel.prepareGraphics(g2d); final Rectangle bounds = this.model.getViewModel().getBounds(g2d); + bounds.width *= scale; + bounds.height *= scale; // Prepare the output image. BufferedImage outputImage = new BufferedImage(bounds.width, bounds.height + 20, BufferedImage.TYPE_INT_RGB); @@ -112,7 +127,10 @@ public class ExportToImageAction extends AbstractAction { // Transform the graphics space to account for the model's offset from origin. AffineTransform originalTransform = g2d.getTransform(); - g2d.setTransform(AffineTransform.getTranslateInstance(-bounds.x, -bounds.y)); + AffineTransform modelTransform = new AffineTransform(); + modelTransform.scale(scale, scale); + modelTransform.translate(-bounds.x, -bounds.y); + g2d.setTransform(modelTransform); DiagramPanel.prepareGraphics(g2d); // Render the model. @@ -124,9 +142,9 @@ public class ExportToImageAction extends AbstractAction { this.model.getRelations().forEach(r -> r.setSelected(selectedRelations.contains(r))); LolcatAction.getInstance().setLolcatEnabled(lolcat); // revert previous lolcat mode - // Revert back to the normal image space, and render a watermark. + // Revert to the normal image space, and render a watermark. g2d.setTransform(originalTransform); - g2d.setColor(Color.LIGHT_GRAY); + g2d.setColor(Color.decode("#e8e8e8")); g2d.setFont(g2d.getFont().deriveFont(10.0f)); g2d.drawString("Created by EntityRelationMappingEditor", 0, outputImage.getHeight() - 3); return outputImage; diff --git a/src/main/java/nl/andrewlalis/erme/control/actions/HtmlDocumentViewerAction.java b/src/main/java/nl/andrewlalis/erme/control/actions/HtmlDocumentViewerAction.java index b23dcf9..640238e 100644 --- a/src/main/java/nl/andrewlalis/erme/control/actions/HtmlDocumentViewerAction.java +++ b/src/main/java/nl/andrewlalis/erme/control/actions/HtmlDocumentViewerAction.java @@ -13,6 +13,9 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.net.URISyntaxException; +/** + * An action which, when performed, opens a view that displays an HTML document. + */ public abstract class HtmlDocumentViewerAction extends AbstractAction { private final String resourceFileName; private final Dialog.ModalityType modalityType; diff --git a/src/main/java/nl/andrewlalis/erme/control/actions/LoadAction.java b/src/main/java/nl/andrewlalis/erme/control/actions/LoadAction.java index 1e85b14..d4e18c7 100644 --- a/src/main/java/nl/andrewlalis/erme/control/actions/LoadAction.java +++ b/src/main/java/nl/andrewlalis/erme/control/actions/LoadAction.java @@ -1,19 +1,22 @@ package nl.andrewlalis.erme.control.actions; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Setter; import nl.andrewlalis.erme.model.MappingModel; import nl.andrewlalis.erme.view.DiagramPanel; import javax.swing.*; import javax.swing.filechooser.FileNameExtensionFilter; -import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.io.File; import java.io.FileInputStream; -import java.io.IOException; -import java.io.ObjectInputStream; import java.util.prefs.Preferences; public class LoadAction extends AbstractAction { @@ -40,8 +43,8 @@ public class LoadAction extends AbstractAction { public void actionPerformed(ActionEvent e) { JFileChooser fileChooser = new JFileChooser(); FileNameExtensionFilter filter = new FileNameExtensionFilter( - "ERME Serialized Files", - "erme" + "JSON Files", + "json" ); fileChooser.setFileFilter(filter); Preferences prefs = Preferences.userNodeForPackage(LoadAction.class); @@ -56,11 +59,15 @@ public class LoadAction extends AbstractAction { JOptionPane.showMessageDialog(fileChooser, "The selected file cannot be read.", "Invalid File", JOptionPane.WARNING_MESSAGE); return; } - try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(chosenFile))) { - MappingModel loadedModel = (MappingModel) ois.readObject(); - this.diagramPanel.setModel(loadedModel); + try (FileInputStream fis = new FileInputStream(chosenFile)) { + ObjectMapper mapper = JsonMapper.builder() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true) + .build(); + JsonNode data = mapper.readValue(fis, JsonNode.class); + this.diagramPanel.setModel(MappingModel.fromJson((ObjectNode) data)); prefs.put(LAST_LOAD_LOCATION_KEY, chosenFile.getAbsolutePath()); - } catch (IOException | ClassNotFoundException | ClassCastException ex) { + } catch (Exception ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(fileChooser, "An error occurred and the file could not be read:\n" + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); } diff --git a/src/main/java/nl/andrewlalis/erme/control/actions/SaveAction.java b/src/main/java/nl/andrewlalis/erme/control/actions/SaveAction.java index 2310d55..7c8d261 100644 --- a/src/main/java/nl/andrewlalis/erme/control/actions/SaveAction.java +++ b/src/main/java/nl/andrewlalis/erme/control/actions/SaveAction.java @@ -1,19 +1,21 @@ package nl.andrewlalis.erme.control.actions; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; import lombok.Setter; import nl.andrewlalis.erme.model.MappingModel; import nl.andrewlalis.erme.view.DiagramPanel; import javax.swing.*; import javax.swing.filechooser.FileNameExtensionFilter; -import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.io.ObjectOutputStream; import java.util.prefs.Preferences; public class SaveAction extends AbstractAction { @@ -29,6 +31,7 @@ public class SaveAction extends AbstractAction { @Setter private MappingModel model; + @Setter private DiagramPanel diagramPanel; @@ -42,8 +45,8 @@ public class SaveAction extends AbstractAction { public void actionPerformed(ActionEvent e) { JFileChooser fileChooser = new JFileChooser(); FileNameExtensionFilter filter = new FileNameExtensionFilter( - "ERME Serialized Files", - "erme" + "JSON Files", + "json" ); fileChooser.setFileFilter(filter); Preferences prefs = Preferences.userNodeForPackage(SaveAction.class); @@ -55,15 +58,23 @@ public class SaveAction extends AbstractAction { if (choice == JFileChooser.APPROVE_OPTION) { File chosenFile = fileChooser.getSelectedFile(); if (chosenFile == null || chosenFile.isDirectory()) { - JOptionPane.showMessageDialog(fileChooser, "The selected file cannot be written to.", "Invalid File", JOptionPane.WARNING_MESSAGE); + JOptionPane.showMessageDialog(this.diagramPanel, "The selected file cannot be written to.", "Invalid File", JOptionPane.WARNING_MESSAGE); return; } - if (!chosenFile.exists() && !chosenFile.getName().endsWith(".erme")) { - chosenFile = new File(chosenFile.getParent(), chosenFile.getName() + ".erme"); + if (!chosenFile.exists() && !chosenFile.getName().endsWith(".json")) { + chosenFile = new File(chosenFile.getParent(), chosenFile.getName() + ".json"); + } else if (chosenFile.exists()) { + int result = JOptionPane.showConfirmDialog(this.diagramPanel, "Are you sure you want overwrite this file?", "Overwrite", JOptionPane.YES_NO_OPTION); + if (result == JOptionPane.NO_OPTION) { + return; + } } - // TODO: Check for confirm before overwriting. - try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(chosenFile))) { - oos.writeObject(this.model); + try (FileOutputStream fos = new FileOutputStream(chosenFile)) { + ObjectMapper mapper = JsonMapper.builder() + .configure(SerializationFeature.INDENT_OUTPUT, true) + .configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true) + .build(); + mapper.writeValue(fos, this.model.toJson(mapper)); prefs.put(LAST_SAVE_LOCATION_KEY, chosenFile.getAbsolutePath()); JOptionPane.showMessageDialog(fileChooser, "File saved successfully.", "Success", JOptionPane.INFORMATION_MESSAGE); } catch (IOException ex) { diff --git a/src/main/java/nl/andrewlalis/erme/model/Attribute.java b/src/main/java/nl/andrewlalis/erme/model/Attribute.java index 4f9bcb0..515222b 100644 --- a/src/main/java/nl/andrewlalis/erme/model/Attribute.java +++ b/src/main/java/nl/andrewlalis/erme/model/Attribute.java @@ -3,14 +3,13 @@ package nl.andrewlalis.erme.model; import lombok.Getter; import nl.andrewlalis.erme.view.view_models.AttributeViewModel; -import java.io.Serializable; import java.util.Objects; /** * A single value that belongs to a relation. */ @Getter -public class Attribute implements Serializable { +public class Attribute { private final Relation relation; private AttributeType type; private String name; @@ -32,7 +31,7 @@ public class Attribute implements Serializable { this.name = name; this.relation.getModel().fireChangedEvent(); } - + public AttributeViewModel getViewModel() { if (this.viewModel == null) { this.viewModel = new AttributeViewModel(this); diff --git a/src/main/java/nl/andrewlalis/erme/model/MappingModel.java b/src/main/java/nl/andrewlalis/erme/model/MappingModel.java index b7763d0..545ff92 100644 --- a/src/main/java/nl/andrewlalis/erme/model/MappingModel.java +++ b/src/main/java/nl/andrewlalis/erme/model/MappingModel.java @@ -1,29 +1,27 @@ package nl.andrewlalis.erme.model; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Getter; -import nl.andrewlalis.erme.view.OrderableListPanel; import nl.andrewlalis.erme.view.view_models.MappingModelViewModel; import nl.andrewlalis.erme.view.view_models.ViewModel; import java.awt.*; -import java.io.Serializable; -import java.util.HashSet; import java.util.List; -import java.util.Objects; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; /** * This model contains all the information about a single mapping diagram, * including each mapped table and the links between them. */ -public class MappingModel implements Serializable, Viewable { +public class MappingModel implements Viewable { @Getter private final Set relations; - private transient Set changeListeners; - - private final static long serialVersionUID = 6153776381873250304L; + private transient final Set changeListeners; public MappingModel() { this.relations = new HashSet<>(); @@ -42,10 +40,20 @@ public class MappingModel implements Serializable, Viewable { } } + /** + * Gets the list of relations which are currently selected. + * @return The list of relations which are selected. + */ public List getSelectedRelations() { return this.relations.stream().filter(Relation::isSelected).collect(Collectors.toList()); } + /** + * Finds an attribute in this model, or returns null otherwise. + * @param relationName The name of the relation the attribute is in. + * @param attributeName The name of the attribute. + * @return The attribute which was found, or null if none was found. + */ public Attribute findAttribute(String relationName, String attributeName) { for (Relation r : this.getRelations()) { if (!r.getName().equals(relationName)) continue; @@ -58,6 +66,11 @@ public class MappingModel implements Serializable, Viewable { return null; } + /** + * Removes all attributes from any relation in the model which reference the + * given attribute. + * @param referenced The attribute to remove references from. + */ public void removeAllReferencingAttributes(Attribute referenced) { for (Relation r : this.getRelations()) { Set removalSet = new HashSet<>(); @@ -73,6 +86,10 @@ public class MappingModel implements Serializable, Viewable { } } + /** + * Gets the bounding rectangle around all relations of the model. + * @return The bounding rectangle around all relations in this model. + */ public Rectangle getRelationBounds() { if (this.getRelations().isEmpty()) { return new Rectangle(0, 0, 0, 0); @@ -90,14 +107,20 @@ public class MappingModel implements Serializable, Viewable { return new Rectangle(minX, minY, maxX - minX, maxY - minY); } + /** + * Adds a listener to this model, which will be notified of changes to the + * model. + * @param listener The listener to add. + */ public void addChangeListener(ModelChangeListener listener) { - if (this.changeListeners == null) { - this.changeListeners = new HashSet<>(); - } this.changeListeners.add(listener); listener.onModelChanged(); } + /** + * Fires an all-purpose event which notifies all listeners that the model + * has changed. + */ public final void fireChangedEvent() { this.changeListeners.forEach(ModelChangeListener::onModelChanged); } @@ -146,4 +169,106 @@ public class MappingModel implements Serializable, Viewable { this.getRelations().forEach(r -> c.addRelation(r.copy(c))); return c; } + + public ObjectNode toJson(ObjectMapper mapper) { + ObjectNode node = mapper.createObjectNode(); + ArrayNode relationsArray = node.withArray("relations"); + for (Relation r : this.relations) { + ObjectNode relationNode = mapper.createObjectNode() + .put("name", r.getName()); + ObjectNode positionNode = mapper.createObjectNode() + .put("x", r.getPosition().x) + .put("y", r.getPosition().y); + relationNode.set("position", positionNode); + ArrayNode attributesArray = relationNode.withArray("attributes"); + for (Attribute a : r.getAttributes()) { + ObjectNode attributeNode = mapper.createObjectNode() + .put("name", a.getName()) + .put("type", a.getType().name()); + if (a instanceof ForeignKeyAttribute) { + ForeignKeyAttribute fk = (ForeignKeyAttribute) a; + ObjectNode referenceNode = mapper.createObjectNode() + .put("relation", fk.getReference().getRelation().getName()) + .put("attribute", fk.getReference().getName()); + attributeNode.set("references", referenceNode); + } + attributesArray.add(attributeNode); + } + relationsArray.add(relationNode); + } + return node; + } + + public static MappingModel fromJson(ObjectNode node) { + MappingModel model = new MappingModel(); + for (JsonNode relationNodeRaw : node.withArray("relations")) { + if (!relationNodeRaw.isObject()) throw new IllegalArgumentException(); + ObjectNode relationNode = (ObjectNode) relationNodeRaw; + String name = relationNode.get("name").asText(); + int x = relationNode.get("position").get("x").asInt(); + int y = relationNode.get("position").get("y").asInt(); + Point position = new Point(x, y); + Relation relation = new Relation(model, position, name); + for (JsonNode attributeNodeRaw : relationNode.withArray("attributes")) { + if (!attributeNodeRaw.isObject()) throw new IllegalArgumentException(); + ObjectNode attributeNode = (ObjectNode) attributeNodeRaw; + String attributeName = attributeNode.get("name").asText(); + AttributeType type = AttributeType.valueOf(attributeNode.get("type").asText().toUpperCase()); + Attribute attribute = new Attribute(relation, type, attributeName); + relation.addAttribute(attribute); + } + model.addRelation(relation); + } + addForeignKeys(model, node); + return model; + } + + private static void addForeignKeys(MappingModel model, ObjectNode node) { + Map references = buildReferenceMap(model, node); + while (!references.isEmpty()) { + boolean workDone = false; + for (Map.Entry entry : references.entrySet()) { + Attribute attribute = entry.getKey(); + String referencedName = entry.getValue().get("attribute").asText(); + String referencedRelation = entry.getValue().get("relation").asText(); + Attribute referencedAttribute = model.findAttribute(referencedRelation, referencedName); + if (referencedAttribute == null) throw new IllegalArgumentException("Foreign key referenced unknown attribute."); + if (!references.containsKey(referencedAttribute)) { + ForeignKeyAttribute fk = new ForeignKeyAttribute(attribute.getRelation(), attribute.getType(), attribute.getName(), referencedAttribute); + attribute.getRelation().removeAttribute(attribute); + attribute.getRelation().addAttribute(fk); + references.remove(attribute); + workDone = true; + } + } + if (!workDone) { + throw new IllegalArgumentException("Invalid foreign key structure. Possible cyclic references."); + } + } + } + + /** + * Builds a map that contains the set of foreign key references, indexed by + * the primitive attribute that is referencing another. + * @param model The model to lookup attributes from. + * @param node The raw JSON data for the model. + * @return A map containing foreign key references, to be used to build a + * complete model with foreign key attributes. + */ + private static Map buildReferenceMap(MappingModel model, ObjectNode node) { + Map references = new HashMap<>(); + for (JsonNode r : node.withArray("relations")) { + for (JsonNode a : r.withArray("attributes")) { + if (a.has("references") && a.get("references").isObject()) { + ObjectNode referenceNode = (ObjectNode) a.get("references"); + String attributeName = a.get("name").asText(); + String relationName = r.get("name").asText(); + Attribute attribute = model.findAttribute(relationName, attributeName); + if (attribute == null) throw new IllegalArgumentException("Mapping model is not complete. Missing attribute " + attributeName + " in relation " + relationName + "."); + references.put(attribute, referenceNode); + } + } + } + return references; + } } diff --git a/src/main/java/nl/andrewlalis/erme/model/Relation.java b/src/main/java/nl/andrewlalis/erme/model/Relation.java index 8393002..4a48350 100644 --- a/src/main/java/nl/andrewlalis/erme/model/Relation.java +++ b/src/main/java/nl/andrewlalis/erme/model/Relation.java @@ -5,7 +5,6 @@ import nl.andrewlalis.erme.view.view_models.RelationViewModel; import nl.andrewlalis.erme.view.view_models.ViewModel; import java.awt.*; -import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -15,7 +14,7 @@ import java.util.stream.Collectors; * Represents a single "relation" or table in the diagram. */ @Getter -public class Relation implements Serializable, Viewable, Comparable { +public class Relation implements Viewable, Comparable { private final MappingModel model; private Point position; private String name; @@ -24,11 +23,15 @@ public class Relation implements Serializable, Viewable, Comparable { private transient boolean selected; private transient RelationViewModel viewModel; - public Relation(MappingModel model, Point position, String name) { + public Relation(MappingModel model, Point position, String name, List attributes) { this.model = model; this.position = position; this.name = name; - this.attributes = new ArrayList<>(); + this.attributes = attributes; + } + + public Relation(MappingModel model, Point position, String name) { + this(model, position, name, new ArrayList<>()); } public void setPosition(Point position) { diff --git a/src/main/java/nl/andrewlalis/erme/util/Hash.java b/src/main/java/nl/andrewlalis/erme/util/Hash.java deleted file mode 100644 index 2e9a25a..0000000 --- a/src/main/java/nl/andrewlalis/erme/util/Hash.java +++ /dev/null @@ -1,50 +0,0 @@ -package nl.andrewlalis.erme.util; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; - -public class Hash { - public static boolean matches(byte[] password, String resourceFile) { - MessageDigest md; - try { - md = MessageDigest.getInstance("SHA-256"); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - return false; - } - byte[] passwordHash = md.digest(password); - InputStream is = Hash.class.getClassLoader().getResourceAsStream(resourceFile); - if (is == null) { - System.err.println("Could not obtain input stream to admin_hash.txt"); - return false; - } - char[] buffer = new char[64]; - try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) { - if (br.read(buffer) != buffer.length) { - System.err.println("Incorrect number of characters read from hash file."); - return false; - } - } catch (IOException e) { - e.printStackTrace(); - return false; - } - String hashHex = String.valueOf(buffer); - byte[] hash = hexStringToByteArray(hashHex); - return Arrays.equals(passwordHash, hash); - } - - private static byte[] hexStringToByteArray(String s) { - int len = s.length(); - byte[] data = new byte[len / 2]; - for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) - + Character.digit(s.charAt(i+1), 16)); - } - return data; - } -} diff --git a/src/main/java/nl/andrewlalis/erme/view/DiagramPanel.java b/src/main/java/nl/andrewlalis/erme/view/DiagramPanel.java index 5e4e8b6..7e3735a 100644 --- a/src/main/java/nl/andrewlalis/erme/view/DiagramPanel.java +++ b/src/main/java/nl/andrewlalis/erme/view/DiagramPanel.java @@ -117,6 +117,7 @@ public class DiagramPanel extends JPanel implements ModelChangeListener { /** * Updates all the action singletons with the latest model information. + * TODO: Clean this up somehow! */ private void updateActionModels() { NewModelAction.getInstance().setDiagramPanel(this); @@ -134,10 +135,10 @@ public class DiagramPanel extends JPanel implements ModelChangeListener { RemoveAttributeAction.getInstance().setDiagramPanel(this); LoadSampleModelAction.getInstance().setDiagramPanel(this); LolcatAction.getInstance().setDiagramPanel(this); - AutoPositionAction.getInstance().setDiagramPanel(this); + AutoPositionAction.getInstance().setDiagramPanel(this); AutoPositionAction.getInstance().setModel(this.model); OrderableListPanel.getInstance().setModel(this.model); - AboutAction.getInstance().setDiagramPanel(this); + AboutAction.getInstance().setDiagramPanel(this); ExitAction.getInstance().setDiagramPanel(this); InstructionsAction.getInstance().setDiagramPanel(this); MappingAlgorithmHelpAction.getInstance().setDiagramPanel(this); diff --git a/src/main/java/nl/andrewlalis/erme/view/EditorFrame.java b/src/main/java/nl/andrewlalis/erme/view/EditorFrame.java index 8bf536d..1f8435d 100644 --- a/src/main/java/nl/andrewlalis/erme/view/EditorFrame.java +++ b/src/main/java/nl/andrewlalis/erme/view/EditorFrame.java @@ -2,17 +2,30 @@ package nl.andrewlalis.erme.view; import nl.andrewlalis.erme.model.MappingModel; +import javax.imageio.ImageIO; import javax.swing.*; import java.awt.*; +import java.io.IOException; +import java.io.InputStream; /** * The main JFrame for the editor. */ public class EditorFrame extends JFrame { - public EditorFrame(boolean includeAdminActions) { + public EditorFrame() { super("ER-Mapping Editor"); this.setContentPane(new DiagramPanel(new MappingModel())); - this.setJMenuBar(new EditorMenuBar(includeAdminActions)); + this.setJMenuBar(new EditorMenuBar()); + try { + InputStream is = getClass().getClassLoader().getResourceAsStream("icon.png"); + if (is == null) { + System.err.println("Could not load application icon."); + } else { + this.setIconImage(ImageIO.read(is)); + } + } catch (IOException e) { + e.printStackTrace(); + } this.setMinimumSize(new Dimension(400, 400)); this.setPreferredSize(new Dimension(800, 800)); this.pack(); diff --git a/src/main/java/nl/andrewlalis/erme/view/EditorMenuBar.java b/src/main/java/nl/andrewlalis/erme/view/EditorMenuBar.java index afd76fa..90114c0 100644 --- a/src/main/java/nl/andrewlalis/erme/view/EditorMenuBar.java +++ b/src/main/java/nl/andrewlalis/erme/view/EditorMenuBar.java @@ -12,10 +12,7 @@ import javax.swing.*; * The menu bar that's visible atop the application. */ public class EditorMenuBar extends JMenuBar { - private final boolean includeAdminActions; - - public EditorMenuBar(boolean includeAdminActions) { - this.includeAdminActions = includeAdminActions; + public EditorMenuBar() { this.add(this.buildFileMenu()); this.add(this.buildEditMenu()); this.add(this.buildHelpMenu()); diff --git a/src/main/resources/admin_hash.txt b/src/main/resources/admin_hash.txt deleted file mode 100644 index d1f06e0..0000000 --- a/src/main/resources/admin_hash.txt +++ /dev/null @@ -1 +0,0 @@ -cfdabe75d984e5a92fb491dadc9091419d9587c049246356a488e83a75505bce \ No newline at end of file diff --git a/src/main/resources/html/instructions.html b/src/main/resources/html/instructions.html index 79c3cba..b5b5bb0 100644 --- a/src/main/resources/html/instructions.html +++ b/src/main/resources/html/instructions.html @@ -12,7 +12,7 @@

A simple UI for editing entity-relation mapping diagrams.

- Have you noticed any unexpected behavior? Is there something you thing would make a good addition to this application? + Have you noticed any unexpected behavior? Is there something you think would make a good addition to this application? Create a new issue on GitHub!

diff --git a/src/main/resources/icon.png b/src/main/resources/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..27ab608381554d9e3618f14118e52fdef768351d GIT binary patch literal 4167 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 BFd+Z{ literal 0 HcmV?d00001