From 84709cc0d45cde514fcd41b454cc8cce8870b690 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Sun, 21 Feb 2021 12:56:13 +0100 Subject: [PATCH 1/5] Added admin actions. --- .../erme/EntityRelationMappingEditor.java | 17 ++++++- .../java/nl/andrewlalis/erme/util/Hash.java | 50 +++++++++++++++++++ .../nl/andrewlalis/erme/view/EditorFrame.java | 4 +- .../andrewlalis/erme/view/EditorMenuBar.java | 2 +- src/main/resources/admin_hash.txt | 1 + 5 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 src/main/java/nl/andrewlalis/erme/util/Hash.java create mode 100644 src/main/resources/admin_hash.txt diff --git a/src/main/java/nl/andrewlalis/erme/EntityRelationMappingEditor.java b/src/main/java/nl/andrewlalis/erme/EntityRelationMappingEditor.java index 5180efb..32095a9 100644 --- a/src/main/java/nl/andrewlalis/erme/EntityRelationMappingEditor.java +++ b/src/main/java/nl/andrewlalis/erme/EntityRelationMappingEditor.java @@ -1,8 +1,11 @@ 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.2.0"; @@ -10,7 +13,19 @@ public class EntityRelationMappingEditor { if (!FlatLightLaf.install()) { System.err.println("Could not install FlatLight Look and Feel."); } - final EditorFrame frame = new EditorFrame(); + final boolean includeAdminActions = shouldIncludeAdminActions(args); + if (includeAdminActions) { + System.out.println("Admin actions have been enabled."); + } + final EditorFrame frame = new EditorFrame(includeAdminActions); 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/util/Hash.java b/src/main/java/nl/andrewlalis/erme/util/Hash.java new file mode 100644 index 0000000..2e9a25a --- /dev/null +++ b/src/main/java/nl/andrewlalis/erme/util/Hash.java @@ -0,0 +1,50 @@ +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/EditorFrame.java b/src/main/java/nl/andrewlalis/erme/view/EditorFrame.java index 362cfb8..8bf536d 100644 --- a/src/main/java/nl/andrewlalis/erme/view/EditorFrame.java +++ b/src/main/java/nl/andrewlalis/erme/view/EditorFrame.java @@ -9,10 +9,10 @@ import java.awt.*; * The main JFrame for the editor. */ public class EditorFrame extends JFrame { - public EditorFrame() { + public EditorFrame(boolean includeAdminActions) { super("ER-Mapping Editor"); this.setContentPane(new DiagramPanel(new MappingModel())); - this.setJMenuBar(new EditorMenuBar()); + this.setJMenuBar(new EditorMenuBar(includeAdminActions)); 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 9192bdd..76e2e36 100644 --- a/src/main/java/nl/andrewlalis/erme/view/EditorMenuBar.java +++ b/src/main/java/nl/andrewlalis/erme/view/EditorMenuBar.java @@ -12,7 +12,7 @@ import javax.swing.*; * The menu bar that's visible atop the application. */ public class EditorMenuBar extends JMenuBar { - public EditorMenuBar() { + public EditorMenuBar(boolean includeAdminActions) { 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 new file mode 100644 index 0000000..d1f06e0 --- /dev/null +++ b/src/main/resources/admin_hash.txt @@ -0,0 +1 @@ +cfdabe75d984e5a92fb491dadc9091419d9587c049246356a488e83a75505bce \ No newline at end of file From a0a3b269b034a0bfe46d94458f6e9316084a25ad Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Sun, 21 Feb 2021 14:38:39 +0100 Subject: [PATCH 2/5] Added er mapping algorithm help. --- .../actions/HtmlDocumentViewerAction.java | 84 +++++++++++++++ .../control/actions/InstructionsAction.java | 72 +------------ .../actions/MappingAlgorithmHelpAction.java | 17 +++ .../andrewlalis/erme/view/EditorMenuBar.java | 6 ++ .../resources/html/er_mapping_algorithm.html | 101 ++++++++++++++++++ 5 files changed, 210 insertions(+), 70 deletions(-) create mode 100644 src/main/java/nl/andrewlalis/erme/control/actions/HtmlDocumentViewerAction.java create mode 100644 src/main/java/nl/andrewlalis/erme/control/actions/MappingAlgorithmHelpAction.java create mode 100644 src/main/resources/html/er_mapping_algorithm.html diff --git a/src/main/java/nl/andrewlalis/erme/control/actions/HtmlDocumentViewerAction.java b/src/main/java/nl/andrewlalis/erme/control/actions/HtmlDocumentViewerAction.java new file mode 100644 index 0000000..22f3348 --- /dev/null +++ b/src/main/java/nl/andrewlalis/erme/control/actions/HtmlDocumentViewerAction.java @@ -0,0 +1,84 @@ +package nl.andrewlalis.erme.control.actions; + +import javax.swing.*; +import javax.swing.event.HyperlinkEvent; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URISyntaxException; + +public abstract class HtmlDocumentViewerAction extends AbstractAction { + private final String resourceFileName; + + public HtmlDocumentViewerAction(String name, String resourceFileName) { + super(name); + this.resourceFileName = resourceFileName; + } + + @Override + public void actionPerformed(ActionEvent e) { + JDialog dialog = new JDialog( + SwingUtilities.getWindowAncestor((Component) e.getSource()), + (String) this.getValue(NAME), + Dialog.ModalityType.APPLICATION_MODAL + ); + JTextPane textPane = new JTextPane(); + textPane.setEditable(false); + textPane.setContentType("text/html"); + try { + textPane.setText(this.readFile()); + textPane.addHyperlinkListener(event -> { + if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { + if (!Desktop.isDesktopSupported()) { + JOptionPane.showMessageDialog(dialog, "Desktop API not supported. You may still visit the link manually:\n" + event.getURL(), "Desktop API Not Supported", JOptionPane.WARNING_MESSAGE); + } else { + Desktop desktop = Desktop.getDesktop(); + try { + desktop.browse(event.getURL().toURI()); + } catch (IOException | URISyntaxException ex) { + ex.printStackTrace(); + JOptionPane.showMessageDialog(dialog, "An error occurred and the URL could not be opened:\n" + event.getURL(), "URL Could Not Open", JOptionPane.ERROR_MESSAGE); + } + } + } + }); + } catch (IOException ex) { + ex.printStackTrace(); + JOptionPane.showMessageDialog( + (Component) e.getSource(), + "An error occured:\n" + ex.getMessage(), + "Error", + JOptionPane.ERROR_MESSAGE + ); + textPane.setContentType("text/plain"); + textPane.setText("Unable to load content."); + } + textPane.setCaretPosition(0); + JScrollPane scrollPane = new JScrollPane(textPane, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); + dialog.setContentPane(scrollPane); + dialog.setMaximumSize(new Dimension(600, 800)); + dialog.setPreferredSize(new Dimension(600, 800)); + dialog.pack(); + dialog.setLocationRelativeTo(null); + dialog.setVisible(true); + } + + private String readFile() throws IOException { + InputStream is = getClass().getClassLoader().getResourceAsStream(this.resourceFileName); + if (is == null) { + throw new IOException("Could not get stream for " + this.resourceFileName); + } + StringBuilder sb = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) { + String line = reader.readLine(); + while (line != null) { + sb.append(line).append('\n'); + line = reader.readLine(); + } + } + return sb.toString(); + } +} diff --git a/src/main/java/nl/andrewlalis/erme/control/actions/InstructionsAction.java b/src/main/java/nl/andrewlalis/erme/control/actions/InstructionsAction.java index 1233cad..258df91 100644 --- a/src/main/java/nl/andrewlalis/erme/control/actions/InstructionsAction.java +++ b/src/main/java/nl/andrewlalis/erme/control/actions/InstructionsAction.java @@ -1,15 +1,6 @@ package nl.andrewlalis.erme.control.actions; -import javax.swing.*; -import javax.swing.event.HyperlinkEvent; -import java.awt.*; -import java.awt.event.ActionEvent; -import java.io.*; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.file.Files; - -public class InstructionsAction extends AbstractAction { +public class InstructionsAction extends HtmlDocumentViewerAction { private static InstructionsAction instance; public static InstructionsAction getInstance() { @@ -18,66 +9,7 @@ public class InstructionsAction extends AbstractAction { } public InstructionsAction() { - super("Instructions"); + super("Instructions", "html/instructions.html"); this.putValue(SHORT_DESCRIPTION, "Instructions for how to use this program."); } - - @Override - public void actionPerformed(ActionEvent e) { - JDialog dialog = new JDialog(SwingUtilities.getWindowAncestor((Component) e.getSource()), "Instructions", Dialog.ModalityType.APPLICATION_MODAL); - JTextPane textPane = new JTextPane(); - textPane.setEditable(false); - textPane.setContentType("text/html"); - try { - textPane.setText(this.readFile()); - textPane.addHyperlinkListener(event -> { - if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { - if (!Desktop.isDesktopSupported()) { - JOptionPane.showMessageDialog(dialog, "Desktop API not supported. You may still visit the link manually:\n" + event.getURL(), "Desktop API Not Supported", JOptionPane.WARNING_MESSAGE); - } else { - Desktop desktop = Desktop.getDesktop(); - try { - desktop.browse(event.getURL().toURI()); - } catch (IOException | URISyntaxException ex) { - ex.printStackTrace(); - JOptionPane.showMessageDialog(dialog, "An error occurred and the URL could not be opened:\n" + event.getURL(), "URL Could Not Open", JOptionPane.ERROR_MESSAGE); - } - } - } - }); - } catch (IOException ex) { - ex.printStackTrace(); - JOptionPane.showMessageDialog( - (Component) e.getSource(), - "An error occured:\n" + ex.getMessage(), - "Error", - JOptionPane.ERROR_MESSAGE - ); - textPane.setContentType("text/plain"); - textPane.setText("Unable to load content."); - } - JScrollPane scrollPane = new JScrollPane(textPane, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); - dialog.setContentPane(scrollPane); - dialog.setMaximumSize(new Dimension(600, 800)); - dialog.setPreferredSize(new Dimension(600, 800)); - dialog.pack(); - dialog.setLocationRelativeTo(null); - dialog.setVisible(true); - } - - private String readFile() throws IOException { - InputStream is = getClass().getClassLoader().getResourceAsStream("html/instructions.html"); - if (is == null) { - throw new IOException("Could not get stream for instructions.html."); - } - StringBuilder sb = new StringBuilder(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(is))) { - String line = reader.readLine(); - while (line != null) { - sb.append(line).append('\n'); - line = reader.readLine(); - } - } - return sb.toString(); - } } diff --git a/src/main/java/nl/andrewlalis/erme/control/actions/MappingAlgorithmHelpAction.java b/src/main/java/nl/andrewlalis/erme/control/actions/MappingAlgorithmHelpAction.java new file mode 100644 index 0000000..dea1d87 --- /dev/null +++ b/src/main/java/nl/andrewlalis/erme/control/actions/MappingAlgorithmHelpAction.java @@ -0,0 +1,17 @@ +package nl.andrewlalis.erme.control.actions; + +public class MappingAlgorithmHelpAction extends HtmlDocumentViewerAction { + private static MappingAlgorithmHelpAction instance; + + public static MappingAlgorithmHelpAction getInstance() { + if (instance == null) { + instance = new MappingAlgorithmHelpAction(); + } + return instance; + } + + public MappingAlgorithmHelpAction() { + super("Mapping Algorithm Help", "html/er_mapping_algorithm.html"); + this.putValue(SHORT_DESCRIPTION, "Shows a quick guide on how to map from an ER model to a schema."); + } +} diff --git a/src/main/java/nl/andrewlalis/erme/view/EditorMenuBar.java b/src/main/java/nl/andrewlalis/erme/view/EditorMenuBar.java index 76e2e36..3fe0891 100644 --- a/src/main/java/nl/andrewlalis/erme/view/EditorMenuBar.java +++ b/src/main/java/nl/andrewlalis/erme/view/EditorMenuBar.java @@ -12,7 +12,10 @@ 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; this.add(this.buildFileMenu()); this.add(this.buildEditMenu()); this.add(this.buildHelpMenu()); @@ -45,6 +48,9 @@ public class EditorMenuBar extends JMenuBar { private JMenu buildHelpMenu() { JMenu menu = new JMenu("Help"); menu.add(InstructionsAction.getInstance()); + if (this.includeAdminActions) { + menu.add(MappingAlgorithmHelpAction.getInstance()); + } menu.add(LoadSampleModelAction.getInstance()); menu.add(AboutAction.getInstance()); return menu; diff --git a/src/main/resources/html/er_mapping_algorithm.html b/src/main/resources/html/er_mapping_algorithm.html new file mode 100644 index 0000000..d6ea0b3 --- /dev/null +++ b/src/main/resources/html/er_mapping_algorithm.html @@ -0,0 +1,101 @@ + + + + + ER-Mapping Algorithm + + + +

Entity-Relation Mapping Algorithm

+

+ Written by @andrewlalis. Adapted from Fundamentals of Database Systems, 7th Edition. +

+ +

1. Mapping of Regular Entity Types

+

+ For each regular (strong) entity type E in the ER schema, create a relation R that includes all the simple attributes of E. Include only the simple component attributes of a composite attribute. Choose one of the key attributes of E as the primary key for R. If the chosen key is a composite, then the set of simple attributes that form it will together form the primary key of R. +

+

+ If multiple keys were identified for E during the design of the schema, then the information describing the attributes that form each additional key should be kept in order to specify additional (unique) keys of the relation R. Knowledge about keys is also kept for indexing purposes and other types of analyses. +

+ +

2. Mapping of Weak Entity Types

+

+ For each weak entity type W in the ER schema with owner entity type E, create a relation R and include all simple attributes (or simple components of composite attributes) of W as attributes of R. In addition, include as foreign key attributes of R, the primary key attribute(s) of the relation(s) that correspond to the owner entity type(s); this takes care of mapping the identifying relationship type of W. The primary key of R is the combination of the primary key(s) of the owner(s) and the partial key of the weak entity type W, if any. If there is a weak entity type E2 whose owner is also a weak entity type E1, then E1 should be mapped first, to determine the primary key(s) that will be required by E2. +

+ +

3. Mapping of Binary 1:1 Relationship Types

+

+ For each binary 1:1 relationship type R in the ER schema, identify the relations S and T that correspond to the entity types participating in R. There are three possible approaches, the first of which is the most useful and should be followed unless special conditions exist: +

+ + +

4. Mapping of Binary 1:N Relationship Types

+

+ There are two possible approaches, the first of which is generally preferred as it reduces the number of tables. +

+ + +

5. Mapping of Binary M:N Relationship Types

+

+ The only option for M:N relationships in the traditional relational model is the relationship relation. For each binary M:N relationship type R, create a new relation S to represent R. Include foreign key attributes in S for the primary keys of both participating entities. The combination of both foreign keys forms the primary key of S. Also include any simple attributes of R as attributes of S. +

+ +

6. Mapping of Multivalued Attributes

+

+ For each multivalued attribute A from an entity E, create a new relation that contains a foreign key to E, and an attribute representing a single instance of A. The primary key of the new relation is the combination of the foreign key and the single attribute. +

+ +

7. Mapping of N-ary Relationship Types

+

+ For each N-ary relationship type R, where n > 2, create a new relationship relation S to represent R. Include as foreign key attributes in S the primary keys of the relations that represent the participating entity types. Also include any simple attributes of the n-ary relationship type (or simple components of the composite attributes) as attributes of S. The primary key of S is usually a combination of all the foreign keys that reference the participating entities (except the foreign keys to entities that participate in the relationship with a cardinality constraint of 1). +

+ +

8. Options for Mapping Specialization or Generalization

+

+ There are a few different options for mapping specializations and generalizations. The four most common are given here: +

+ + +

9. Mapping of Union Types (Categories)

+

+ For mapping a union type, we create a surrogate key attribute which should be appended to the relation of any entity that participates in the union. A relation is made for the union type itself, and that relation uses the surrogate key as its primary key. We then also declare the surrogate key in each participating entity relation as a foreign key to the union type's primary key. +

+ + \ No newline at end of file From 7a49772540f28c5e7c7e8428693456bd821cb40f Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Sun, 21 Feb 2021 15:40:17 +0100 Subject: [PATCH 3/5] Some improvements! --- .../control/actions/ExportToImageAction.java | 3 +++ .../control/actions/edits/AddRelationAction.java | 4 +++- .../nl/andrewlalis/erme/model/MappingModel.java | 16 ++++++++++++++++ .../nl/andrewlalis/erme/view/DiagramPanel.java | 10 +++++++--- 4 files changed, 29 insertions(+), 4 deletions(-) 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 a9774be..8fb316b 100644 --- a/src/main/java/nl/andrewlalis/erme/control/actions/ExportToImageAction.java +++ b/src/main/java/nl/andrewlalis/erme/control/actions/ExportToImageAction.java @@ -3,6 +3,7 @@ package nl.andrewlalis.erme.control.actions; 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.MappingModelViewModel; import javax.imageio.ImageIO; @@ -84,6 +85,7 @@ public class ExportToImageAction extends AbstractAction { private BufferedImage renderModel() { 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); BufferedImage outputImage = new BufferedImage(bounds.width, bounds.height + 20, BufferedImage.TYPE_INT_RGB); g2d = outputImage.createGraphics(); @@ -91,6 +93,7 @@ public class ExportToImageAction extends AbstractAction { g2d.fillRect(outputImage.getMinX(), outputImage.getMinY(), outputImage.getWidth(), outputImage.getHeight()); AffineTransform originalTransform = g2d.getTransform(); g2d.setTransform(AffineTransform.getTranslateInstance(-bounds.x, -bounds.y)); + DiagramPanel.prepareGraphics(g2d); List selectedRelations = this.model.getSelectedRelations(); this.model.getSelectedRelations().forEach(r -> r.setSelected(false)); diff --git a/src/main/java/nl/andrewlalis/erme/control/actions/edits/AddRelationAction.java b/src/main/java/nl/andrewlalis/erme/control/actions/edits/AddRelationAction.java index 9f7c904..6e92623 100644 --- a/src/main/java/nl/andrewlalis/erme/control/actions/edits/AddRelationAction.java +++ b/src/main/java/nl/andrewlalis/erme/control/actions/edits/AddRelationAction.java @@ -39,7 +39,9 @@ public class AddRelationAction extends AbstractAction { JOptionPane.PLAIN_MESSAGE ); if (name != null) { - this.model.addRelation(new Relation(this.model, new Point(0, 0), name)); + Rectangle bounds = this.model.getRelationBounds(); + Point center = new Point(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2); + this.model.addRelation(new Relation(this.model, center, name)); } } } diff --git a/src/main/java/nl/andrewlalis/erme/model/MappingModel.java b/src/main/java/nl/andrewlalis/erme/model/MappingModel.java index 328f9ad..3a37643 100644 --- a/src/main/java/nl/andrewlalis/erme/model/MappingModel.java +++ b/src/main/java/nl/andrewlalis/erme/model/MappingModel.java @@ -22,6 +22,8 @@ public class MappingModel implements Serializable, Viewable { private transient Set changeListeners; + private final static long serialVersionUID = 6153776381873250304L; + public MappingModel() { this.relations = new HashSet<>(); this.changeListeners = new HashSet<>(); @@ -70,6 +72,20 @@ public class MappingModel implements Serializable, Viewable { } } + public Rectangle getRelationBounds() { + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + for (Relation r : this.getRelations()) { + minX = Math.min(minX, r.getPosition().x); + minY = Math.min(minY, r.getPosition().y); + maxX = Math.max(maxX, r.getPosition().x); + maxY = Math.max(maxY, r.getPosition().y); + } + return new Rectangle(minX, minY, maxX - minX, maxY - minY); + } + public void addChangeListener(ModelChangeListener listener) { if (this.changeListeners == null) { this.changeListeners = new HashSet<>(); diff --git a/src/main/java/nl/andrewlalis/erme/view/DiagramPanel.java b/src/main/java/nl/andrewlalis/erme/view/DiagramPanel.java index b5f10e8..8e805e2 100644 --- a/src/main/java/nl/andrewlalis/erme/view/DiagramPanel.java +++ b/src/main/java/nl/andrewlalis/erme/view/DiagramPanel.java @@ -100,9 +100,7 @@ public class DiagramPanel extends JPanel implements ModelChangeListener { public Graphics2D getGraphics2D(Graphics g) { Graphics2D g2d = (Graphics2D) g; - g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g.setFont(g.getFont().deriveFont(14.0f)); + prepareGraphics(g2d); return g2d; } @@ -130,4 +128,10 @@ public class DiagramPanel extends JPanel implements ModelChangeListener { RemoveAttributeAction.getInstance().setModel(this.model); LoadSampleModelAction.getInstance().setDiagramPanel(this); } + + public static void prepareGraphics(Graphics2D g) { + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setFont(g.getFont().deriveFont(14.0f)); + } } From f37df5defccfb4c593a6615b69971239a83ba594 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Tue, 23 Feb 2021 09:10:23 +0100 Subject: [PATCH 4/5] Added better load and save preferences, non-modal help. --- .../control/actions/ExportToImageAction.java | 35 ++++++++++++++----- .../actions/HtmlDocumentViewerAction.java | 8 ++++- .../erme/control/actions/LoadAction.java | 16 +++++---- .../actions/MappingAlgorithmHelpAction.java | 4 ++- .../erme/control/actions/SaveAction.java | 21 ++++++----- 5 files changed, 59 insertions(+), 25 deletions(-) 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 8fb316b..96c6fba 100644 --- a/src/main/java/nl/andrewlalis/erme/control/actions/ExportToImageAction.java +++ b/src/main/java/nl/andrewlalis/erme/control/actions/ExportToImageAction.java @@ -18,11 +18,12 @@ import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.List; -import java.util.Set; +import java.util.prefs.Preferences; public class ExportToImageAction extends AbstractAction { - private static ExportToImageAction instance; + private static final String LAST_EXPORT_LOCATION_KEY = "lastExportLocation"; + private static ExportToImageAction instance; public static ExportToImageAction getInstance() { if (instance == null) { instance = new ExportToImageAction(); @@ -30,8 +31,6 @@ public class ExportToImageAction extends AbstractAction { return instance; } - private File lastSelectedFile; - @Setter private MappingModel model; @@ -52,12 +51,14 @@ public class ExportToImageAction extends AbstractAction { ); return; } - JFileChooser fileChooser = new JFileChooser(this.lastSelectedFile); + JFileChooser fileChooser = new JFileChooser(); fileChooser.setFileFilter(new FileNameExtensionFilter( "Image files", ImageIO.getReaderFileSuffixes() )); - if (this.lastSelectedFile != null) { - fileChooser.setSelectedFile(this.lastSelectedFile); + Preferences prefs = Preferences.userNodeForPackage(ExportToImageAction.class); + String path = prefs.get(LAST_EXPORT_LOCATION_KEY, null); + if (path != null) { + fileChooser.setSelectedFile(new File(path)); } int choice = fileChooser.showSaveDialog((Component) e.getSource()); if (choice == JFileChooser.APPROVE_OPTION) { @@ -74,7 +75,18 @@ public class ExportToImageAction extends AbstractAction { chosenFile = new File(chosenFile.getParent(), chosenFile.getName() + '.' + extension); } try { - ImageIO.write(this.renderModel(), extension, chosenFile); + long start = System.currentTimeMillis(); + BufferedImage render = this.renderModel(); + double durationSeconds = (System.currentTimeMillis() - start) / 1000.0; + ImageIO.write(render, extension, chosenFile); + prefs.put(LAST_EXPORT_LOCATION_KEY, chosenFile.getAbsolutePath()); + JOptionPane.showMessageDialog( + fileChooser, + "Image export completed in " + String.format("%.4f", durationSeconds) + " seconds.\n" + + "Resolution: " + render.getWidth() + "x" + render.getHeight(), + "Image Export Complete", + JOptionPane.INFORMATION_MESSAGE + ); } catch (IOException ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(fileChooser, "An error occurred and the file could not be saved:\n" + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); @@ -83,23 +95,30 @@ public class ExportToImageAction extends AbstractAction { } private BufferedImage renderModel() { + // 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); + + // Prepare the output image. BufferedImage outputImage = new BufferedImage(bounds.width, bounds.height + 20, BufferedImage.TYPE_INT_RGB); g2d = outputImage.createGraphics(); g2d.setColor(Color.WHITE); g2d.fillRect(outputImage.getMinX(), outputImage.getMinY(), outputImage.getWidth(), outputImage.getHeight()); + + // 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)); DiagramPanel.prepareGraphics(g2d); + // Render the model. List selectedRelations = this.model.getSelectedRelations(); this.model.getSelectedRelations().forEach(r -> r.setSelected(false)); new MappingModelViewModel(this.model).draw(g2d); this.model.getRelations().forEach(r -> r.setSelected(selectedRelations.contains(r))); + // Revert back to the normal image space, and render a watermark. g2d.setTransform(originalTransform); g2d.setColor(Color.LIGHT_GRAY); g2d.setFont(g2d.getFont().deriveFont(10.0f)); 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 22f3348..da5ad90 100644 --- a/src/main/java/nl/andrewlalis/erme/control/actions/HtmlDocumentViewerAction.java +++ b/src/main/java/nl/andrewlalis/erme/control/actions/HtmlDocumentViewerAction.java @@ -12,10 +12,16 @@ import java.net.URISyntaxException; public abstract class HtmlDocumentViewerAction extends AbstractAction { private final String resourceFileName; + private final Dialog.ModalityType modalityType; public HtmlDocumentViewerAction(String name, String resourceFileName) { + this(name, resourceFileName, Dialog.ModalityType.APPLICATION_MODAL); + } + + public HtmlDocumentViewerAction(String name, String resourceFileName, Dialog.ModalityType modalityType) { super(name); this.resourceFileName = resourceFileName; + this.modalityType = modalityType; } @Override @@ -23,7 +29,7 @@ public abstract class HtmlDocumentViewerAction extends AbstractAction { JDialog dialog = new JDialog( SwingUtilities.getWindowAncestor((Component) e.getSource()), (String) this.getValue(NAME), - Dialog.ModalityType.APPLICATION_MODAL + this.modalityType ); JTextPane textPane = new JTextPane(); textPane.setEditable(false); 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 370fba7..5cf2859 100644 --- a/src/main/java/nl/andrewlalis/erme/control/actions/LoadAction.java +++ b/src/main/java/nl/andrewlalis/erme/control/actions/LoadAction.java @@ -14,10 +14,12 @@ 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 { - private static LoadAction instance; + private static final String LAST_LOAD_LOCATION_KEY = "lastLoadLocation"; + private static LoadAction instance; public static LoadAction getInstance() { if (instance == null) { instance = new LoadAction(); @@ -25,8 +27,6 @@ public class LoadAction extends AbstractAction { return instance; } - private File lastSelectedFile; - @Setter private DiagramPanel diagramPanel; @@ -38,14 +38,16 @@ public class LoadAction extends AbstractAction { @Override public void actionPerformed(ActionEvent e) { - JFileChooser fileChooser = new JFileChooser(this.lastSelectedFile); + JFileChooser fileChooser = new JFileChooser(); FileNameExtensionFilter filter = new FileNameExtensionFilter( "ERME Serialized Files", "erme" ); fileChooser.setFileFilter(filter); - if (this.lastSelectedFile != null) { - fileChooser.setSelectedFile(this.lastSelectedFile); + Preferences prefs = Preferences.userNodeForPackage(LoadAction.class); + String path = prefs.get(LAST_LOAD_LOCATION_KEY, null); + if (path != null) { + fileChooser.setSelectedFile(new File(path)); } int choice = fileChooser.showOpenDialog((Component) e.getSource()); if (choice == JFileChooser.APPROVE_OPTION) { @@ -56,8 +58,8 @@ public class LoadAction extends AbstractAction { } try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(chosenFile))) { MappingModel loadedModel = (MappingModel) ois.readObject(); - this.lastSelectedFile = chosenFile; this.diagramPanel.setModel(loadedModel); + prefs.put(LAST_LOAD_LOCATION_KEY, chosenFile.getAbsolutePath()); } catch (IOException | ClassNotFoundException | ClassCastException 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/MappingAlgorithmHelpAction.java b/src/main/java/nl/andrewlalis/erme/control/actions/MappingAlgorithmHelpAction.java index dea1d87..bd3f790 100644 --- a/src/main/java/nl/andrewlalis/erme/control/actions/MappingAlgorithmHelpAction.java +++ b/src/main/java/nl/andrewlalis/erme/control/actions/MappingAlgorithmHelpAction.java @@ -1,5 +1,7 @@ package nl.andrewlalis.erme.control.actions; +import java.awt.*; + public class MappingAlgorithmHelpAction extends HtmlDocumentViewerAction { private static MappingAlgorithmHelpAction instance; @@ -11,7 +13,7 @@ public class MappingAlgorithmHelpAction extends HtmlDocumentViewerAction { } public MappingAlgorithmHelpAction() { - super("Mapping Algorithm Help", "html/er_mapping_algorithm.html"); + super("Mapping Algorithm Help", "html/er_mapping_algorithm.html", Dialog.ModalityType.DOCUMENT_MODAL); this.putValue(SHORT_DESCRIPTION, "Shows a quick guide on how to map from an ER model to a schema."); } } 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 afc728b..9bd0d44 100644 --- a/src/main/java/nl/andrewlalis/erme/control/actions/SaveAction.java +++ b/src/main/java/nl/andrewlalis/erme/control/actions/SaveAction.java @@ -9,11 +9,16 @@ import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; -import java.io.*; +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 { - private static SaveAction instance; + private static final String LAST_SAVE_LOCATION_KEY = "lastSaveLocation"; + private static SaveAction instance; public static SaveAction getInstance() { if (instance == null) { instance = new SaveAction(); @@ -21,8 +26,6 @@ public class SaveAction extends AbstractAction { return instance; } - private File lastSelectedFile; - @Setter private MappingModel model; @@ -34,14 +37,16 @@ public class SaveAction extends AbstractAction { @Override public void actionPerformed(ActionEvent e) { - JFileChooser fileChooser = new JFileChooser(this.lastSelectedFile); + JFileChooser fileChooser = new JFileChooser(); FileNameExtensionFilter filter = new FileNameExtensionFilter( "ERME Serialized Files", "erme" ); fileChooser.setFileFilter(filter); - if (this.lastSelectedFile != null) { - fileChooser.setSelectedFile(this.lastSelectedFile); + Preferences prefs = Preferences.userNodeForPackage(SaveAction.class); + String path = prefs.get(LAST_SAVE_LOCATION_KEY, null); + if (path != null) { + fileChooser.setSelectedFile(new File(path)); } int choice = fileChooser.showSaveDialog((Component) e.getSource()); if (choice == JFileChooser.APPROVE_OPTION) { @@ -56,7 +61,7 @@ public class SaveAction extends AbstractAction { // TODO: Check for confirm before overwriting. try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(chosenFile))) { oos.writeObject(this.model); - this.lastSelectedFile = chosenFile; + prefs.put(LAST_SAVE_LOCATION_KEY, chosenFile.getAbsolutePath()); JOptionPane.showMessageDialog(fileChooser, "File saved successfully.", "Success", JOptionPane.INFORMATION_MESSAGE); } catch (IOException ex) { ex.printStackTrace(); From 32638e103db3d9ff8f2ab60e28a529705b74119d Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Tue, 23 Feb 2021 09:10:47 +0100 Subject: [PATCH 5/5] Version bump. --- pom.xml | 2 +- .../java/nl/andrewlalis/erme/EntityRelationMappingEditor.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 779af24..5cb56d7 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ nl.andrewlalis EntityRelationMappingEditor - 1.2.0 + 1.3.0 diff --git a/src/main/java/nl/andrewlalis/erme/EntityRelationMappingEditor.java b/src/main/java/nl/andrewlalis/erme/EntityRelationMappingEditor.java index 32095a9..d85cf52 100644 --- a/src/main/java/nl/andrewlalis/erme/EntityRelationMappingEditor.java +++ b/src/main/java/nl/andrewlalis/erme/EntityRelationMappingEditor.java @@ -7,7 +7,7 @@ import nl.andrewlalis.erme.view.EditorFrame; import java.nio.charset.StandardCharsets; public class EntityRelationMappingEditor { - public static final String VERSION = "1.2.0"; + public static final String VERSION = "1.3.0"; public static void main(String[] args) { if (!FlatLightLaf.install()) {