Removed useless admin stuff, changed to save files as JSON, added image scaling for exports.

This commit is contained in:
Andrew Lalis 2021-10-27 13:00:55 +02:00
parent d63eb3345f
commit bc464ba42b
19 changed files with 247 additions and 125 deletions

View File

@ -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
View File

@ -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>

View File

@ -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");
}
} }

View File

@ -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;

View File

@ -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;

View File

@ -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);
} }

View File

@ -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 (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(chosenFile))) { try (FileOutputStream fos = new FileOutputStream(chosenFile)) {
oos.writeObject(this.model); ObjectMapper mapper = JsonMapper.builder()
.configure(SerializationFeature.INDENT_OUTPUT, true)
.configure(MapperFeature.PROPAGATE_TRANSIENT_MARKER, true)
.build();
mapper.writeValue(fos, this.model.toJson(mapper));
prefs.put(LAST_SAVE_LOCATION_KEY, chosenFile.getAbsolutePath()); 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) {

View File

@ -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;

View File

@ -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;
}
} }

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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();

View File

@ -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());

View File

@ -1 +0,0 @@
cfdabe75d984e5a92fb491dadc9091419d9587c049246356a488e83a75505bce

View File

@ -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>

BIN
src/main/resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB