Add Transaction Properties #15
|
@ -91,6 +91,8 @@ public class PerfinApp extends Application {
|
||||||
router.map("edit-transaction", PerfinApp.class.getResource("/edit-transaction.fxml"));
|
router.map("edit-transaction", PerfinApp.class.getResource("/edit-transaction.fxml"));
|
||||||
router.map("create-balance-record", PerfinApp.class.getResource("/create-balance-record.fxml"));
|
router.map("create-balance-record", PerfinApp.class.getResource("/create-balance-record.fxml"));
|
||||||
router.map("balance-record", PerfinApp.class.getResource("/balance-record-view.fxml"));
|
router.map("balance-record", PerfinApp.class.getResource("/balance-record-view.fxml"));
|
||||||
|
router.map("vendors", PerfinApp.class.getResource("/vendors-view.fxml"));
|
||||||
|
router.map("edit-vendor", PerfinApp.class.getResource("/edit-vendor.fxml"));
|
||||||
|
|
||||||
// Help pages.
|
// Help pages.
|
||||||
helpRouter.map("home", PerfinApp.class.getResource("/help-pages/home.fxml"));
|
helpRouter.map("home", PerfinApp.class.getResource("/help-pages/home.fxml"));
|
||||||
|
|
|
@ -11,6 +11,7 @@ import com.andrewlalis.perfin.model.Profile;
|
||||||
import com.andrewlalis.perfin.view.component.FileSelectionArea;
|
import com.andrewlalis.perfin.view.component.FileSelectionArea;
|
||||||
import com.andrewlalis.perfin.view.component.PropertiesPane;
|
import com.andrewlalis.perfin.view.component.PropertiesPane;
|
||||||
import com.andrewlalis.perfin.view.component.validation.ValidationApplier;
|
import com.andrewlalis.perfin.view.component.validation.ValidationApplier;
|
||||||
|
import com.andrewlalis.perfin.view.component.validation.ValidationFunction;
|
||||||
import com.andrewlalis.perfin.view.component.validation.ValidationResult;
|
import com.andrewlalis.perfin.view.component.validation.ValidationResult;
|
||||||
import com.andrewlalis.perfin.view.component.validation.validators.CurrencyAmountValidator;
|
import com.andrewlalis.perfin.view.component.validation.validators.CurrencyAmountValidator;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
@ -39,7 +40,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
|
||||||
private Account account;
|
private Account account;
|
||||||
|
|
||||||
@FXML public void initialize() {
|
@FXML public void initialize() {
|
||||||
var timestampValid = new ValidationApplier<String>(input -> {
|
var timestampValid = new ValidationApplier<>((ValidationFunction<String>) input -> {
|
||||||
try {
|
try {
|
||||||
DateUtil.DEFAULT_DATETIME_FORMAT.parse(input);
|
DateUtil.DEFAULT_DATETIME_FORMAT.parse(input);
|
||||||
return ValidationResult.valid();
|
return ValidationResult.valid();
|
||||||
|
|
|
@ -60,6 +60,7 @@ public class EditTransactionController implements RouteSelectionListener {
|
||||||
@FXML public AccountSelectionBox creditAccountSelector;
|
@FXML public AccountSelectionBox creditAccountSelector;
|
||||||
|
|
||||||
@FXML public ComboBox<String> vendorComboBox;
|
@FXML public ComboBox<String> vendorComboBox;
|
||||||
|
@FXML public Hyperlink vendorsHyperlink;
|
||||||
@FXML public ComboBox<String> categoryComboBox;
|
@FXML public ComboBox<String> categoryComboBox;
|
||||||
@FXML public ComboBox<String> tagsComboBox;
|
@FXML public ComboBox<String> tagsComboBox;
|
||||||
@FXML public Button addTagButton;
|
@FXML public Button addTagButton;
|
||||||
|
@ -89,6 +90,8 @@ public class EditTransactionController implements RouteSelectionListener {
|
||||||
).validatedInitially().attach(descriptionField, descriptionField.textProperty());
|
).validatedInitially().attach(descriptionField, descriptionField.textProperty());
|
||||||
var linkedAccountsValid = initializeLinkedAccountsValidationUi();
|
var linkedAccountsValid = initializeLinkedAccountsValidationUi();
|
||||||
initializeTagSelectionUi();
|
initializeTagSelectionUi();
|
||||||
|
// Setup hyperlinks.
|
||||||
|
vendorsHyperlink.setOnAction(event -> router.navigate("vendors"));
|
||||||
|
|
||||||
var formValid = timestampValid.and(amountValid).and(descriptionValid).and(linkedAccountsValid);
|
var formValid = timestampValid.and(amountValid).and(descriptionValid).and(linkedAccountsValid);
|
||||||
saveButton.disableProperty().bind(formValid.not());
|
saveButton.disableProperty().bind(formValid.not());
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
package com.andrewlalis.perfin.control;
|
||||||
|
|
||||||
|
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
|
||||||
|
import com.andrewlalis.perfin.data.DataSource;
|
||||||
|
import com.andrewlalis.perfin.data.TransactionVendorRepository;
|
||||||
|
import com.andrewlalis.perfin.model.Profile;
|
||||||
|
import com.andrewlalis.perfin.model.TransactionVendor;
|
||||||
|
import com.andrewlalis.perfin.view.component.validation.ValidationApplier;
|
||||||
|
import com.andrewlalis.perfin.view.component.validation.validators.PredicateValidator;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.TextArea;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
import static com.andrewlalis.perfin.PerfinApp.router;
|
||||||
|
|
||||||
|
public class EditVendorController implements RouteSelectionListener {
|
||||||
|
private TransactionVendor vendor;
|
||||||
|
|
||||||
|
@FXML public TextField nameField;
|
||||||
|
@FXML public TextArea descriptionField;
|
||||||
|
@FXML public Button saveButton;
|
||||||
|
|
||||||
|
@FXML public void initialize() {
|
||||||
|
var nameValid = new ValidationApplier<>(new PredicateValidator<String>()
|
||||||
|
.addTerminalPredicate(s -> s != null && !s.isBlank(), "Name is required.")
|
||||||
|
.addPredicate(s -> s.strip().length() <= TransactionVendor.NAME_MAX_LENGTH, "Name is too long.")
|
||||||
|
// A predicate that prevents duplicate names.
|
||||||
|
.addAsyncPredicate(
|
||||||
|
s -> {
|
||||||
|
if (Profile.getCurrent() == null) return CompletableFuture.completedFuture(false);
|
||||||
|
return Profile.getCurrent().dataSource().mapRepoAsync(
|
||||||
|
TransactionVendorRepository.class,
|
||||||
|
repo -> {
|
||||||
|
var vendorByName = repo.findByName(s).orElse(null);
|
||||||
|
if (this.vendor != null) {
|
||||||
|
return this.vendor.equals(vendorByName) || vendorByName == null;
|
||||||
|
}
|
||||||
|
return vendorByName == null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
"Vendor with this name already exists."
|
||||||
|
)
|
||||||
|
).validatedInitially().attachToTextField(nameField);
|
||||||
|
var descriptionValid = new ValidationApplier<>(new PredicateValidator<String>()
|
||||||
|
.addPredicate(
|
||||||
|
s -> s == null || s.strip().length() <= TransactionVendor.DESCRIPTION_MAX_LENGTH,
|
||||||
|
"Description is too long."
|
||||||
|
)
|
||||||
|
).validatedInitially().attach(descriptionField, descriptionField.textProperty());
|
||||||
|
|
||||||
|
var formValid = nameValid.and(descriptionValid);
|
||||||
|
saveButton.disableProperty().bind(formValid.not());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRouteSelected(Object context) {
|
||||||
|
if (context instanceof TransactionVendor tv) {
|
||||||
|
this.vendor = tv;
|
||||||
|
nameField.setText(vendor.getName());
|
||||||
|
descriptionField.setText(vendor.getDescription());
|
||||||
|
} else {
|
||||||
|
nameField.setText(null);
|
||||||
|
descriptionField.setText(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML public void save() {
|
||||||
|
String name = nameField.getText().strip();
|
||||||
|
String description = descriptionField.getText() == null ? null : descriptionField.getText().strip();
|
||||||
|
DataSource ds = Profile.getCurrent().dataSource();
|
||||||
|
if (vendor != null) {
|
||||||
|
ds.useRepo(TransactionVendorRepository.class, repo -> repo.update(vendor.id, name, description));
|
||||||
|
} else {
|
||||||
|
ds.useRepo(TransactionVendorRepository.class, repo -> {
|
||||||
|
if (description == null || description.isEmpty()) {
|
||||||
|
repo.insert(name);
|
||||||
|
} else {
|
||||||
|
repo.insert(name, description);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
router.replace("vendors");
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML public void cancel() {
|
||||||
|
router.navigateBackAndClear();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package com.andrewlalis.perfin.control;
|
||||||
|
|
||||||
|
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
|
||||||
|
import com.andrewlalis.perfin.data.TransactionVendorRepository;
|
||||||
|
import com.andrewlalis.perfin.model.Profile;
|
||||||
|
import com.andrewlalis.perfin.model.TransactionVendor;
|
||||||
|
import com.andrewlalis.perfin.view.BindingUtil;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.layout.BorderPane;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static com.andrewlalis.perfin.PerfinApp.router;
|
||||||
|
|
||||||
|
public class VendorsViewController implements RouteSelectionListener {
|
||||||
|
@FXML public VBox vendorsVBox;
|
||||||
|
private final ObservableList<TransactionVendor> vendors = FXCollections.observableArrayList();
|
||||||
|
|
||||||
|
@FXML public void initialize() {
|
||||||
|
BindingUtil.mapContent(vendorsVBox.getChildren(), vendors, this::buildVendorTile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Node buildVendorTile(TransactionVendor transactionVendor) {
|
||||||
|
BorderPane pane = new BorderPane();
|
||||||
|
pane.getStyleClass().addAll("tile", "std-spacing");
|
||||||
|
pane.setOnMouseClicked(event -> router.navigate("edit-vendor", transactionVendor));
|
||||||
|
|
||||||
|
Label nameLabel = new Label(transactionVendor.getName());
|
||||||
|
nameLabel.getStyleClass().addAll("bold-text");
|
||||||
|
Label descriptionLabel = new Label(transactionVendor.getDescription());
|
||||||
|
descriptionLabel.setWrapText(true);
|
||||||
|
VBox contentVBox = new VBox(nameLabel, descriptionLabel);
|
||||||
|
contentVBox.getStyleClass().addAll("std-spacing");
|
||||||
|
pane.setCenter(contentVBox);
|
||||||
|
BorderPane.setAlignment(contentVBox, Pos.TOP_LEFT);
|
||||||
|
|
||||||
|
Button removeButton = new Button("Remove");
|
||||||
|
removeButton.setOnAction(event -> {
|
||||||
|
boolean confirm = Popups.confirm(removeButton, "Are you sure you want to remove this vendor? Any transactions with assigned to this vendor will have their vendor field cleared. This cannot be undone.");
|
||||||
|
if (confirm) {
|
||||||
|
Profile.getCurrent().dataSource().useRepo(TransactionVendorRepository.class, repo -> {
|
||||||
|
repo.deleteById(transactionVendor.id);
|
||||||
|
});
|
||||||
|
refreshVendors();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pane.setRight(removeButton);
|
||||||
|
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRouteSelected(Object context) {
|
||||||
|
refreshVendors();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML public void addVendor() {
|
||||||
|
router.navigate("edit-vendor");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshVendors() {
|
||||||
|
Profile.getCurrent().dataSource().useRepoAsync(TransactionVendorRepository.class, repo -> {
|
||||||
|
final List<TransactionVendor> vendors = repo.findAll();
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
this.vendors.clear();
|
||||||
|
this.vendors.addAll(vendors);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,5 +11,6 @@ public interface TransactionVendorRepository extends Repository, AutoCloseable {
|
||||||
List<TransactionVendor> findAll();
|
List<TransactionVendor> findAll();
|
||||||
long insert(String name, String description);
|
long insert(String name, String description);
|
||||||
long insert(String name);
|
long insert(String name);
|
||||||
|
void update(long id, String name, String description);
|
||||||
void deleteById(long id);
|
void deleteById(long id);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import java.sql.Connection;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public record JdbcTransactionVendorRepository(Connection conn) implements TransactionVendorRepository {
|
public record JdbcTransactionVendorRepository(Connection conn) implements TransactionVendorRepository {
|
||||||
|
@ -58,6 +59,29 @@ public record JdbcTransactionVendorRepository(Connection conn) implements Transa
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(long id, String name, String description) {
|
||||||
|
DbUtil.doTransaction(conn, () -> {
|
||||||
|
TransactionVendor vendor = findById(id).orElseThrow();
|
||||||
|
if (!vendor.getName().equals(name)) {
|
||||||
|
DbUtil.updateOne(
|
||||||
|
conn,
|
||||||
|
"UPDATE transaction_vendor SET name = ? WHERE id = ?",
|
||||||
|
name,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!Objects.equals(vendor.getDescription(), description)) {
|
||||||
|
DbUtil.updateOne(
|
||||||
|
conn,
|
||||||
|
"UPDATE transaction_vendor SET description = ? WHERE id = ?",
|
||||||
|
description,
|
||||||
|
id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void deleteById(long id) {
|
public void deleteById(long id) {
|
||||||
DbUtil.update(conn, "DELETE FROM transaction_vendor WHERE id = ?", List.of(id));
|
DbUtil.update(conn, "DELETE FROM transaction_vendor WHERE id = ?", List.of(id));
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
package com.andrewlalis.perfin.view.component.validation;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public interface AsyncValidationFunction<T> {
|
||||||
|
CompletableFuture<ValidationResult> validate(T input);
|
||||||
|
}
|
|
@ -1,24 +1,40 @@
|
||||||
package com.andrewlalis.perfin.view.component.validation;
|
package com.andrewlalis.perfin.view.component.validation;
|
||||||
|
|
||||||
import com.andrewlalis.perfin.view.component.validation.decorators.FieldSubtextDecorator;
|
import com.andrewlalis.perfin.view.component.validation.decorators.FieldSubtextDecorator;
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.beans.binding.BooleanExpression;
|
import javafx.beans.binding.BooleanExpression;
|
||||||
import javafx.beans.property.Property;
|
import javafx.beans.property.Property;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fluent interface for applying a validator to one or more controls.
|
* Fluent interface for applying a validator to one or more controls.
|
||||||
* @param <T> The value type.
|
* @param <T> The value type.
|
||||||
*/
|
*/
|
||||||
public class ValidationApplier<T> {
|
public class ValidationApplier<T> {
|
||||||
private final ValidationFunction<T> validator;
|
private final AsyncValidationFunction<T> validator;
|
||||||
private ValidationDecorator decorator = new FieldSubtextDecorator();
|
private ValidationDecorator decorator = new FieldSubtextDecorator();
|
||||||
private boolean validateInitially = false;
|
private boolean validateInitially = false;
|
||||||
|
|
||||||
public ValidationApplier(ValidationFunction<T> validator) {
|
public ValidationApplier(ValidationFunction<T> validator) {
|
||||||
|
this.validator = input -> CompletableFuture.completedFuture(validator.validate(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValidationApplier(AsyncValidationFunction<T> validator) {
|
||||||
this.validator = validator;
|
this.validator = validator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <T> ValidationApplier<T> of(ValidationFunction<T> validator) {
|
||||||
|
return new ValidationApplier<>(validator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ValidationApplier<T> ofAsync(AsyncValidationFunction<T> validator) {
|
||||||
|
return new ValidationApplier<>(validator);
|
||||||
|
}
|
||||||
|
|
||||||
public ValidationApplier<T> decoratedWith(ValidationDecorator decorator) {
|
public ValidationApplier<T> decoratedWith(ValidationDecorator decorator) {
|
||||||
this.decorator = decorator;
|
this.decorator = decorator;
|
||||||
return this;
|
return this;
|
||||||
|
@ -29,24 +45,47 @@ public class ValidationApplier<T> {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches the configured validator and decorator to a node, so that when
|
||||||
|
* the node's specified valueProperty changes, the validator will be called
|
||||||
|
* and if the new value is invalid, the decorator will update the UI to
|
||||||
|
* show the message(s) to the user.
|
||||||
|
* @param node The node to attach to.
|
||||||
|
* @param valueProperty The property to listen for changes and validate on.
|
||||||
|
* @param triggerProperties Additional properties that, when changed, can
|
||||||
|
* trigger validation.
|
||||||
|
* @return A boolean expression that tells whether the given valueProperty
|
||||||
|
* is valid at any given time.
|
||||||
|
*/
|
||||||
public BooleanExpression attach(Node node, Property<T> valueProperty, Property<?>... triggerProperties) {
|
public BooleanExpression attach(Node node, Property<T> valueProperty, Property<?>... triggerProperties) {
|
||||||
BooleanExpression validProperty = BooleanExpression.booleanExpression(
|
final SimpleBooleanProperty validProperty = new SimpleBooleanProperty();
|
||||||
valueProperty.map(value -> validator.validate(value).isValid())
|
|
||||||
);
|
|
||||||
valueProperty.addListener((observable, oldValue, newValue) -> {
|
valueProperty.addListener((observable, oldValue, newValue) -> {
|
||||||
ValidationResult result = validator.validate(newValue);
|
validProperty.set(false); // Always set valid to false before we start validation.
|
||||||
|
validator.validate(newValue)
|
||||||
|
.thenAccept(result -> Platform.runLater(() -> {
|
||||||
|
validProperty.set(result.isValid());
|
||||||
decorator.decorate(node, result);
|
decorator.decorate(node, result);
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
for (Property<?> influencingProperty : triggerProperties) {
|
for (Property<?> influencingProperty : triggerProperties) {
|
||||||
influencingProperty.addListener((observable, oldValue, newValue) -> {
|
influencingProperty.addListener((observable, oldValue, newValue) -> {
|
||||||
ValidationResult result = validator.validate(valueProperty.getValue());
|
validProperty.set(false); // Always set valid to false before we start validation.
|
||||||
|
validator.validate(valueProperty.getValue())
|
||||||
|
.thenAccept(result -> Platform.runLater(() -> {
|
||||||
|
validProperty.set(result.isValid());
|
||||||
decorator.decorate(node, result);
|
decorator.decorate(node, result);
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validateInitially) {
|
if (validateInitially) {
|
||||||
// Call the decorator once to perform validation right away.
|
// Call the decorator once to perform validation right away.
|
||||||
decorator.decorate(node, validator.validate(valueProperty.getValue()));
|
validProperty.set(false); // Always set valid to false before we start validation.
|
||||||
|
validator.validate(valueProperty.getValue())
|
||||||
|
.thenAccept(result -> Platform.runLater(() -> {
|
||||||
|
validProperty.set(result.isValid());
|
||||||
|
decorator.decorate(node, result);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
return validProperty;
|
return validProperty;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
package com.andrewlalis.perfin.view.component.validation.validators;
|
package com.andrewlalis.perfin.view.component.validation.validators;
|
||||||
|
|
||||||
import com.andrewlalis.perfin.view.component.validation.ValidationFunction;
|
import com.andrewlalis.perfin.view.component.validation.AsyncValidationFunction;
|
||||||
import com.andrewlalis.perfin.view.component.validation.ValidationResult;
|
import com.andrewlalis.perfin.view.component.validation.ValidationResult;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -12,32 +16,73 @@ import java.util.function.Function;
|
||||||
* determine if it's valid. If invalid, a message is added.
|
* determine if it's valid. If invalid, a message is added.
|
||||||
* @param <T> The value type.
|
* @param <T> The value type.
|
||||||
*/
|
*/
|
||||||
public class PredicateValidator<T> implements ValidationFunction<T> {
|
public class PredicateValidator<T> implements AsyncValidationFunction<T> {
|
||||||
private record ValidationStep<T>(Function<T, Boolean> predicate, String message, boolean terminal) {}
|
private static final Logger logger = LoggerFactory.getLogger(PredicateValidator.class);
|
||||||
|
|
||||||
|
private record ValidationStep<T>(Function<T, CompletableFuture<Boolean>> predicate, String message, boolean terminal) {}
|
||||||
|
|
||||||
private final List<ValidationStep<T>> steps = new ArrayList<>();
|
private final List<ValidationStep<T>> steps = new ArrayList<>();
|
||||||
|
|
||||||
public PredicateValidator<T> addPredicate(Function<T, Boolean> predicate, String errorMessage) {
|
private PredicateValidator<T> addPredicate(Function<T, Boolean> predicate, String errorMessage, boolean terminal) {
|
||||||
steps.add(new ValidationStep<>(predicate, errorMessage, false));
|
steps.add(new ValidationStep<>(
|
||||||
|
v -> CompletableFuture.completedFuture(predicate.apply(v)),
|
||||||
|
errorMessage,
|
||||||
|
terminal
|
||||||
|
));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PredicateValidator<T> addTerminalPredicate(Function<T, Boolean> predicate, String errorMessage) {
|
private PredicateValidator<T> addAsyncPredicate(Function<T, CompletableFuture<Boolean>> asyncPredicate, String errorMessage, boolean terminal) {
|
||||||
steps.add(new ValidationStep<>(predicate, errorMessage, true));
|
steps.add(new ValidationStep<>(asyncPredicate, errorMessage, terminal));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PredicateValidator<T> addPredicate(Function<T, Boolean> predicate, String errorMessage) {
|
||||||
|
return addPredicate(predicate, errorMessage, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PredicateValidator<T> addAsyncPredicate(Function<T, CompletableFuture<Boolean>> asyncPredicate, String errorMessage) {
|
||||||
|
return addAsyncPredicate(asyncPredicate, errorMessage, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a terminal predicate, that is, if the given boolean function
|
||||||
|
* evaluates to false, then no further predicates are evaluated.
|
||||||
|
* @param predicate The predicate function.
|
||||||
|
* @param errorMessage The error message to display if the predicate
|
||||||
|
* evaluates to false for a given value.
|
||||||
|
* @return A reference to the validator, for method chaining.
|
||||||
|
*/
|
||||||
|
public PredicateValidator<T> addTerminalPredicate(Function<T, Boolean> predicate, String errorMessage) {
|
||||||
|
return addPredicate(predicate, errorMessage, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PredicateValidator<T> addTerminalAsyncPredicate(Function<T, CompletableFuture<Boolean>> asyncPredicate, String errorMessage) {
|
||||||
|
return addAsyncPredicate(asyncPredicate, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ValidationResult validate(T input) {
|
public CompletableFuture<ValidationResult> validate(T input) {
|
||||||
|
CompletableFuture<ValidationResult> cf = new CompletableFuture<>();
|
||||||
|
Thread.ofVirtual().start(() -> {
|
||||||
List<String> messages = new ArrayList<>();
|
List<String> messages = new ArrayList<>();
|
||||||
for (var step : steps) {
|
for (var step : steps) {
|
||||||
if (!step.predicate().apply(input)) {
|
try {
|
||||||
|
boolean success = step.predicate().apply(input).get();
|
||||||
|
if (!success) {
|
||||||
messages.add(step.message());
|
messages.add(step.message());
|
||||||
if (step.terminal()) {
|
if (step.terminal()) {
|
||||||
return new ValidationResult(messages);
|
cf.complete(new ValidationResult(messages));
|
||||||
|
return; // Exit if this is a terminal step and it failed.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
logger.error("Applying a predicate to input failed.", e);
|
||||||
|
cf.completeExceptionally(e);
|
||||||
}
|
}
|
||||||
return new ValidationResult(messages);
|
}
|
||||||
|
cf.complete(new ValidationResult(messages));
|
||||||
|
});
|
||||||
|
return cf;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
|
|
||||||
<VBox>
|
<VBox>
|
||||||
<Label text="Vendor" labelFor="${vendorComboBox}" styleClass="bold-text"/>
|
<Label text="Vendor" labelFor="${vendorComboBox}" styleClass="bold-text"/>
|
||||||
<Hyperlink text="Manage vendors" styleClass="small-font"/>
|
<Hyperlink fx:id="vendorsHyperlink" text="Manage vendors" styleClass="small-font"/>
|
||||||
</VBox>
|
</VBox>
|
||||||
<ComboBox fx:id="vendorComboBox" editable="true" maxWidth="Infinity"/>
|
<ComboBox fx:id="vendorComboBox" editable="true" maxWidth="Infinity"/>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import com.andrewlalis.perfin.view.component.PropertiesPane?>
|
||||||
|
<?import javafx.scene.control.*?>
|
||||||
|
<?import javafx.scene.layout.*?>
|
||||||
|
<BorderPane xmlns="http://javafx.com/javafx"
|
||||||
|
xmlns:fx="http://javafx.com/fxml"
|
||||||
|
fx:controller="com.andrewlalis.perfin.control.EditVendorController"
|
||||||
|
>
|
||||||
|
<top>
|
||||||
|
<Label text="Edit Vendor" styleClass="bold-text,large-font,std-padding"/>
|
||||||
|
</top>
|
||||||
|
<center>
|
||||||
|
<VBox>
|
||||||
|
<PropertiesPane hgap="5" vgap="5" styleClass="std-padding" maxWidth="500">
|
||||||
|
<columnConstraints>
|
||||||
|
<ColumnConstraints hgrow="NEVER" halignment="LEFT" minWidth="150"/>
|
||||||
|
<ColumnConstraints hgrow="ALWAYS" halignment="RIGHT"/>
|
||||||
|
</columnConstraints>
|
||||||
|
|
||||||
|
<Label text="Name" labelFor="${nameField}"/>
|
||||||
|
<TextField fx:id="nameField"/>
|
||||||
|
|
||||||
|
<Label text="Description" labelFor="${descriptionField}"/>
|
||||||
|
<TextArea fx:id="descriptionField" wrapText="true"/>
|
||||||
|
</PropertiesPane>
|
||||||
|
<HBox styleClass="std-padding,std-spacing">
|
||||||
|
<Button text="Save" fx:id="saveButton" onAction="#save"/>
|
||||||
|
<Button text="Cancel" onAction="#cancel"/>
|
||||||
|
</HBox>
|
||||||
|
</VBox>
|
||||||
|
</center>
|
||||||
|
</BorderPane>
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
|
||||||
|
<?import javafx.scene.control.Button?>
|
||||||
|
<?import javafx.scene.control.Label?>
|
||||||
|
<?import javafx.scene.control.ScrollPane?>
|
||||||
|
<?import javafx.scene.layout.BorderPane?>
|
||||||
|
<?import javafx.scene.layout.HBox?>
|
||||||
|
<?import javafx.scene.layout.VBox?>
|
||||||
|
<?import javafx.scene.text.Text?>
|
||||||
|
<BorderPane xmlns="http://javafx.com/javafx"
|
||||||
|
xmlns:fx="http://javafx.com/fxml"
|
||||||
|
fx:controller="com.andrewlalis.perfin.control.VendorsViewController"
|
||||||
|
>
|
||||||
|
<top>
|
||||||
|
<Label text="Vendors" styleClass="large-font,bold-text,std-padding"/>
|
||||||
|
</top>
|
||||||
|
<center>
|
||||||
|
<VBox>
|
||||||
|
<HBox styleClass="std-padding,std-spacing" VBox.vgrow="NEVER">
|
||||||
|
<Button text="Add Vendor" onAction="#addVendor"/>
|
||||||
|
</HBox>
|
||||||
|
<ScrollPane styleClass="tile-container-scroll" VBox.vgrow="ALWAYS">
|
||||||
|
<VBox fx:id="vendorsVBox" styleClass="tile-container"/>
|
||||||
|
</ScrollPane>
|
||||||
|
</VBox>
|
||||||
|
</center>
|
||||||
|
</BorderPane>
|
Loading…
Reference in New Issue