diff --git a/README.md b/README.md index 638e4a0..0d9cbc0 100644 --- a/README.md +++ b/README.md @@ -43,11 +43,15 @@ Some encryption classes that use format-preserving-encryption. ### fx -JavaFX library that binds an FXML View to an ordinary Java Model. See [fx/README.md](fx/README.md). +JavaFX library that binds an FXML view to an ordinary Java model. See [fx/README.md](fx/README.md). + +### injection + +Package scanner and type-based container used by `fx`. See [injection/README.md](injection/README.md). ### fx-example -Person desk that shows how to use `fx`. Run `mvn -pl fx-example javafx:run`. +Person desk that shows how to use `fx` and `injection`. Run `mvn -pl fx-example -am javafx:run`. See [fx-example/README.md](fx-example/README.md). ### gson diff --git a/core/src/main/java/de/neitzel/core/sql/Query.java b/core/src/main/java/de/neitzel/core/sql/Query.java index e588f11..2974ae1 100644 --- a/core/src/main/java/de/neitzel/core/sql/Query.java +++ b/core/src/main/java/de/neitzel/core/sql/Query.java @@ -242,8 +242,16 @@ public class Query { */ private Stream streamFromResultSet(final ResultSet rs, final Function factory) { Iterator iterator = new Iterator<>() { + /** + * Whether this iterator has already looked ahead in the {@link ResultSet}. + * A later hasNext call reuses that look-ahead until next consumes the row. + */ private boolean hasNextChecked = false; + /** + * Whether the look-ahead found another row in the {@link ResultSet}. + * The value is meaningful only after the look-ahead has run. + */ private boolean hasNext; /** diff --git a/encryption/src/main/java/de/neitzel/encryption/FF1Encryption.java b/encryption/src/main/java/de/neitzel/encryption/FF1Encryption.java index 87c849d..d90ff1e 100644 --- a/encryption/src/main/java/de/neitzel/encryption/FF1Encryption.java +++ b/encryption/src/main/java/de/neitzel/encryption/FF1Encryption.java @@ -15,6 +15,10 @@ import java.security.SecureRandom; @SuppressWarnings("unused") public class FF1Encryption { + /** + * Random source used when creating a new tweak. + * {@link #createNewTweak(int)} fills the tweak from this generator. + */ private static final SecureRandom RANDOM = new SecureRandom(); /** diff --git a/fx-example/README.md b/fx-example/README.md index 9056906..2013b0e 100644 --- a/fx-example/README.md +++ b/fx-example/README.md @@ -1,11 +1,13 @@ # fx-example -A person desk that uses the [`fx`](../fx/README.md) library: a Direct binding, a Converter, a Computed binding, a nested Component, a Repeat, a list of simple values, and Messages. +A person desk that uses [`fx`](../fx/README.md) and [`injection`](../injection/README.md). The guides use this module as the worked example. + +The desk shows a direct binding in each direction, a converter, a computed binding, a view model subclass, a component with a nested note, a repeat, a table with a starting width and an open column, a date column, a list of simple values, and messages. The container side shows `@Config`, `@Singleton`, `@Named`, and injection by constructor, field, and setter. From the NeitzelLib root, with Java 21: ```bash -mvn -pl fx-example javafx:run +mvn -pl fx-example -am javafx:run ``` -The launch class is `de.neitzel.fx.example.Main`. The Model is an ordinary `Person`. The View is `Desk.fxml`. +The launch class is `de.neitzel.fx.example.Main`. The model is an ordinary `Person`. The view is `Desk.fxml`. `DeskLoadsTest` loads that same desk. diff --git a/fx-example/pom.xml b/fx-example/pom.xml index b8cc2d9..aa380e5 100644 --- a/fx-example/pom.xml +++ b/fx-example/pom.xml @@ -52,6 +52,41 @@ ${main.class} + + org.apache.maven.plugins + maven-jar-plugin + + + + true + libs/ + ${main.class} + + + + + + org.apache.maven.plugins + maven-dependency-plugin + ${maven.dependency.plugin} + + + copy-dependencies + package + + copy-dependencies + + + ${project.build.directory}/libs + runtime + true + false + true + true + + + + com.github.spotbugs spotbugs-maven-plugin diff --git a/fx-example/src/main/java/de/neitzel/fx/example/DeskApp.java b/fx-example/src/main/java/de/neitzel/fx/example/DeskApp.java index 1ba8d82..f0cbb25 100644 --- a/fx-example/src/main/java/de/neitzel/fx/example/DeskApp.java +++ b/fx-example/src/main/java/de/neitzel/fx/example/DeskApp.java @@ -1,22 +1,31 @@ package de.neitzel.fx.example; import de.neitzel.fx.example.model.Address; +import de.neitzel.fx.example.model.Note; import de.neitzel.fx.example.model.Person; +import de.neitzel.fx.example.model.Role; import de.neitzel.fx.example.view.DeskController; import de.neitzel.fx.view.LoadedView; import de.neitzel.fx.view.MessagePopup; import de.neitzel.fx.view.ViewLoader; import de.neitzel.injection.ApplicationContext; +import de.neitzel.injection.annotation.Config; import javafx.application.Application; import javafx.scene.Scene; import javafx.stage.Stage; +import java.time.LocalDate; import java.util.ArrayList; import java.util.List; /** * Opens the person desk for one ordinary {@link Person}. + *

+ * {@link Config} names the package the container scans. The same package would be used without + * the annotation, because this class lives there. The attribute is set so the annotation is what + * chooses the package. */ +@Config(basePackage = "de.neitzel.fx.example") public class DeskApp extends Application { /** @@ -28,16 +37,24 @@ public class DeskApp extends Application { /** * Builds the person the desk edits. * - * @return a person with a home address, one visit, and two tags + * @return a person with a home address, three visits, and two tags */ public static Person sample() { Address home = new Address("Sample Street 1", "Sample City"); + home.setNote(new Note("Kept at home")); Address visit = new Address("Work Road 2", "Office City"); + visit.setVisitedOn(LocalDate.of(2020, 1, 2)); + Address berlin = new Address("Sample Lane", "Berlin"); + berlin.setVisitedOn(LocalDate.of(2020, 3, 4)); + Address bonn = new Address("Oak Street", "Bonn"); + bonn.setVisitedOn(LocalDate.of(2019, 12, 1)); Person person = new Person(); person.setName("Ada Lovelace"); person.setAge(36); + person.setActive(true); + person.setRole(Role.AUTHOR); person.setHome(home); - person.setVisits(new ArrayList<>(List.of(visit))); + person.setVisits(new ArrayList<>(List.of(visit, berlin, bonn))); person.setSelectedVisit(visit); person.setTags(new ArrayList<>(List.of("dns", "math"))); return person; diff --git a/fx-example/src/main/java/de/neitzel/fx/example/Main.java b/fx-example/src/main/java/de/neitzel/fx/example/Main.java index 717567b..d02c7c2 100644 --- a/fx-example/src/main/java/de/neitzel/fx/example/Main.java +++ b/fx-example/src/main/java/de/neitzel/fx/example/Main.java @@ -9,6 +9,9 @@ import javafx.application.Application; */ public final class Main { + /** + * Prevents instantiation. The class is only a launch entry point. + */ private Main() { } diff --git a/fx-example/src/main/java/de/neitzel/fx/example/model/Address.java b/fx-example/src/main/java/de/neitzel/fx/example/model/Address.java index 1621577..17eb8d0 100644 --- a/fx-example/src/main/java/de/neitzel/fx/example/model/Address.java +++ b/fx-example/src/main/java/de/neitzel/fx/example/model/Address.java @@ -1,13 +1,32 @@ package de.neitzel.fx.example.model; +import java.time.LocalDate; + /** * An ordinary address the desk edits. */ public class Address { + /** + * The street. + */ private String street = ""; + + /** + * The city. + */ private String city = ""; + /** + * The day of the visit, or {@code null} when the address is not a dated visit. + */ + private LocalDate visitedOn; + + /** + * The note attached to this address, or {@code null} when there is none. + */ + private Note note; + /** * Creates an empty address. */ @@ -61,6 +80,42 @@ public class Address { this.city = city; } + /** + * Returns the day of the visit. + * + * @return the date, or {@code null} when none is set + */ + public LocalDate getVisitedOn() { + return visitedOn; + } + + /** + * Sets the day of the visit. + * + * @param visitedOn the date, or {@code null} to clear it + */ + public void setVisitedOn(LocalDate visitedOn) { + this.visitedOn = visitedOn; + } + + /** + * Returns the note attached to this address. + * + * @return the note, or {@code null} when none is set + */ + public Note getNote() { + return note; + } + + /** + * Sets the note attached to this address. + * + * @param note the note, or {@code null} when there is none + */ + public void setNote(Note note) { + this.note = note; + } + /** * Returns the street and city, for a combo box row. * diff --git a/fx-example/src/main/java/de/neitzel/fx/example/model/Note.java b/fx-example/src/main/java/de/neitzel/fx/example/model/Note.java new file mode 100644 index 0000000..5d87220 --- /dev/null +++ b/fx-example/src/main/java/de/neitzel/fx/example/model/Note.java @@ -0,0 +1,48 @@ +package de.neitzel.fx.example.model; + +/** + * A short note attached to an address. + *

+ * The class is a complex attribute of {@link Address}: it has a read-write property of its own. + * The home address form nests a component that edits this note. + */ +public class Note { + + /** + * The note text. + */ + private String text = ""; + + /** + * Creates an empty note. + */ + public Note() { + } + + /** + * Creates a note with the given text. + * + * @param text the note text + */ + public Note(String text) { + this.text = text; + } + + /** + * Returns the note text. + * + * @return the text + */ + public String getText() { + return text; + } + + /** + * Sets the note text. + * + * @param text the text + */ + public void setText(String text) { + this.text = text; + } +} diff --git a/fx-example/src/main/java/de/neitzel/fx/example/model/Person.java b/fx-example/src/main/java/de/neitzel/fx/example/model/Person.java index e3de57e..49fe640 100644 --- a/fx-example/src/main/java/de/neitzel/fx/example/model/Person.java +++ b/fx-example/src/main/java/de/neitzel/fx/example/model/Person.java @@ -10,13 +10,46 @@ import java.util.List; */ public class Person { + /** + * The person's name. + */ private String name = ""; + + /** + * The person's age in years, or {@code null} when it has not been set. + */ private Integer age; + + /** + * The home address. + */ private Address home; + + /** + * The visit addresses. After the View loads, this is the list the visit table and Repeat watch. + */ private List

visits = new ArrayList<>(); + + /** + * The visit selected in the table and the combo box, or {@code null} when none is selected. + */ private Address selectedVisit; + + /** + * The tags. After the View loads, this is the list the list view watches. + */ private List tags = new ArrayList<>(); + /** + * Whether the person is active. A primitive boolean cannot be null. + */ + private boolean active; + + /** + * The person's role, or {@code null} when none is set. + */ + private Role role; + /** * Creates an empty person. */ @@ -98,7 +131,7 @@ public class Person { } /** - * Returns the visit chosen in the combo box. + * Returns the visit chosen in the table and the combo box. * * @return the selected visit, or {@code null} when none is selected */ @@ -107,7 +140,7 @@ public class Person { } /** - * Sets the visit chosen in the combo box. + * Sets the visit chosen in the table and the combo box. * * @param selectedVisit the selected visit */ @@ -134,4 +167,40 @@ public class Person { public void setTags(List tags) { this.tags = tags; } + + /** + * Returns whether the person is active. + * + * @return {@code true} when the person is active + */ + public boolean isActive() { + return active; + } + + /** + * Sets whether the person is active. + * + * @param active {@code true} when the person is active + */ + public void setActive(boolean active) { + this.active = active; + } + + /** + * Returns the role. + * + * @return the role, or {@code null} when none is set + */ + public Role getRole() { + return role; + } + + /** + * Sets the role. + * + * @param role the role + */ + public void setRole(Role role) { + this.role = role; + } } diff --git a/fx-example/src/main/java/de/neitzel/fx/example/model/Role.java b/fx-example/src/main/java/de/neitzel/fx/example/model/Role.java new file mode 100644 index 0000000..990c194 --- /dev/null +++ b/fx-example/src/main/java/de/neitzel/fx/example/model/Role.java @@ -0,0 +1,19 @@ +package de.neitzel.fx.example.model; + +/** + * The role a person has on the desk. + *

+ * An enum is a simple attribute. The desk binds it to a choice box. + */ +public enum Role { + + /** + * A person who reads. + */ + READER, + + /** + * A person who writes. + */ + AUTHOR +} diff --git a/fx-example/src/main/java/de/neitzel/fx/example/view/AddressForm.java b/fx-example/src/main/java/de/neitzel/fx/example/view/AddressForm.java index e2d01c6..be35b6f 100644 --- a/fx-example/src/main/java/de/neitzel/fx/example/view/AddressForm.java +++ b/fx-example/src/main/java/de/neitzel/fx/example/view/AddressForm.java @@ -1,21 +1,47 @@ package de.neitzel.fx.example.view; +import de.neitzel.fx.view.HasViewModel; +import de.neitzel.fx.view.ViewModel; +import jakarta.inject.Inject; import jakarta.inject.Singleton; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.fxml.FXML; +import javafx.scene.control.Label; import javafx.scene.layout.VBox; /** - * Component that edits one address. + * Component that edits the home address, including its nested note. *

- * The class is the root and the Controller of {@code AddressForm.fxml}. A parent View sets - * {@link #modelProperty()} from a Direct binding. A repeated row gets its own instance. + * The class is the root and the controller of {@code AddressForm.fxml}. The desk sets + * {@link #modelProperty()} once. {@link #setHeading} is setter injection. {@link #setViewModel} + * runs after that model is set and before this form's own bindings. */ @Singleton -public class AddressForm extends VBox { +public class AddressForm extends VBox implements HasViewModel { + /** + * The address this form edits. The desk sets it once. + */ private final ObjectProperty model = new SimpleObjectProperty<>(this, "model"); + /** + * The heading shown above the fields. The container sets it through {@link #setHeading}. + */ + private FormHeading heading; + + /** + * The heading label. {@link #initialize()} writes the injected heading onto it. + */ + @FXML + private Label headingLabel; + + /** + * The label filled from the address before the field bindings run. + */ + @FXML + private Label openedAsLabel; + /** * Creates an empty form. The container calls this constructor. */ @@ -23,7 +49,37 @@ public class AddressForm extends VBox { } /** - * Returns the model property a parent View sets. + * Receives the heading. The container calls this setter after construction. + * + * @param heading the heading + */ + @Inject + public void setHeading(FormHeading heading) { + this.heading = heading; + } + + /** + * Writes the injected heading onto the label. + *

+ * FXML calls this during load, before the model is set. + */ + @FXML + public void initialize() { + headingLabel.setText(heading.text()); + } + + /** + * Records the city as it stands when the model arrives, before the field bindings run. + * + * @param viewModel the address view model + */ + @Override + public void setViewModel(ViewModel viewModel) { + openedAsLabel.setText("Opened as " + viewModel.property("city").getValue()); + } + + /** + * Returns the model property the desk sets. * * @return the model property */ diff --git a/fx-example/src/main/java/de/neitzel/fx/example/view/AgeConverter.java b/fx-example/src/main/java/de/neitzel/fx/example/view/AgeConverter.java index 8f3424c..c6f3274 100644 --- a/fx-example/src/main/java/de/neitzel/fx/example/view/AgeConverter.java +++ b/fx-example/src/main/java/de/neitzel/fx/example/view/AgeConverter.java @@ -10,6 +10,9 @@ import jakarta.inject.Singleton; @Singleton public class AgeConverter implements Converter { + /** + * The text written in front of the age. + */ private final AgeSuffix suffix; /** diff --git a/fx-example/src/main/java/de/neitzel/fx/example/view/AgeSuffix.java b/fx-example/src/main/java/de/neitzel/fx/example/view/AgeSuffix.java index 079b237..7fb4fe5 100644 --- a/fx-example/src/main/java/de/neitzel/fx/example/view/AgeSuffix.java +++ b/fx-example/src/main/java/de/neitzel/fx/example/view/AgeSuffix.java @@ -1,11 +1,14 @@ package de.neitzel.fx.example.view; -import jakarta.inject.Singleton; +import jakarta.inject.Named; /** * The suffix an {@link AgeConverter} writes in front of an age. + *

+ * {@link Named} marks the class for scanning the same way {@link jakarta.inject.Singleton} does. + * The container does not read the annotation value, and the instance is still cached. */ -@Singleton +@Named public class AgeSuffix { /** diff --git a/fx-example/src/main/java/de/neitzel/fx/example/view/DeskController.java b/fx-example/src/main/java/de/neitzel/fx/example/view/DeskController.java index 7acb8a5..faade7b 100644 --- a/fx-example/src/main/java/de/neitzel/fx/example/view/DeskController.java +++ b/fx-example/src/main/java/de/neitzel/fx/example/view/DeskController.java @@ -4,28 +4,40 @@ import de.neitzel.fx.example.model.Address; import de.neitzel.fx.view.HasViewModel; import de.neitzel.fx.view.Message; import de.neitzel.fx.view.ViewModel; +import de.neitzel.fx.view.ViewModelClass; import jakarta.inject.Singleton; import javafx.fxml.FXML; import javafx.scene.control.TextField; +import java.time.LocalDate; import java.util.List; /** * Controller for the person desk. *

- * The container creates this class and injects nothing else. {@link #setViewModel} receives the - * ViewModel before the binding elements run, so {@link #greeting()} can read the name field on - * its first call. + * The container creates this class. {@link #setViewModel} receives the view model before the + * binding elements run, so {@link #greeting()} can read the name field and the age on its first + * call. {@link PersonViewModel} is the view model class for this controller. */ @Singleton +@ViewModelClass(PersonViewModel.class) public class DeskController implements HasViewModel { + /** + * The name field. {@link #greeting()} reads it. + */ @FXML private TextField nameField; + /** + * The tag field cleared after {@link #addTag()}. + */ @FXML private TextField tagField; + /** + * The person view model, set before the bindings run. + */ private ViewModel viewModel; /** @@ -35,9 +47,9 @@ public class DeskController implements HasViewModel { } /** - * Receives the ViewModel for the person on the desk. + * Receives the view model for the person on the desk. * - * @param viewModel the ViewModel + * @param viewModel the view model */ @Override public void setViewModel(ViewModel viewModel) { @@ -47,16 +59,18 @@ public class DeskController implements HasViewModel { /** * Returns the greeting written to the label. *

- * The method reads the name field. It does not write the Model. + * The method reads the name field and the age on the view model. It does not write the model. + * A change of either one calls it again. * * @return the greeting */ public String greeting() { String name = nameField.getText(); + Object age = viewModel.property("age").getValue(); if (name == null || name.isBlank()) { - return "Hello"; + return "Hello (" + age + ")"; } - return "Hello, " + name.trim(); + return "Hello, " + name.trim() + " (" + age + ")"; } /** @@ -64,7 +78,21 @@ public class DeskController implements HasViewModel { */ @FXML public void addVisit() { - visits().add(new Address("New street", "New city")); + Address created = new Address("New street", "New city"); + created.setVisitedOn(LocalDate.now()); + visits().add(created); + } + + /** + * Removes a visit from the list the table and Repeat are watching. + * + * @param visit the visit + */ + public void removeVisit(Address visit) { + if (visit == null) { + return; + } + visits().remove(visit); } /** @@ -81,22 +109,63 @@ public class DeskController implements HasViewModel { } /** - * Adds an info Message that quotes the name already stored on the Model. + * Adds an info message that quotes the name already stored on the model. */ @FXML public void leaveNote() { - Object name = viewModel.property("name").getValue(); - viewModel.addMessage(new Message( - Message.Severity.INFO, - "The model already has the name " + name + ".", - "desk")); + leave(Message.Severity.INFO, "The model already has the name " + name() + "."); } + /** + * Adds a warning. The notice label shows it, and it is not written to the log. + */ + @FXML + public void leaveWarning() { + leave(Message.Severity.WARN, "Check the age before leaving the desk."); + } + + /** + * Adds a fatal notice. The notice label shows it, and it is written to the log. + */ + @FXML + public void leaveFatal() { + leave(Message.Severity.FATAL, "The desk cannot store this person."); + } + + /** + * Adds a notice whose source is this desk. + * + * @param severity how serious the notice is + * @param text the notice text + */ + private void leave(Message.Severity severity, String text) { + viewModel.addMessage(new Message(severity, text, "desk")); + } + + /** + * Returns the name currently held by the view model. + * + * @return the name + */ + private Object name() { + return viewModel.property("name").getValue(); + } + + /** + * Returns the visit list the view is watching. + * + * @return the visits + */ @SuppressWarnings("unchecked") private List

visits() { return (List
) viewModel.property("visits").getValue(); } + /** + * Returns the tag list the view is watching. + * + * @return the tags + */ @SuppressWarnings("unchecked") private List tags() { return (List) viewModel.property("tags").getValue(); diff --git a/fx-example/src/main/java/de/neitzel/fx/example/view/FormHeading.java b/fx-example/src/main/java/de/neitzel/fx/example/view/FormHeading.java new file mode 100644 index 0000000..8739dcc --- /dev/null +++ b/fx-example/src/main/java/de/neitzel/fx/example/view/FormHeading.java @@ -0,0 +1,25 @@ +package de.neitzel.fx.example.view; + +import jakarta.inject.Singleton; + +/** + * The heading an address form shows above its fields. + */ +@Singleton +public class FormHeading { + + /** + * Creates the heading used by the desk. + */ + public FormHeading() { + } + + /** + * Returns the heading text. + * + * @return the heading + */ + public String text() { + return "Address"; + } +} diff --git a/fx-example/src/main/java/de/neitzel/fx/example/view/NoteForm.java b/fx-example/src/main/java/de/neitzel/fx/example/view/NoteForm.java new file mode 100644 index 0000000..e96fc70 --- /dev/null +++ b/fx-example/src/main/java/de/neitzel/fx/example/view/NoteForm.java @@ -0,0 +1,54 @@ +package de.neitzel.fx.example.view; + +import jakarta.inject.Singleton; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.layout.VBox; + +/** + * Component that edits one {@link de.neitzel.fx.example.model.Note}. + *

+ * The class is the root and the controller of {@code NoteForm.fxml}. The home address form + * nests this component and sets {@link #modelProperty()} once. + */ +@Singleton +public class NoteForm extends VBox { + + /** + * The note this form edits. The parent component sets it once. + */ + private final ObjectProperty model = new SimpleObjectProperty<>(this, "model"); + + /** + * Creates an empty form. The container calls this constructor. + */ + public NoteForm() { + } + + /** + * Returns the model property the parent sets. + * + * @return the model property + */ + public ObjectProperty modelProperty() { + return model; + } + + /** + * Returns the note this form edits. + * + * @return the note, or {@code null} when none is set + */ + public Object getModel() { + return model.get(); + } + + /** + * Sets the note this form edits. + * + * @param model the note + */ + public void setModel(Object model) { + this.model.set(model); + } +} diff --git a/fx-example/src/main/java/de/neitzel/fx/example/view/PersonViewModel.java b/fx-example/src/main/java/de/neitzel/fx/example/view/PersonViewModel.java new file mode 100644 index 0000000..dc67de0 --- /dev/null +++ b/fx-example/src/main/java/de/neitzel/fx/example/view/PersonViewModel.java @@ -0,0 +1,40 @@ +package de.neitzel.fx.example.view; + +import de.neitzel.fx.example.model.Person; +import de.neitzel.fx.view.ViewModel; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +/** + * ViewModel for a {@link Person} that adds a title beside the person's attributes. + *

+ * The library constructs this class. The container does not. {@link DeskController} names it + * with {@link de.neitzel.fx.view.ViewModelClass}. + */ +public class PersonViewModel extends ViewModel { + + /** + * The title this view model adds. A binding addresses it as {@code title}. + */ + private final StringProperty title = new SimpleStringProperty(this, "title"); + + /** + * Builds the view model and keeps the title aligned with the person's name. + * + * @param model the person + */ + public PersonViewModel(Object model) { + super(model); + title.set("titled-" + ((Person) model).getName()); + property("name").addListener((observable, oldValue, newValue) -> title.set("titled-" + newValue)); + } + + /** + * Returns the title property a binding element addresses by name. + * + * @return the title property + */ + public StringProperty titleProperty() { + return title; + } +} diff --git a/fx-example/src/main/java/de/neitzel/fx/example/view/StreetConverter.java b/fx-example/src/main/java/de/neitzel/fx/example/view/StreetConverter.java new file mode 100644 index 0000000..9302873 --- /dev/null +++ b/fx-example/src/main/java/de/neitzel/fx/example/view/StreetConverter.java @@ -0,0 +1,56 @@ +package de.neitzel.fx.example.view; + +import de.neitzel.fx.view.Converter; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +/** + * Keeps a street that is not blank, and reports an empty street through the injected policy. + *

+ * The container fills {@link #policy} by field injection after the no-arg constructor runs. + */ +@Singleton +public class StreetConverter implements Converter { + + /** + * The notice text used when the street is empty. The container sets this field. + */ + @Inject + private StreetPolicy policy; + + /** + * Creates a converter. The container calls this constructor, then injects {@link #policy}. + */ + public StreetConverter() { + } + + /** + * Returns the street unchanged. + * + * @param attributeValue the street + * @return the text shown in the field + */ + @Override + public Object toControlValue(Object attributeValue) { + if (attributeValue == null) { + return ""; + } + return attributeValue.toString(); + } + + /** + * Returns the street with surrounding whitespace removed. + * + * @param controlValue the field text + * @return the street written to the address + * @throws IllegalArgumentException when the text is blank + */ + @Override + public Object toAttributeValue(Object controlValue) { + String text = controlValue == null ? "" : controlValue.toString().trim(); + if (text.isEmpty()) { + throw new IllegalArgumentException(policy.message()); + } + return text; + } +} diff --git a/fx-example/src/main/java/de/neitzel/fx/example/view/StreetPolicy.java b/fx-example/src/main/java/de/neitzel/fx/example/view/StreetPolicy.java new file mode 100644 index 0000000..a08d885 --- /dev/null +++ b/fx-example/src/main/java/de/neitzel/fx/example/view/StreetPolicy.java @@ -0,0 +1,25 @@ +package de.neitzel.fx.example.view; + +import jakarta.inject.Singleton; + +/** + * The rule a {@link StreetConverter} applies when a street is empty. + */ +@Singleton +public class StreetPolicy { + + /** + * Creates the policy used by the desk. + */ + public StreetPolicy() { + } + + /** + * Returns the notice text for an empty street. + * + * @return the notice text + */ + public String message() { + return "Street is required"; + } +} diff --git a/fx-example/src/main/java/de/neitzel/fx/example/view/VisitForm.java b/fx-example/src/main/java/de/neitzel/fx/example/view/VisitForm.java new file mode 100644 index 0000000..b31c32c --- /dev/null +++ b/fx-example/src/main/java/de/neitzel/fx/example/view/VisitForm.java @@ -0,0 +1,56 @@ +package de.neitzel.fx.example.view; + +import jakarta.inject.Singleton; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.layout.VBox; + +/** + * Component that edits one visit address in the repeated list. + *

+ * The class is the root and the controller of {@code VisitForm.fxml}. Repeat creates a new + * instance for each visit. The home address uses {@link AddressForm}, which nests a note. + * This form does not, so a visit added after the desk has loaded does not reuse the note + * component already showing on the home address. + */ +@Singleton +public class VisitForm extends VBox { + + /** + * The visit this form edits. Repeat sets it once. + */ + private final ObjectProperty model = new SimpleObjectProperty<>(this, "model"); + + /** + * Creates an empty form. The container calls this constructor. + */ + public VisitForm() { + } + + /** + * Returns the model property Repeat sets. + * + * @return the model property + */ + public ObjectProperty modelProperty() { + return model; + } + + /** + * Returns the visit this form edits. + * + * @return the visit, or {@code null} when none is set + */ + public Object getModel() { + return model.get(); + } + + /** + * Sets the visit this form edits. + * + * @param model the visit + */ + public void setModel(Object model) { + this.model.set(model); + } +} diff --git a/fx-example/src/main/java/de/neitzel/fx/example/view/VisitRow.java b/fx-example/src/main/java/de/neitzel/fx/example/view/VisitRow.java new file mode 100644 index 0000000..d0a2275 --- /dev/null +++ b/fx-example/src/main/java/de/neitzel/fx/example/view/VisitRow.java @@ -0,0 +1,77 @@ +package de.neitzel.fx.example.view; + +import de.neitzel.fx.example.model.Address; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.fxml.FXML; +import javafx.scene.layout.HBox; + +/** + * Row that edits one visit in the visit table. + *

+ * The class is the root and the Controller of {@code VisitRow.fxml}. The table sets + * {@link #modelProperty()} once, and this row binds its own fields. Those fields are in column + * order: street, city, then the visit date. Remove sits after them and is not a column. + */ +@Singleton +public class VisitRow extends HBox { + + /** + * The desk controller that removes a visit from the person. + */ + private final DeskController desk; + + /** + * The visit this row edits. The table sets it once. + */ + private final ObjectProperty model = new SimpleObjectProperty<>(this, "model"); + + /** + * Creates a row that can remove its visit. + * + * @param desk the desk controller + */ + @Inject + public VisitRow(DeskController desk) { + this.desk = desk; + } + + /** + * Returns the model property the table sets. + * + * @return the model property + */ + public ObjectProperty modelProperty() { + return model; + } + + /** + * Returns the visit this row edits. + * + * @return the visit, or {@code null} when none is set + */ + public Object getModel() { + return model.get(); + } + + /** + * Sets the visit this row edits. + * + * @param model the visit + */ + public void setModel(Object model) { + this.model.set(model); + } + + /** + * Removes this visit from the person. + */ + @FXML + public void remove() { + if (getModel() instanceof Address address) { + desk.removeVisit(address); + } + } +} diff --git a/fx-example/src/main/java/de/neitzel/fx/example/view/VisitedOnConverter.java b/fx-example/src/main/java/de/neitzel/fx/example/view/VisitedOnConverter.java new file mode 100644 index 0000000..53b8b6b --- /dev/null +++ b/fx-example/src/main/java/de/neitzel/fx/example/view/VisitedOnConverter.java @@ -0,0 +1,54 @@ +package de.neitzel.fx.example.view; + +import de.neitzel.fx.view.Converter; +import jakarta.inject.Singleton; + +import java.time.LocalDate; +import java.time.format.DateTimeParseException; + +/** + * Translates a visit date between a {@link LocalDate} and the ISO text in the table row. + */ +@Singleton +public class VisitedOnConverter implements Converter { + + /** + * Creates the converter. The container calls this constructor. + */ + public VisitedOnConverter() { + } + + /** + * Writes the date as an ISO local date, or an empty string when the date is missing. + * + * @param attributeValue the visit date + * @return the text shown in the field + */ + @Override + public Object toControlValue(Object attributeValue) { + if (attributeValue == null) { + return ""; + } + return attributeValue.toString(); + } + + /** + * Reads an ISO local date. An empty field clears the date. + * + * @param controlValue the field text + * @return the date, or {@code null} when the field is empty + * @throws IllegalArgumentException when the text is not an ISO local date + */ + @Override + public Object toAttributeValue(Object controlValue) { + String text = controlValue == null ? "" : controlValue.toString().trim(); + if (text.isEmpty()) { + return null; + } + try { + return LocalDate.parse(text); + } catch (DateTimeParseException failure) { + throw new IllegalArgumentException("Visited on must be a date: " + text); + } + } +} diff --git a/fx-example/src/main/resources/de/neitzel/fx/example/view/AddressForm.fxml b/fx-example/src/main/resources/de/neitzel/fx/example/view/AddressForm.fxml index 75176bf..76862bf 100644 --- a/fx-example/src/main/resources/de/neitzel/fx/example/view/AddressForm.fxml +++ b/fx-example/src/main/resources/de/neitzel/fx/example/view/AddressForm.fxml @@ -4,6 +4,7 @@ + @@ -14,12 +15,17 @@ - + + +