commit
d70426c407
2
pom.xml
2
pom.xml
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
<groupId>nl.andrewlalis</groupId>
|
<groupId>nl.andrewlalis</groupId>
|
||||||
<artifactId>EntityRelationMappingEditor</artifactId>
|
<artifactId>EntityRelationMappingEditor</artifactId>
|
||||||
<version>1.2.0</version>
|
<version>1.3.0</version>
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
|
|
|
@ -1,16 +1,31 @@
|
||||||
package nl.andrewlalis.erme;
|
package nl.andrewlalis.erme;
|
||||||
|
|
||||||
import com.formdev.flatlaf.FlatLightLaf;
|
import com.formdev.flatlaf.FlatLightLaf;
|
||||||
|
import nl.andrewlalis.erme.util.Hash;
|
||||||
import nl.andrewlalis.erme.view.EditorFrame;
|
import nl.andrewlalis.erme.view.EditorFrame;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
public class EntityRelationMappingEditor {
|
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) {
|
public static void main(String[] args) {
|
||||||
if (!FlatLightLaf.install()) {
|
if (!FlatLightLaf.install()) {
|
||||||
System.err.println("Could not install FlatLight Look and Feel.");
|
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);
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package nl.andrewlalis.erme.control.actions;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import nl.andrewlalis.erme.model.MappingModel;
|
import nl.andrewlalis.erme.model.MappingModel;
|
||||||
import nl.andrewlalis.erme.model.Relation;
|
import nl.andrewlalis.erme.model.Relation;
|
||||||
|
import nl.andrewlalis.erme.view.DiagramPanel;
|
||||||
import nl.andrewlalis.erme.view.view_models.MappingModelViewModel;
|
import nl.andrewlalis.erme.view.view_models.MappingModelViewModel;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
|
@ -17,11 +18,12 @@ import java.awt.image.BufferedImage;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.prefs.Preferences;
|
||||||
|
|
||||||
public class ExportToImageAction extends AbstractAction {
|
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() {
|
public static ExportToImageAction getInstance() {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
instance = new ExportToImageAction();
|
instance = new ExportToImageAction();
|
||||||
|
@ -29,8 +31,6 @@ public class ExportToImageAction extends AbstractAction {
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private File lastSelectedFile;
|
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
private MappingModel model;
|
private MappingModel model;
|
||||||
|
|
||||||
|
@ -51,12 +51,14 @@ public class ExportToImageAction extends AbstractAction {
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
JFileChooser fileChooser = new JFileChooser(this.lastSelectedFile);
|
JFileChooser fileChooser = new JFileChooser();
|
||||||
fileChooser.setFileFilter(new FileNameExtensionFilter(
|
fileChooser.setFileFilter(new FileNameExtensionFilter(
|
||||||
"Image files", ImageIO.getReaderFileSuffixes()
|
"Image files", ImageIO.getReaderFileSuffixes()
|
||||||
));
|
));
|
||||||
if (this.lastSelectedFile != null) {
|
Preferences prefs = Preferences.userNodeForPackage(ExportToImageAction.class);
|
||||||
fileChooser.setSelectedFile(this.lastSelectedFile);
|
String path = prefs.get(LAST_EXPORT_LOCATION_KEY, null);
|
||||||
|
if (path != null) {
|
||||||
|
fileChooser.setSelectedFile(new File(path));
|
||||||
}
|
}
|
||||||
int choice = fileChooser.showSaveDialog((Component) e.getSource());
|
int choice = fileChooser.showSaveDialog((Component) e.getSource());
|
||||||
if (choice == JFileChooser.APPROVE_OPTION) {
|
if (choice == JFileChooser.APPROVE_OPTION) {
|
||||||
|
@ -73,7 +75,18 @@ public class ExportToImageAction extends AbstractAction {
|
||||||
chosenFile = new File(chosenFile.getParent(), chosenFile.getName() + '.' + extension);
|
chosenFile = new File(chosenFile.getParent(), chosenFile.getName() + '.' + extension);
|
||||||
}
|
}
|
||||||
try {
|
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) {
|
} catch (IOException ex) {
|
||||||
ex.printStackTrace();
|
ex.printStackTrace();
|
||||||
JOptionPane.showMessageDialog(fileChooser, "An error occurred and the file could not be saved:\n" + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
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() {
|
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);
|
BufferedImage bufferedImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
|
||||||
Graphics2D g2d = bufferedImage.createGraphics();
|
Graphics2D g2d = bufferedImage.createGraphics();
|
||||||
|
DiagramPanel.prepareGraphics(g2d);
|
||||||
final Rectangle bounds = this.model.getViewModel().getBounds(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);
|
BufferedImage outputImage = new BufferedImage(bounds.width, bounds.height + 20, BufferedImage.TYPE_INT_RGB);
|
||||||
g2d = outputImage.createGraphics();
|
g2d = outputImage.createGraphics();
|
||||||
g2d.setColor(Color.WHITE);
|
g2d.setColor(Color.WHITE);
|
||||||
g2d.fillRect(outputImage.getMinX(), outputImage.getMinY(), outputImage.getWidth(), outputImage.getHeight());
|
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();
|
AffineTransform originalTransform = g2d.getTransform();
|
||||||
g2d.setTransform(AffineTransform.getTranslateInstance(-bounds.x, -bounds.y));
|
g2d.setTransform(AffineTransform.getTranslateInstance(-bounds.x, -bounds.y));
|
||||||
|
DiagramPanel.prepareGraphics(g2d);
|
||||||
|
|
||||||
|
// Render the model.
|
||||||
List<Relation> selectedRelations = this.model.getSelectedRelations();
|
List<Relation> selectedRelations = this.model.getSelectedRelations();
|
||||||
this.model.getSelectedRelations().forEach(r -> r.setSelected(false));
|
this.model.getSelectedRelations().forEach(r -> r.setSelected(false));
|
||||||
new MappingModelViewModel(this.model).draw(g2d);
|
new MappingModelViewModel(this.model).draw(g2d);
|
||||||
this.model.getRelations().forEach(r -> r.setSelected(selectedRelations.contains(r)));
|
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.setTransform(originalTransform);
|
||||||
g2d.setColor(Color.LIGHT_GRAY);
|
g2d.setColor(Color.LIGHT_GRAY);
|
||||||
g2d.setFont(g2d.getFont().deriveFont(10.0f));
|
g2d.setFont(g2d.getFont().deriveFont(10.0f));
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,6 @@
|
||||||
package nl.andrewlalis.erme.control.actions;
|
package nl.andrewlalis.erme.control.actions;
|
||||||
|
|
||||||
import javax.swing.*;
|
public class InstructionsAction extends HtmlDocumentViewerAction {
|
||||||
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 {
|
|
||||||
private static InstructionsAction instance;
|
private static InstructionsAction instance;
|
||||||
|
|
||||||
public static InstructionsAction getInstance() {
|
public static InstructionsAction getInstance() {
|
||||||
|
@ -18,66 +9,7 @@ public class InstructionsAction extends AbstractAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
public InstructionsAction() {
|
public InstructionsAction() {
|
||||||
super("Instructions");
|
super("Instructions", "html/instructions.html");
|
||||||
this.putValue(SHORT_DESCRIPTION, "Instructions for how to use this program.");
|
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,10 +14,12 @@ import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.ObjectInputStream;
|
import java.io.ObjectInputStream;
|
||||||
|
import java.util.prefs.Preferences;
|
||||||
|
|
||||||
public class LoadAction extends AbstractAction {
|
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() {
|
public static LoadAction getInstance() {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
instance = new LoadAction();
|
instance = new LoadAction();
|
||||||
|
@ -25,8 +27,6 @@ public class LoadAction extends AbstractAction {
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private File lastSelectedFile;
|
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
private DiagramPanel diagramPanel;
|
private DiagramPanel diagramPanel;
|
||||||
|
|
||||||
|
@ -38,14 +38,16 @@ public class LoadAction extends AbstractAction {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void actionPerformed(ActionEvent e) {
|
public void actionPerformed(ActionEvent e) {
|
||||||
JFileChooser fileChooser = new JFileChooser(this.lastSelectedFile);
|
JFileChooser fileChooser = new JFileChooser();
|
||||||
FileNameExtensionFilter filter = new FileNameExtensionFilter(
|
FileNameExtensionFilter filter = new FileNameExtensionFilter(
|
||||||
"ERME Serialized Files",
|
"ERME Serialized Files",
|
||||||
"erme"
|
"erme"
|
||||||
);
|
);
|
||||||
fileChooser.setFileFilter(filter);
|
fileChooser.setFileFilter(filter);
|
||||||
if (this.lastSelectedFile != null) {
|
Preferences prefs = Preferences.userNodeForPackage(LoadAction.class);
|
||||||
fileChooser.setSelectedFile(this.lastSelectedFile);
|
String path = prefs.get(LAST_LOAD_LOCATION_KEY, null);
|
||||||
|
if (path != null) {
|
||||||
|
fileChooser.setSelectedFile(new File(path));
|
||||||
}
|
}
|
||||||
int choice = fileChooser.showOpenDialog((Component) e.getSource());
|
int choice = fileChooser.showOpenDialog((Component) e.getSource());
|
||||||
if (choice == JFileChooser.APPROVE_OPTION) {
|
if (choice == JFileChooser.APPROVE_OPTION) {
|
||||||
|
@ -56,8 +58,8 @@ public class LoadAction extends AbstractAction {
|
||||||
}
|
}
|
||||||
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(chosenFile))) {
|
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(chosenFile))) {
|
||||||
MappingModel loadedModel = (MappingModel) ois.readObject();
|
MappingModel loadedModel = (MappingModel) ois.readObject();
|
||||||
this.lastSelectedFile = chosenFile;
|
|
||||||
this.diagramPanel.setModel(loadedModel);
|
this.diagramPanel.setModel(loadedModel);
|
||||||
|
prefs.put(LAST_LOAD_LOCATION_KEY, chosenFile.getAbsolutePath());
|
||||||
} catch (IOException | ClassNotFoundException | ClassCastException ex) {
|
} catch (IOException | ClassNotFoundException | ClassCastException ex) {
|
||||||
ex.printStackTrace();
|
ex.printStackTrace();
|
||||||
JOptionPane.showMessageDialog(fileChooser, "An error occurred and the file could not be read:\n" + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
JOptionPane.showMessageDialog(fileChooser, "An error occurred and the file could not be read:\n" + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
|
||||||
|
|
|
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,11 +9,16 @@ import java.awt.*;
|
||||||
import java.awt.event.ActionEvent;
|
import java.awt.event.ActionEvent;
|
||||||
import java.awt.event.InputEvent;
|
import java.awt.event.InputEvent;
|
||||||
import java.awt.event.KeyEvent;
|
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 {
|
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() {
|
public static SaveAction getInstance() {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
instance = new SaveAction();
|
instance = new SaveAction();
|
||||||
|
@ -21,8 +26,6 @@ public class SaveAction extends AbstractAction {
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
private File lastSelectedFile;
|
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
private MappingModel model;
|
private MappingModel model;
|
||||||
|
|
||||||
|
@ -34,14 +37,16 @@ public class SaveAction extends AbstractAction {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void actionPerformed(ActionEvent e) {
|
public void actionPerformed(ActionEvent e) {
|
||||||
JFileChooser fileChooser = new JFileChooser(this.lastSelectedFile);
|
JFileChooser fileChooser = new JFileChooser();
|
||||||
FileNameExtensionFilter filter = new FileNameExtensionFilter(
|
FileNameExtensionFilter filter = new FileNameExtensionFilter(
|
||||||
"ERME Serialized Files",
|
"ERME Serialized Files",
|
||||||
"erme"
|
"erme"
|
||||||
);
|
);
|
||||||
fileChooser.setFileFilter(filter);
|
fileChooser.setFileFilter(filter);
|
||||||
if (this.lastSelectedFile != null) {
|
Preferences prefs = Preferences.userNodeForPackage(SaveAction.class);
|
||||||
fileChooser.setSelectedFile(this.lastSelectedFile);
|
String path = prefs.get(LAST_SAVE_LOCATION_KEY, null);
|
||||||
|
if (path != null) {
|
||||||
|
fileChooser.setSelectedFile(new File(path));
|
||||||
}
|
}
|
||||||
int choice = fileChooser.showSaveDialog((Component) e.getSource());
|
int choice = fileChooser.showSaveDialog((Component) e.getSource());
|
||||||
if (choice == JFileChooser.APPROVE_OPTION) {
|
if (choice == JFileChooser.APPROVE_OPTION) {
|
||||||
|
@ -56,7 +61,7 @@ public class SaveAction extends AbstractAction {
|
||||||
// TODO: Check for confirm before overwriting.
|
// TODO: Check for confirm before overwriting.
|
||||||
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(chosenFile))) {
|
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(chosenFile))) {
|
||||||
oos.writeObject(this.model);
|
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);
|
JOptionPane.showMessageDialog(fileChooser, "File saved successfully.", "Success", JOptionPane.INFORMATION_MESSAGE);
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
ex.printStackTrace();
|
ex.printStackTrace();
|
||||||
|
|
|
@ -39,7 +39,9 @@ public class AddRelationAction extends AbstractAction {
|
||||||
JOptionPane.PLAIN_MESSAGE
|
JOptionPane.PLAIN_MESSAGE
|
||||||
);
|
);
|
||||||
if (name != null) {
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,8 @@ public class MappingModel implements Serializable, Viewable {
|
||||||
|
|
||||||
private transient Set<ModelChangeListener> changeListeners;
|
private transient Set<ModelChangeListener> changeListeners;
|
||||||
|
|
||||||
|
private final static long serialVersionUID = 6153776381873250304L;
|
||||||
|
|
||||||
public MappingModel() {
|
public MappingModel() {
|
||||||
this.relations = new HashSet<>();
|
this.relations = new HashSet<>();
|
||||||
this.changeListeners = 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) {
|
public void addChangeListener(ModelChangeListener listener) {
|
||||||
if (this.changeListeners == null) {
|
if (this.changeListeners == null) {
|
||||||
this.changeListeners = new HashSet<>();
|
this.changeListeners = new HashSet<>();
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -100,9 +100,7 @@ public class DiagramPanel extends JPanel implements ModelChangeListener {
|
||||||
|
|
||||||
public Graphics2D getGraphics2D(Graphics g) {
|
public Graphics2D getGraphics2D(Graphics g) {
|
||||||
Graphics2D g2d = (Graphics2D) g;
|
Graphics2D g2d = (Graphics2D) g;
|
||||||
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
prepareGraphics(g2d);
|
||||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
|
||||||
g.setFont(g.getFont().deriveFont(14.0f));
|
|
||||||
return g2d;
|
return g2d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,4 +128,10 @@ public class DiagramPanel extends JPanel implements ModelChangeListener {
|
||||||
RemoveAttributeAction.getInstance().setModel(this.model);
|
RemoveAttributeAction.getInstance().setModel(this.model);
|
||||||
LoadSampleModelAction.getInstance().setDiagramPanel(this);
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,10 @@ import java.awt.*;
|
||||||
* The main JFrame for the editor.
|
* The main JFrame for the editor.
|
||||||
*/
|
*/
|
||||||
public class EditorFrame extends JFrame {
|
public class EditorFrame extends JFrame {
|
||||||
public EditorFrame() {
|
public EditorFrame(boolean includeAdminActions) {
|
||||||
super("ER-Mapping Editor");
|
super("ER-Mapping Editor");
|
||||||
this.setContentPane(new DiagramPanel(new MappingModel()));
|
this.setContentPane(new DiagramPanel(new MappingModel()));
|
||||||
this.setJMenuBar(new EditorMenuBar());
|
this.setJMenuBar(new EditorMenuBar(includeAdminActions));
|
||||||
this.setMinimumSize(new Dimension(400, 400));
|
this.setMinimumSize(new Dimension(400, 400));
|
||||||
this.setPreferredSize(new Dimension(800, 800));
|
this.setPreferredSize(new Dimension(800, 800));
|
||||||
this.pack();
|
this.pack();
|
||||||
|
|
|
@ -12,7 +12,10 @@ import javax.swing.*;
|
||||||
* The menu bar that's visible atop the application.
|
* The menu bar that's visible atop the application.
|
||||||
*/
|
*/
|
||||||
public class EditorMenuBar extends JMenuBar {
|
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.buildFileMenu());
|
||||||
this.add(this.buildEditMenu());
|
this.add(this.buildEditMenu());
|
||||||
this.add(this.buildHelpMenu());
|
this.add(this.buildHelpMenu());
|
||||||
|
@ -45,6 +48,9 @@ public class EditorMenuBar extends JMenuBar {
|
||||||
private JMenu buildHelpMenu() {
|
private JMenu buildHelpMenu() {
|
||||||
JMenu menu = new JMenu("Help");
|
JMenu menu = new JMenu("Help");
|
||||||
menu.add(InstructionsAction.getInstance());
|
menu.add(InstructionsAction.getInstance());
|
||||||
|
if (this.includeAdminActions) {
|
||||||
|
menu.add(MappingAlgorithmHelpAction.getInstance());
|
||||||
|
}
|
||||||
menu.add(LoadSampleModelAction.getInstance());
|
menu.add(LoadSampleModelAction.getInstance());
|
||||||
menu.add(AboutAction.getInstance());
|
menu.add(AboutAction.getInstance());
|
||||||
return menu;
|
return menu;
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
cfdabe75d984e5a92fb491dadc9091419d9587c049246356a488e83a75505bce
|
|
@ -0,0 +1,101 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>ER-Mapping Algorithm</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>Entity-Relation Mapping Algorithm</h1>
|
||||||
|
<p>
|
||||||
|
Written by <a href="https://github.com/andrewlalis">@andrewlalis</a>. Adapted from <em>Fundamentals of Database Systems, 7th Edition.</em>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>1. Mapping of Regular Entity Types</h3>
|
||||||
|
<p>
|
||||||
|
For each regular (strong) entity type <em>E</em> in the ER schema, create a relation <em>R</em> that includes all the simple attributes of <em>E</em>. Include only the simple component attributes of a composite attribute. Choose one of the key attributes of <em>E</em> as the primary key for <em>R</em>. If the chosen key is a composite, then the set of simple attributes that form it will together form the primary key of <em>R</em>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If multiple keys were identified for <em>E</em> 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 <em>R</em>. Knowledge about keys is also kept for indexing purposes and other types of analyses.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>2. Mapping of Weak Entity Types</h3>
|
||||||
|
<p>
|
||||||
|
For each weak entity type <em>W</em> in the ER schema with owner entity type <em>E</em>, create a relation <em>R</em> and include all simple attributes (or simple components of composite attributes) of <em>W</em> as attributes of <em>R</em>. In addition, include as foreign key attributes of <em>R</em>, 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 <em>W</em>. The primary key of <em>R</em> is the combination of the primary key(s) of the owner(s) and the partial key of the weak entity type <em>W</em>, if any. If there is a weak entity type <em>E<sub>2</sub></em> whose owner is also a weak entity type <em>E<sub>1</sub></em>, then <em>E<sub>1</sub></em> should be mapped first, to determine the primary key(s) that will be required by <em>E<sub>2</sub></em>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>3. Mapping of Binary 1:1 Relationship Types</h3>
|
||||||
|
<p>
|
||||||
|
For each binary 1:1 relationship type <em>R</em> in the ER schema, identify the relations <em>S</em> and <em>T</em> that correspond to the entity types participating in <em>R</em>. There are three possible approaches, the first of which is the most useful and should be followed unless special conditions exist:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Foreign Key Approach</strong>: Choose one of the relations, for example <em>S</em> (preferably one with total participation in the relationship), and include a foreign key in <em>S</em> which references the primary key of <em>T</em>. Include all simple attributes of the relationship type <em>R</em> as attributes of <em>S</em>.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Merged Relation Approach</strong>: Merge the two entity types and the relationship into a single relation. This is only possible when <em>both participations are total</em>, as ths would indicate that the two tables will have the exact same number of tuples at all times.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Cross-Reference or Relationship Relation Approach</strong>: Set up a third relation for the purpose of cross-referencing the primary keys of the two relations <em>S</em> and <em>T</em> representing the entity types. As we will see, this approach is required for binary M:N relationships. The resulting relation is called a <strong>relationship relation</strong> (or <strong>lookup table</strong>/<strong>join table</strong>), because each tuple in the relation represents an instance of the relationship that connects one tuple from <em>S</em> with one tuple from <em>T</em>. The relation will therefore include the primary key attributes of <em>S</em> and <em>T</em> 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.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>4. Mapping of Binary 1:N Relationship Types</h3>
|
||||||
|
<p>
|
||||||
|
There are two possible approaches, the first of which is generally preferred as it reduces the number of tables.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Foreign Key Approach</strong>:
|
||||||
|
For each regular binary 1:N (or N:1, depending on your perspective), identify the relation which represents the entity that's on the <em>N</em> side of the relationship, and include in that entity's relation a foreign key to the entity on the <em>1</em> side. Include any simple attributes (or simple components of composite attributes) of the 1:N relationship as attributes of the <em>N</em>-side entity.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Relationship Relation Approach</strong>:
|
||||||
|
An alternative approach is to use a <strong>relationship relation</strong>, where we create a separate relation <em>R</em> whose attributes are the primary keys of the two related entities, <em>S</em> and <em>T</em>. Those attributes will also be foreign keys to their respective entity relation. The primary key of <em>R</em> is the same as the primary key of the <em>N</em>-side entity.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>5. Mapping of Binary M:N Relationship Types</h3>
|
||||||
|
<p>
|
||||||
|
The only option for M:N relationships in the traditional relational model is the <strong>relationship relation</strong>. For each binary M:N relationship type <em>R</em>, create a new relation <em>S</em> to represent <em>R</em>. Include foreign key attributes in <em>S</em> for the primary keys of both participating entities. The combination of both foreign keys forms the primary key of <em>S</em>. Also include any simple attributes of <em>R</em> as attributes of <em>S</em>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>6. Mapping of Multivalued Attributes</h3>
|
||||||
|
<p>
|
||||||
|
For each multivalued attribute <em>A</em> from an entity <em>E</em>, create a new relation that contains a foreign key to <em>E</em>, and an attribute representing a single instance of <em>A</em>. The primary key of the new relation is the combination of the foreign key and the single attribute.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>7. Mapping of <em>N</em>-ary Relationship Types</h3>
|
||||||
|
<p>
|
||||||
|
For each <em>N</em>-ary relationship type <em>R</em>, where <em>n > 2</em>, create a new <strong>relationship relation</strong> <em>S</em> to represent <em>R</em>. Include as foreign key attributes in <em>S</em> the primary keys of the relations that represent the participating entity types. Also include any simple attributes of the <em>n</em>-ary relationship type (or simple components of the composite attributes) as attributes of <em>S</em>. The primary key of <em>S</em> 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).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3>8. Options for Mapping Specialization or Generalization</h3>
|
||||||
|
<p>
|
||||||
|
There are a few different options for mapping specializations and generalizations. The four most common are given here:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Multiple Relations - Super and Subclasses</strong>:
|
||||||
|
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).
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Multiple Relations - Subclasses Only</strong>:
|
||||||
|
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 <em>at least</em> one subclass). This option is also only recommended for <em>disjointed</em> specializations, because an overlapping specialization would lead to duplicate superclass entity information in possibly many of the subclass relations.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Single Relation with One Type Attribute</strong>:
|
||||||
|
Create a single relation with the attributes of the superclass, combined with the attributes of every subclass, and a <strong>discriminating type</strong> attribute whose value indicates the subclass to which each tuple belongs. This option works only for a specialization whose subclasses are <em>disjoint</em>, and has the potential for generating a huge number of <code>NULL</code> values if there are many separate subclass-specific attributes.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Single Relation with Multiple Attribute Types</strong>:
|
||||||
|
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 <strong>boolean type attribute</strong> 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 <em>overlapping</em> (but will also work with disjoint).
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>9. Mapping of Union Types (Categories)</h3>
|
||||||
|
<p>
|
||||||
|
For mapping a union type, we create a <strong>surrogate key</strong> 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.
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue