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 5180efb..d85cf52 100644 --- a/src/main/java/nl/andrewlalis/erme/EntityRelationMappingEditor.java +++ b/src/main/java/nl/andrewlalis/erme/EntityRelationMappingEditor.java @@ -1,16 +1,31 @@ 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"; + public static final String VERSION = "1.3.0"; public static void main(String[] args) { 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/control/actions/ExportToImageAction.java b/src/main/java/nl/andrewlalis/erme/control/actions/ExportToImageAction.java index a9774be..96c6fba 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; @@ -17,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(); @@ -29,8 +31,6 @@ public class ExportToImageAction extends AbstractAction { return instance; } - private File lastSelectedFile; - @Setter private MappingModel model; @@ -51,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) { @@ -73,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); @@ -82,21 +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 new file mode 100644 index 0000000..da5ad90 --- /dev/null +++ b/src/main/java/nl/andrewlalis/erme/control/actions/HtmlDocumentViewerAction.java @@ -0,0 +1,90 @@ +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; + 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 + public void actionPerformed(ActionEvent e) { + JDialog dialog = new JDialog( + SwingUtilities.getWindowAncestor((Component) e.getSource()), + (String) this.getValue(NAME), + this.modalityType + ); + 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/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 new file mode 100644 index 0000000..bd3f790 --- /dev/null +++ b/src/main/java/nl/andrewlalis/erme/control/actions/MappingAlgorithmHelpAction.java @@ -0,0 +1,19 @@ +package nl.andrewlalis.erme.control.actions; + +import java.awt.*; + +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", 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(); 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/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/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)); + } } 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..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 { - public EditorMenuBar() { + 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/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 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: +

+
    +
  • + Foreign Key Approach: Choose one of the relations, for example S (preferably one with total participation in the relationship), and include a foreign key in S which references the primary key of T. Include all simple attributes of the relationship type R as attributes of S. +
  • +
  • + Merged Relation Approach: Merge the two entity types and the relationship into a single relation. This is only possible when both participations are total, as ths would indicate that the two tables will have the exact same number of tuples at all times. +
  • +
  • + Cross-Reference or Relationship Relation Approach: Set up a third relation for the purpose of cross-referencing the primary keys of the two relations S and T representing the entity types. As we will see, this approach is required for binary M:N relationships. The resulting relation is called a relationship relation (or lookup table/join table), because each tuple in the relation represents an instance of the relationship that connects one tuple from S with one tuple from T. The relation will therefore include the primary key attributes of S and T as foreign keys to their respective relations. The drawback of this approach is the added complexity of an additional relation, and requiring extra join operations when combining related tuples from the tables. +
  • +
+ +

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. +

+
    +
  • + Foreign Key Approach: + For each regular binary 1:N (or N:1, depending on your perspective), identify the relation which represents the entity that's on the N side of the relationship, and include in that entity's relation a foreign key to the entity on the 1 side. Include any simple attributes (or simple components of composite attributes) of the 1:N relationship as attributes of the N-side entity. +
  • +
  • + Relationship Relation Approach: + An alternative approach is to use a relationship relation, where we create a separate relation R whose attributes are the primary keys of the two related entities, S and T. Those attributes will also be foreign keys to their respective entity relation. The primary key of R is the same as the primary key of the N-side entity. +
  • +
+ +

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: +

+
    +
  • + Multiple Relations - Super and Subclasses: + Create a relation for the superclass just as you would for a normal entity. Create a relation for each subclass (specialization) of the superclass, which contains a foreign key to the super class, as well as all attributes of the subclass. The foreign key to the superclass will also be the subclass' primary key. This option works for any specialization (total or partial, disjoint or overlapping). +
  • +
  • + Multiple Relations - Subclasses Only: + Create a relation for each subclass, which contains all attributes of the subclass, alongside all attributes of the superclass, and use the superclass' primary key as the primary key for each relation. This option only works for specializations where the superclass has total participation (every entity in the superclass must belong to at least one subclass). This option is also only recommended for disjointed specializations, because an overlapping specialization would lead to duplicate superclass entity information in possibly many of the subclass relations. +
  • +
  • + Single Relation with One Type Attribute: + Create a single relation with the attributes of the superclass, combined with the attributes of every subclass, and a discriminating type attribute whose value indicates the subclass to which each tuple belongs. This option works only for a specialization whose subclasses are disjoint, and has the potential for generating a huge number of NULL values if there are many separate subclass-specific attributes. +
  • +
  • + Single Relation with Multiple Attribute Types: + Create a single relation with the attributes of the superclass and all subclasses, just as with the previous option, but instead of a single discriminating type attribute, create one boolean type attribute for each subclass, to indicate whether or not the entity belongs to a given subclass. This option is a variant of the previous, which has been designed to work with overlapping (but will also work with disjoint). +
  • +
+ +

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