Removed useless admin stuff, changed to save files as JSON, added image scaling for exports.
This commit is contained in:
parent
d63eb3345f
commit
bc464ba42b
|
@ -1,6 +1,11 @@
|
||||||
# Entity-Relation Mapping Editor
|
# Entity-Relation Mapping Editor
|
||||||
A simple UI for editing entity-relation mapping diagrams.
|
A simple UI for editing entity-relation mapping diagrams.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
This program is distributed as an executable **jar** file. You can find the latest release [here](https://github.com/andrewlalis/EntityRelationMappingEditor/releases). You will need Java installed on your computer (version 8 or higher). [You can install an OpenJDK version of Java here.](https://adoptium.net/)
|
||||||
|
|
||||||
|
Simply double-click on the jar file, or execute `java -jar <jar file>` from the command line (where `<jar file>` is replaced with the path to your actual jar file).
|
||||||
|
|
||||||
![window screenshot](https://raw.githubusercontent.com/andrewlalis/EntityRelationMappingEditor/main/design/main_interface.PNG)
|
![window screenshot](https://raw.githubusercontent.com/andrewlalis/EntityRelationMappingEditor/main/design/main_interface.PNG)
|
||||||
|
|
||||||
## How to Use
|
## How to Use
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 8.7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 14 KiB |
12
pom.xml
12
pom.xml
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
<groupId>nl.andrewlalis</groupId>
|
<groupId>nl.andrewlalis</groupId>
|
||||||
<artifactId>EntityRelationMappingEditor</artifactId>
|
<artifactId>EntityRelationMappingEditor</artifactId>
|
||||||
<version>1.5.0</version>
|
<version>1.6.0</version>
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
|
@ -50,13 +50,19 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.formdev</groupId>
|
<groupId>com.formdev</groupId>
|
||||||
<artifactId>flatlaf</artifactId>
|
<artifactId>flatlaf</artifactId>
|
||||||
<version>1.0-rc3</version>
|
<version>1.6.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<version>1.18.16</version>
|
<version>1.18.22</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>2.13.0</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
|
@ -1,31 +1,16 @@
|
||||||
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.5.0";
|
public static final String VERSION = "1.6.0";
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
if (!FlatLightLaf.install()) {
|
if (!FlatLightLaf.setup()) {
|
||||||
System.err.println("Could not install FlatLight Look and Feel.");
|
System.err.println("Could not install FlatLight Look and Feel.");
|
||||||
}
|
}
|
||||||
final boolean includeAdminActions = shouldIncludeAdminActions(args);
|
EditorFrame frame = new EditorFrame();
|
||||||
if (includeAdminActions) {
|
|
||||||
System.out.println("Admin actions have been enabled.");
|
|
||||||
}
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ 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.DiagramPanel;
|
||||||
import nl.andrewlalis.erme.view.view_models.AttributeViewModel;
|
|
||||||
import nl.andrewlalis.erme.view.view_models.MappingModelViewModel;
|
import nl.andrewlalis.erme.view.view_models.MappingModelViewModel;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
|
@ -77,9 +76,18 @@ public class ExportToImageAction extends AbstractAction {
|
||||||
} else {
|
} else {
|
||||||
chosenFile = new File(chosenFile.getParent(), chosenFile.getName() + '.' + extension);
|
chosenFile = new File(chosenFile.getParent(), chosenFile.getName() + '.' + extension);
|
||||||
}
|
}
|
||||||
|
String input = JOptionPane.showInputDialog(this.diagramPanel, "Choose a scale for the image.", "3.0");
|
||||||
|
float scale;
|
||||||
|
try {
|
||||||
|
scale = Float.parseFloat(input);
|
||||||
|
if (scale <= 0.0f || scale > 64.0f) throw new IllegalArgumentException();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
JOptionPane.showMessageDialog(this.diagramPanel, "Invalid scale value. Should be a positive number less than 64.", "Invalid Scale", JOptionPane.WARNING_MESSAGE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
BufferedImage render = this.renderModel();
|
BufferedImage render = this.renderModel(scale);
|
||||||
double durationSeconds = (System.currentTimeMillis() - start) / 1000.0;
|
double durationSeconds = (System.currentTimeMillis() - start) / 1000.0;
|
||||||
ImageIO.write(render, extension, chosenFile);
|
ImageIO.write(render, extension, chosenFile);
|
||||||
prefs.put(LAST_EXPORT_LOCATION_KEY, chosenFile.getAbsolutePath());
|
prefs.put(LAST_EXPORT_LOCATION_KEY, chosenFile.getAbsolutePath());
|
||||||
|
@ -97,12 +105,19 @@ public class ExportToImageAction extends AbstractAction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private BufferedImage renderModel() {
|
/**
|
||||||
|
* Renders the mapping model to an image with the given resolution.
|
||||||
|
* @param scale The scale to use. Should be greater than zero.
|
||||||
|
* @return The image which was rendered.
|
||||||
|
*/
|
||||||
|
private BufferedImage renderModel(float scale) {
|
||||||
// Prepare a tiny sample image that we can use to determine the bounds of the model in a graphics context.
|
// 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);
|
DiagramPanel.prepareGraphics(g2d);
|
||||||
final Rectangle bounds = this.model.getViewModel().getBounds(g2d);
|
final Rectangle bounds = this.model.getViewModel().getBounds(g2d);
|
||||||
|
bounds.width *= scale;
|
||||||
|
bounds.height *= scale;
|
||||||
|
|
||||||
// Prepare the output image.
|
// 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);
|
||||||
|
@ -112,7 +127,10 @@ public class ExportToImageAction extends AbstractAction {
|
||||||
|
|
||||||
// Transform the graphics space to account for the model's offset from origin.
|
// 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));
|
AffineTransform modelTransform = new AffineTransform();
|
||||||
|
modelTransform.scale(scale, scale);
|
||||||
|
modelTransform.translate(-bounds.x, -bounds.y);
|
||||||
|
g2d.setTransform(modelTransform);
|
||||||
DiagramPanel.prepareGraphics(g2d);
|
DiagramPanel.prepareGraphics(g2d);
|
||||||
|
|
||||||
// Render the model.
|
// Render the model.
|
||||||
|
@ -124,9 +142,9 @@ public class ExportToImageAction extends AbstractAction {
|
||||||
this.model.getRelations().forEach(r -> r.setSelected(selectedRelations.contains(r)));
|
this.model.getRelations().forEach(r -> r.setSelected(selectedRelations.contains(r)));
|
||||||
LolcatAction.getInstance().setLolcatEnabled(lolcat); // revert previous lolcat mode
|
LolcatAction.getInstance().setLolcatEnabled(lolcat); // revert previous lolcat mode
|
||||||
|
|
||||||
// Revert back to the normal image space, and render a watermark.
|
// Revert to the normal image space, and render a watermark.
|
||||||
g2d.setTransform(originalTransform);
|
g2d.setTransform(originalTransform);
|
||||||
g2d.setColor(Color.LIGHT_GRAY);
|
g2d.setColor(Color.decode("#e8e8e8"));
|
||||||
g2d.setFont(g2d.getFont().deriveFont(10.0f));
|
g2d.setFont(g2d.getFont().deriveFont(10.0f));
|
||||||
g2d.drawString("Created by EntityRelationMappingEditor", 0, outputImage.getHeight() - 3);
|
g2d.drawString("Created by EntityRelationMappingEditor", 0, outputImage.getHeight() - 3);
|
||||||
return outputImage;
|
return outputImage;
|
||||||
|
|
|
@ -13,6 +13,9 @@ import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An action which, when performed, opens a view that displays an HTML document.
|
||||||
|
*/
|
||||||
public abstract class HtmlDocumentViewerAction extends AbstractAction {
|
public abstract class HtmlDocumentViewerAction extends AbstractAction {
|
||||||
private final String resourceFileName;
|
private final String resourceFileName;
|
||||||
private final Dialog.ModalityType modalityType;
|
private final Dialog.ModalityType modalityType;
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
package nl.andrewlalis.erme.control.actions;
|
package nl.andrewlalis.erme.control.actions;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.MapperFeature;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import nl.andrewlalis.erme.model.MappingModel;
|
import nl.andrewlalis.erme.model.MappingModel;
|
||||||
import nl.andrewlalis.erme.view.DiagramPanel;
|
import nl.andrewlalis.erme.view.DiagramPanel;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||||
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.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.ObjectInputStream;
|
|
||||||
import java.util.prefs.Preferences;
|
import java.util.prefs.Preferences;
|
||||||
|
|
||||||
public class LoadAction extends AbstractAction {
|
public class LoadAction extends AbstractAction {
|
||||||
|
@ -40,8 +43,8 @@ public class LoadAction extends AbstractAction {
|
||||||
public void actionPerformed(ActionEvent e) {
|
public void actionPerformed(ActionEvent e) {
|
||||||
JFileChooser fileChooser = new JFileChooser();
|
JFileChooser fileChooser = new JFileChooser();
|
||||||
FileNameExtensionFilter filter = new FileNameExtensionFilter(
|
FileNameExtensionFilter filter = new FileNameExtensionFilter(
|
||||||
"ERME Serialized Files",
|
"JSON Files",
|
||||||
"erme"
|
"json"
|
||||||
);
|
);
|
||||||
fileChooser.setFileFilter(filter);
|
fileChooser.setFileFilter(filter);
|
||||||
Preferences prefs = Preferences.userNodeForPackage(LoadAction.class);
|
Preferences prefs = Preferences.userNodeForPackage(LoadAction.class);
|
||||||
|
@ -56,11 +59,15 @@ public class LoadAction extends AbstractAction {
|
||||||
JOptionPane.showMessageDialog(fileChooser, "The selected file cannot be read.", "Invalid File", JOptionPane.WARNING_MESSAGE);
|
JOptionPane.showMessageDialog(fileChooser, "The selected file cannot be read.", "Invalid File", JOptionPane.WARNING_MESSAGE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(chosenFile))) {
|
try (FileInputStream fis = new FileInputStream(chosenFile)) {
|
||||||
MappingModel loadedModel = (MappingModel) ois.readObject();
|
ObjectMapper mapper = JsonMapper.builder()
|
||||||
this.diagramPanel.setModel(loadedModel);
|
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||||
|
.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true)
|
||||||
|
.build();
|
||||||
|
JsonNode data = mapper.readValue(fis, JsonNode.class);
|
||||||
|
this.diagramPanel.setModel(MappingModel.fromJson((ObjectNode) data));
|
||||||
prefs.put(LAST_LOAD_LOCATION_KEY, chosenFile.getAbsolutePath());
|
prefs.put(LAST_LOAD_LOCATION_KEY, chosenFile.getAbsolutePath());
|
||||||
} catch (IOException | ClassNotFoundException | ClassCastException ex) {
|
} catch (Exception 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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
package nl.andrewlalis.erme.control.actions;
|
package nl.andrewlalis.erme.control.actions;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.MapperFeature;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||||
|
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import nl.andrewlalis.erme.model.MappingModel;
|
import nl.andrewlalis.erme.model.MappingModel;
|
||||||
import nl.andrewlalis.erme.view.DiagramPanel;
|
import nl.andrewlalis.erme.view.DiagramPanel;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import javax.swing.filechooser.FileNameExtensionFilter;
|
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||||
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.File;
|
import java.io.File;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.ObjectOutputStream;
|
|
||||||
import java.util.prefs.Preferences;
|
import java.util.prefs.Preferences;
|
||||||
|
|
||||||
public class SaveAction extends AbstractAction {
|
public class SaveAction extends AbstractAction {
|
||||||
|
@ -29,6 +31,7 @@ public class SaveAction extends AbstractAction {
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
private MappingModel model;
|
private MappingModel model;
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
private DiagramPanel diagramPanel;
|
private DiagramPanel diagramPanel;
|
||||||
|
|
||||||
|
@ -42,8 +45,8 @@ public class SaveAction extends AbstractAction {
|
||||||
public void actionPerformed(ActionEvent e) {
|
public void actionPerformed(ActionEvent e) {
|
||||||
JFileChooser fileChooser = new JFileChooser();
|
JFileChooser fileChooser = new JFileChooser();
|
||||||
FileNameExtensionFilter filter = new FileNameExtensionFilter(
|
FileNameExtensionFilter filter = new FileNameExtensionFilter(
|
||||||
"ERME Serialized Files",
|
"JSON Files",
|
||||||
"erme"
|
"json"
|
||||||
);
|
);
|
||||||
fileChooser.setFileFilter(filter);
|
fileChooser.setFileFilter(filter);
|
||||||
Preferences prefs = Preferences.userNodeForPackage(SaveAction.class);
|
Preferences prefs = Preferences.userNodeForPackage(SaveAction.class);
|
||||||
|
@ -55,15 +58,23 @@ public class SaveAction extends AbstractAction {
|
||||||
if (choice == JFileChooser.APPROVE_OPTION) {
|
if (choice == JFileChooser.APPROVE_OPTION) {
|
||||||
File chosenFile = fileChooser.getSelectedFile();
|
File chosenFile = fileChooser.getSelectedFile();
|
||||||
if (chosenFile == null || chosenFile.isDirectory()) {
|
if (chosenFile == null || chosenFile.isDirectory()) {
|
||||||
JOptionPane.showMessageDialog(fileChooser, "The selected file cannot be written to.", "Invalid File", JOptionPane.WARNING_MESSAGE);
|
JOptionPane.showMessageDialog(this.diagramPanel, "The selected file cannot be written to.", "Invalid File", JOptionPane.WARNING_MESSAGE);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!chosenFile.exists() && !chosenFile.getName().endsWith(".erme")) {
|
if (!chosenFile.exists() && !chosenFile.getName().endsWith(".json")) {
|
||||||
chosenFile = new File(chosenFile.getParent(), chosenFile.getName() + ".erme");
|
chosenFile = new File(chosenFile.getParent(), chosenFile.getName() + ".json");
|
||||||
|
} else if (chosenFile.exists()) {
|
||||||
|
int result = JOptionPane.showConfirmDialog(this.diagramPanel, "Are you sure you want overwrite this file?", "Overwrite", JOptionPane.YES_NO_OPTION);
|
||||||
|
if (result == JOptionPane.NO_OPTION) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// TODO: Check for confirm before overwriting.
|
try (FileOutputStream fos = new FileOutputStream(chosenFile)) {
|
||||||
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(chosenFile))) {
|
ObjectMapper mapper = JsonMapper.builder()
|
||||||
oos.writeObject(this.model);
|
.configure(SerializationFeature.INDENT_OUTPUT, true)
|
||||||
|
.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true)
|
||||||
|
.build();
|
||||||
|
mapper.writeValue(fos, this.model.toJson(mapper));
|
||||||
prefs.put(LAST_SAVE_LOCATION_KEY, chosenFile.getAbsolutePath());
|
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) {
|
||||||
|
|
|
@ -3,14 +3,13 @@ package nl.andrewlalis.erme.model;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import nl.andrewlalis.erme.view.view_models.AttributeViewModel;
|
import nl.andrewlalis.erme.view.view_models.AttributeViewModel;
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A single value that belongs to a relation.
|
* A single value that belongs to a relation.
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
public class Attribute implements Serializable {
|
public class Attribute {
|
||||||
private final Relation relation;
|
private final Relation relation;
|
||||||
private AttributeType type;
|
private AttributeType type;
|
||||||
private String name;
|
private String name;
|
||||||
|
@ -32,7 +31,7 @@ public class Attribute implements Serializable {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.relation.getModel().fireChangedEvent();
|
this.relation.getModel().fireChangedEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public AttributeViewModel getViewModel() {
|
public AttributeViewModel getViewModel() {
|
||||||
if (this.viewModel == null) {
|
if (this.viewModel == null) {
|
||||||
this.viewModel = new AttributeViewModel(this);
|
this.viewModel = new AttributeViewModel(this);
|
||||||
|
|
|
@ -1,29 +1,27 @@
|
||||||
package nl.andrewlalis.erme.model;
|
package nl.andrewlalis.erme.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import nl.andrewlalis.erme.view.OrderableListPanel;
|
|
||||||
import nl.andrewlalis.erme.view.view_models.MappingModelViewModel;
|
import nl.andrewlalis.erme.view.view_models.MappingModelViewModel;
|
||||||
import nl.andrewlalis.erme.view.view_models.ViewModel;
|
import nl.andrewlalis.erme.view.view_models.ViewModel;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.*;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This model contains all the information about a single mapping diagram,
|
* This model contains all the information about a single mapping diagram,
|
||||||
* including each mapped table and the links between them.
|
* including each mapped table and the links between them.
|
||||||
*/
|
*/
|
||||||
public class MappingModel implements Serializable, Viewable {
|
public class MappingModel implements Viewable {
|
||||||
@Getter
|
@Getter
|
||||||
private final Set<Relation> relations;
|
private final Set<Relation> relations;
|
||||||
|
|
||||||
private transient Set<ModelChangeListener> changeListeners;
|
private transient final Set<ModelChangeListener> changeListeners;
|
||||||
|
|
||||||
private final static long serialVersionUID = 6153776381873250304L;
|
|
||||||
|
|
||||||
public MappingModel() {
|
public MappingModel() {
|
||||||
this.relations = new HashSet<>();
|
this.relations = new HashSet<>();
|
||||||
|
@ -42,10 +40,20 @@ public class MappingModel implements Serializable, Viewable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the list of relations which are currently selected.
|
||||||
|
* @return The list of relations which are selected.
|
||||||
|
*/
|
||||||
public List<Relation> getSelectedRelations() {
|
public List<Relation> getSelectedRelations() {
|
||||||
return this.relations.stream().filter(Relation::isSelected).collect(Collectors.toList());
|
return this.relations.stream().filter(Relation::isSelected).collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds an attribute in this model, or returns null otherwise.
|
||||||
|
* @param relationName The name of the relation the attribute is in.
|
||||||
|
* @param attributeName The name of the attribute.
|
||||||
|
* @return The attribute which was found, or null if none was found.
|
||||||
|
*/
|
||||||
public Attribute findAttribute(String relationName, String attributeName) {
|
public Attribute findAttribute(String relationName, String attributeName) {
|
||||||
for (Relation r : this.getRelations()) {
|
for (Relation r : this.getRelations()) {
|
||||||
if (!r.getName().equals(relationName)) continue;
|
if (!r.getName().equals(relationName)) continue;
|
||||||
|
@ -58,6 +66,11 @@ public class MappingModel implements Serializable, Viewable {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes all attributes from any relation in the model which reference the
|
||||||
|
* given attribute.
|
||||||
|
* @param referenced The attribute to remove references from.
|
||||||
|
*/
|
||||||
public void removeAllReferencingAttributes(Attribute referenced) {
|
public void removeAllReferencingAttributes(Attribute referenced) {
|
||||||
for (Relation r : this.getRelations()) {
|
for (Relation r : this.getRelations()) {
|
||||||
Set<Attribute> removalSet = new HashSet<>();
|
Set<Attribute> removalSet = new HashSet<>();
|
||||||
|
@ -73,6 +86,10 @@ public class MappingModel implements Serializable, Viewable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the bounding rectangle around all relations of the model.
|
||||||
|
* @return The bounding rectangle around all relations in this model.
|
||||||
|
*/
|
||||||
public Rectangle getRelationBounds() {
|
public Rectangle getRelationBounds() {
|
||||||
if (this.getRelations().isEmpty()) {
|
if (this.getRelations().isEmpty()) {
|
||||||
return new Rectangle(0, 0, 0, 0);
|
return new Rectangle(0, 0, 0, 0);
|
||||||
|
@ -90,14 +107,20 @@ public class MappingModel implements Serializable, Viewable {
|
||||||
return new Rectangle(minX, minY, maxX - minX, maxY - minY);
|
return new Rectangle(minX, minY, maxX - minX, maxY - minY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a listener to this model, which will be notified of changes to the
|
||||||
|
* model.
|
||||||
|
* @param listener The listener to add.
|
||||||
|
*/
|
||||||
public void addChangeListener(ModelChangeListener listener) {
|
public void addChangeListener(ModelChangeListener listener) {
|
||||||
if (this.changeListeners == null) {
|
|
||||||
this.changeListeners = new HashSet<>();
|
|
||||||
}
|
|
||||||
this.changeListeners.add(listener);
|
this.changeListeners.add(listener);
|
||||||
listener.onModelChanged();
|
listener.onModelChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires an all-purpose event which notifies all listeners that the model
|
||||||
|
* has changed.
|
||||||
|
*/
|
||||||
public final void fireChangedEvent() {
|
public final void fireChangedEvent() {
|
||||||
this.changeListeners.forEach(ModelChangeListener::onModelChanged);
|
this.changeListeners.forEach(ModelChangeListener::onModelChanged);
|
||||||
}
|
}
|
||||||
|
@ -146,4 +169,106 @@ public class MappingModel implements Serializable, Viewable {
|
||||||
this.getRelations().forEach(r -> c.addRelation(r.copy(c)));
|
this.getRelations().forEach(r -> c.addRelation(r.copy(c)));
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ObjectNode toJson(ObjectMapper mapper) {
|
||||||
|
ObjectNode node = mapper.createObjectNode();
|
||||||
|
ArrayNode relationsArray = node.withArray("relations");
|
||||||
|
for (Relation r : this.relations) {
|
||||||
|
ObjectNode relationNode = mapper.createObjectNode()
|
||||||
|
.put("name", r.getName());
|
||||||
|
ObjectNode positionNode = mapper.createObjectNode()
|
||||||
|
.put("x", r.getPosition().x)
|
||||||
|
.put("y", r.getPosition().y);
|
||||||
|
relationNode.set("position", positionNode);
|
||||||
|
ArrayNode attributesArray = relationNode.withArray("attributes");
|
||||||
|
for (Attribute a : r.getAttributes()) {
|
||||||
|
ObjectNode attributeNode = mapper.createObjectNode()
|
||||||
|
.put("name", a.getName())
|
||||||
|
.put("type", a.getType().name());
|
||||||
|
if (a instanceof ForeignKeyAttribute) {
|
||||||
|
ForeignKeyAttribute fk = (ForeignKeyAttribute) a;
|
||||||
|
ObjectNode referenceNode = mapper.createObjectNode()
|
||||||
|
.put("relation", fk.getReference().getRelation().getName())
|
||||||
|
.put("attribute", fk.getReference().getName());
|
||||||
|
attributeNode.set("references", referenceNode);
|
||||||
|
}
|
||||||
|
attributesArray.add(attributeNode);
|
||||||
|
}
|
||||||
|
relationsArray.add(relationNode);
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MappingModel fromJson(ObjectNode node) {
|
||||||
|
MappingModel model = new MappingModel();
|
||||||
|
for (JsonNode relationNodeRaw : node.withArray("relations")) {
|
||||||
|
if (!relationNodeRaw.isObject()) throw new IllegalArgumentException();
|
||||||
|
ObjectNode relationNode = (ObjectNode) relationNodeRaw;
|
||||||
|
String name = relationNode.get("name").asText();
|
||||||
|
int x = relationNode.get("position").get("x").asInt();
|
||||||
|
int y = relationNode.get("position").get("y").asInt();
|
||||||
|
Point position = new Point(x, y);
|
||||||
|
Relation relation = new Relation(model, position, name);
|
||||||
|
for (JsonNode attributeNodeRaw : relationNode.withArray("attributes")) {
|
||||||
|
if (!attributeNodeRaw.isObject()) throw new IllegalArgumentException();
|
||||||
|
ObjectNode attributeNode = (ObjectNode) attributeNodeRaw;
|
||||||
|
String attributeName = attributeNode.get("name").asText();
|
||||||
|
AttributeType type = AttributeType.valueOf(attributeNode.get("type").asText().toUpperCase());
|
||||||
|
Attribute attribute = new Attribute(relation, type, attributeName);
|
||||||
|
relation.addAttribute(attribute);
|
||||||
|
}
|
||||||
|
model.addRelation(relation);
|
||||||
|
}
|
||||||
|
addForeignKeys(model, node);
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addForeignKeys(MappingModel model, ObjectNode node) {
|
||||||
|
Map<Attribute, ObjectNode> references = buildReferenceMap(model, node);
|
||||||
|
while (!references.isEmpty()) {
|
||||||
|
boolean workDone = false;
|
||||||
|
for (Map.Entry<Attribute, ObjectNode> entry : references.entrySet()) {
|
||||||
|
Attribute attribute = entry.getKey();
|
||||||
|
String referencedName = entry.getValue().get("attribute").asText();
|
||||||
|
String referencedRelation = entry.getValue().get("relation").asText();
|
||||||
|
Attribute referencedAttribute = model.findAttribute(referencedRelation, referencedName);
|
||||||
|
if (referencedAttribute == null) throw new IllegalArgumentException("Foreign key referenced unknown attribute.");
|
||||||
|
if (!references.containsKey(referencedAttribute)) {
|
||||||
|
ForeignKeyAttribute fk = new ForeignKeyAttribute(attribute.getRelation(), attribute.getType(), attribute.getName(), referencedAttribute);
|
||||||
|
attribute.getRelation().removeAttribute(attribute);
|
||||||
|
attribute.getRelation().addAttribute(fk);
|
||||||
|
references.remove(attribute);
|
||||||
|
workDone = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!workDone) {
|
||||||
|
throw new IllegalArgumentException("Invalid foreign key structure. Possible cyclic references.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a map that contains the set of foreign key references, indexed by
|
||||||
|
* the primitive attribute that is referencing another.
|
||||||
|
* @param model The model to lookup attributes from.
|
||||||
|
* @param node The raw JSON data for the model.
|
||||||
|
* @return A map containing foreign key references, to be used to build a
|
||||||
|
* complete model with foreign key attributes.
|
||||||
|
*/
|
||||||
|
private static Map<Attribute, ObjectNode> buildReferenceMap(MappingModel model, ObjectNode node) {
|
||||||
|
Map<Attribute, ObjectNode> references = new HashMap<>();
|
||||||
|
for (JsonNode r : node.withArray("relations")) {
|
||||||
|
for (JsonNode a : r.withArray("attributes")) {
|
||||||
|
if (a.has("references") && a.get("references").isObject()) {
|
||||||
|
ObjectNode referenceNode = (ObjectNode) a.get("references");
|
||||||
|
String attributeName = a.get("name").asText();
|
||||||
|
String relationName = r.get("name").asText();
|
||||||
|
Attribute attribute = model.findAttribute(relationName, attributeName);
|
||||||
|
if (attribute == null) throw new IllegalArgumentException("Mapping model is not complete. Missing attribute " + attributeName + " in relation " + relationName + ".");
|
||||||
|
references.put(attribute, referenceNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return references;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import nl.andrewlalis.erme.view.view_models.RelationViewModel;
|
||||||
import nl.andrewlalis.erme.view.view_models.ViewModel;
|
import nl.andrewlalis.erme.view.view_models.ViewModel;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
@ -15,7 +14,7 @@ import java.util.stream.Collectors;
|
||||||
* Represents a single "relation" or table in the diagram.
|
* Represents a single "relation" or table in the diagram.
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
public class Relation implements Serializable, Viewable, Comparable<Relation> {
|
public class Relation implements Viewable, Comparable<Relation> {
|
||||||
private final MappingModel model;
|
private final MappingModel model;
|
||||||
private Point position;
|
private Point position;
|
||||||
private String name;
|
private String name;
|
||||||
|
@ -24,11 +23,15 @@ public class Relation implements Serializable, Viewable, Comparable<Relation> {
|
||||||
private transient boolean selected;
|
private transient boolean selected;
|
||||||
private transient RelationViewModel viewModel;
|
private transient RelationViewModel viewModel;
|
||||||
|
|
||||||
public Relation(MappingModel model, Point position, String name) {
|
public Relation(MappingModel model, Point position, String name, List<Attribute> attributes) {
|
||||||
this.model = model;
|
this.model = model;
|
||||||
this.position = position;
|
this.position = position;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.attributes = new ArrayList<>();
|
this.attributes = attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Relation(MappingModel model, Point position, String name) {
|
||||||
|
this(model, position, name, new ArrayList<>());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPosition(Point position) {
|
public void setPosition(Point position) {
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
package nl.andrewlalis.erme.util;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
public class Hash {
|
|
||||||
public static boolean matches(byte[] password, String resourceFile) {
|
|
||||||
MessageDigest md;
|
|
||||||
try {
|
|
||||||
md = MessageDigest.getInstance("SHA-256");
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
byte[] passwordHash = md.digest(password);
|
|
||||||
InputStream is = Hash.class.getClassLoader().getResourceAsStream(resourceFile);
|
|
||||||
if (is == null) {
|
|
||||||
System.err.println("Could not obtain input stream to admin_hash.txt");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
char[] buffer = new char[64];
|
|
||||||
try (BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
|
|
||||||
if (br.read(buffer) != buffer.length) {
|
|
||||||
System.err.println("Incorrect number of characters read from hash file.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String hashHex = String.valueOf(buffer);
|
|
||||||
byte[] hash = hexStringToByteArray(hashHex);
|
|
||||||
return Arrays.equals(passwordHash, hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] hexStringToByteArray(String s) {
|
|
||||||
int len = s.length();
|
|
||||||
byte[] data = new byte[len / 2];
|
|
||||||
for (int i = 0; i < len; i += 2) {
|
|
||||||
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
|
||||||
+ Character.digit(s.charAt(i+1), 16));
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -117,6 +117,7 @@ public class DiagramPanel extends JPanel implements ModelChangeListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates all the action singletons with the latest model information.
|
* Updates all the action singletons with the latest model information.
|
||||||
|
* TODO: Clean this up somehow!
|
||||||
*/
|
*/
|
||||||
private void updateActionModels() {
|
private void updateActionModels() {
|
||||||
NewModelAction.getInstance().setDiagramPanel(this);
|
NewModelAction.getInstance().setDiagramPanel(this);
|
||||||
|
@ -134,10 +135,10 @@ public class DiagramPanel extends JPanel implements ModelChangeListener {
|
||||||
RemoveAttributeAction.getInstance().setDiagramPanel(this);
|
RemoveAttributeAction.getInstance().setDiagramPanel(this);
|
||||||
LoadSampleModelAction.getInstance().setDiagramPanel(this);
|
LoadSampleModelAction.getInstance().setDiagramPanel(this);
|
||||||
LolcatAction.getInstance().setDiagramPanel(this);
|
LolcatAction.getInstance().setDiagramPanel(this);
|
||||||
AutoPositionAction.getInstance().setDiagramPanel(this);
|
AutoPositionAction.getInstance().setDiagramPanel(this);
|
||||||
AutoPositionAction.getInstance().setModel(this.model);
|
AutoPositionAction.getInstance().setModel(this.model);
|
||||||
OrderableListPanel.getInstance().setModel(this.model);
|
OrderableListPanel.getInstance().setModel(this.model);
|
||||||
AboutAction.getInstance().setDiagramPanel(this);
|
AboutAction.getInstance().setDiagramPanel(this);
|
||||||
ExitAction.getInstance().setDiagramPanel(this);
|
ExitAction.getInstance().setDiagramPanel(this);
|
||||||
InstructionsAction.getInstance().setDiagramPanel(this);
|
InstructionsAction.getInstance().setDiagramPanel(this);
|
||||||
MappingAlgorithmHelpAction.getInstance().setDiagramPanel(this);
|
MappingAlgorithmHelpAction.getInstance().setDiagramPanel(this);
|
||||||
|
|
|
@ -2,17 +2,30 @@ package nl.andrewlalis.erme.view;
|
||||||
|
|
||||||
import nl.andrewlalis.erme.model.MappingModel;
|
import nl.andrewlalis.erme.model.MappingModel;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main JFrame for the editor.
|
* The main JFrame for the editor.
|
||||||
*/
|
*/
|
||||||
public class EditorFrame extends JFrame {
|
public class EditorFrame extends JFrame {
|
||||||
public EditorFrame(boolean includeAdminActions) {
|
public EditorFrame() {
|
||||||
super("ER-Mapping Editor");
|
super("ER-Mapping Editor");
|
||||||
this.setContentPane(new DiagramPanel(new MappingModel()));
|
this.setContentPane(new DiagramPanel(new MappingModel()));
|
||||||
this.setJMenuBar(new EditorMenuBar(includeAdminActions));
|
this.setJMenuBar(new EditorMenuBar());
|
||||||
|
try {
|
||||||
|
InputStream is = getClass().getClassLoader().getResourceAsStream("icon.png");
|
||||||
|
if (is == null) {
|
||||||
|
System.err.println("Could not load application icon.");
|
||||||
|
} else {
|
||||||
|
this.setIconImage(ImageIO.read(is));
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
this.setMinimumSize(new Dimension(400, 400));
|
this.setMinimumSize(new Dimension(400, 400));
|
||||||
this.setPreferredSize(new Dimension(800, 800));
|
this.setPreferredSize(new Dimension(800, 800));
|
||||||
this.pack();
|
this.pack();
|
||||||
|
|
|
@ -12,10 +12,7 @@ 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 {
|
||||||
private final boolean includeAdminActions;
|
public EditorMenuBar() {
|
||||||
|
|
||||||
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());
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
cfdabe75d984e5a92fb491dadc9091419d9587c049246356a488e83a75505bce
|
|
|
@ -12,7 +12,7 @@
|
||||||
<p><em>A simple UI for editing entity-relation mapping diagrams.</em></p>
|
<p><em>A simple UI for editing entity-relation mapping diagrams.</em></p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Have you noticed any unexpected behavior? Is there something you thing would make a good addition to this application?
|
Have you noticed any unexpected behavior? Is there something you think would make a good addition to this application?
|
||||||
<a href="https://github.com/andrewlalis/EntityRelationMappingEditor/issues">Create a new issue on GitHub!</a>
|
<a href="https://github.com/andrewlalis/EntityRelationMappingEditor/issues">Create a new issue on GitHub!</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
Loading…
Reference in New Issue