net.officefloor.eclipse.configurer.internal.AbstractBuilder Maven / Gradle / Ivy
/*
* OfficeFloor - http://www.officefloor.net
* Copyright (C) 2005-2018 Daniel Sagenschneider
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
package net.officefloor.eclipse.configurer.internal;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Function;
import javafx.beans.InvalidationListener;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.util.Callback;
import net.officefloor.eclipse.configurer.Actioner;
import net.officefloor.eclipse.configurer.Builder;
import net.officefloor.eclipse.configurer.DefaultImages;
import net.officefloor.eclipse.configurer.ErrorListener;
import net.officefloor.eclipse.configurer.ValueLoader;
import net.officefloor.eclipse.configurer.ValueValidator;
import net.officefloor.eclipse.configurer.ValueValidator.ValueValidatorContext;
/**
* Abstract {@link ValueRenderer}.
*
* @author Daniel Sagenschneider
*/
public abstract class AbstractBuilder>
implements Builder, ValueRendererFactory, ColumnRenderer {
/**
* Label.
*/
private final String label;
/**
* {@link ValueLoader}.
*/
private ValueLoader valueLoader = null;
/**
* {@link Function} to obtain the initial value from the model.
*/
private Function getInitialValue = null;
/**
* {@link ValueValidator} instances.
*/
private List> validators = new LinkedList<>();
/**
* Instantiate.
*
* @param label
* Label.
*/
public AbstractBuilder(String label) {
this.label = label;
}
/**
* Creates the input {@link ValueInput} for the {@link ObservableValue}.
*
* @param context
* {@link ValueInputContext}.
* @return {@link ValueInput} to configure the {@link ObservableValue}.
*/
protected abstract I createInput(ValueInputContext context);
/**
* Creates the label {@link Node}.
*
* @param labelText
* Label text.
* @param valueInput
* {@link ValueInput}.
* @return Label {@link Node}.
*/
protected Node createLabel(String labelText, I valueInput) {
return new Label(this.getLabel());
}
/**
* Creates the error feedback {@link Node}.
*
* @param valueInput
* {@link ValueInput}.
* @param errorProperty
* Error {@link Property}.
* @return Error feedback {@link Node}.
*/
protected Node createErrorFeedback(I valueInput, Property errorProperty) {
ImageView error = new ImageView(new Image(DefaultImages.ERROR_IMAGE_PATH, 15, 15, true, true));
Tooltip errorTooltip = new Tooltip();
errorTooltip.getStyleClass().add("error-tooltip");
Tooltip.install(error, errorTooltip);
InvalidationListener listener = (observableError) -> {
Throwable cause = errorProperty.getValue();
if (cause != null) {
// Display the error
errorTooltip.setText(cause.getMessage());
error.visibleProperty().set(true);
} else {
// No error
error.visibleProperty().set(false);
}
};
errorProperty.addListener(listener);
listener.invalidated(errorProperty); // Initialise
return error;
}
/**
* Obtains the error.
*
* @param valueInput
* {@link ValueInput}.
* @param error
* {@link Throwable} error. May be null if no error.
* @return {@link Throwable} error or null if no error.
*/
protected Throwable getError(I valueInput, ReadOnlyProperty error) {
return error.getValue();
}
/**
* Creates the {@link Property} for the {@link TableCell}.
*
* @return {@link Property} for the {@link TableCell}.
*/
protected Property createCellProperty() {
return new SimpleObjectProperty<>();
}
/**
* Allow overriding to configure the {@link TableColumn}.
*
* @param
* Row object type.
* @param table
* {@link TableView} that will contain the {@link TableColumn}.
* @param column
* {@link TableColumn}.
* @param callback
* {@link Callback}.
*/
protected void configureTableColumn(TableView table, TableColumn column,
Callback> callback) {
}
/**
* Obtains the label.
*
* @return Label.
*/
protected String getLabel() {
return this.label == null ? "" : this.label;
}
/*
* ============ Builder ===============
*/
@Override
@SuppressWarnings("unchecked")
public B init(Function getInitialValue) {
this.getInitialValue = getInitialValue;
return (B) this;
}
@Override
@SuppressWarnings("unchecked")
public B validate(ValueValidator validator) {
this.validators.add(validator);
return (B) this;
}
@Override
@SuppressWarnings("unchecked")
public B setValue(ValueLoader valueLoader) {
this.valueLoader = valueLoader;
return (B) this;
}
/*
* ============ ValueRendererFactory =================
*/
@Override
public ValueRenderer createValueRenderer(ValueRendererContext context) {
return new ValueRendererImpl(context);
}
/**
* {@link ValueRenderer} implementation.
*/
private class ValueRendererImpl
implements ValueRenderer, ValueValidatorContext, ValueInputContext {
/**
* {@link ObservableValue}.
*/
private final Property value = new SimpleObjectProperty<>();
/**
* {@link ValueRendererContext}.
*/
private final ValueRendererContext context;
/**
* {@link ValueValidator} instances.
*/
private final List> validators = new ArrayList<>(1);
/**
* Tracks if an error occurred on validation.
*/
private boolean isError = false;
/**
* Error.
*/
private final Property error = new SimpleObjectProperty<>();
/**
* Initialises the {@link ValueRenderer}.
*/
private ValueRendererImpl(ValueRendererContext context) {
this.context = context;
// Obtain the model
M model = this.context.getModel();
// Initialise to model
this.loadValue();
// Refresh on error or value change
this.error.addListener((event) -> this.context.refreshError());
this.value.addListener((event) -> this.context.refreshError());
// Load value to model (so model consistent before validation)
if (AbstractBuilder.this.valueLoader != null) {
this.value.addListener(
(event) -> AbstractBuilder.this.valueLoader.loadValue(model, this.value.getValue()));
}
// Undertake validation (after loading value to model)
this.validators.addAll(AbstractBuilder.this.validators);
this.value.addListener((event) -> this.validate());
this.validate(); // validate initial value
// Track model becoming dirty
this.value.addListener((event) -> context.dirtyProperty().setValue(true));
}
/**
* Loads the value.
*/
private void loadValue() {
if (AbstractBuilder.this.getInitialValue != null) {
V initialValue = AbstractBuilder.this.getInitialValue.apply(this.context.getModel());
this.value.setValue(initialValue);
}
}
/**
* Undertakes validation.
*/
private void validate() {
// Track whether error in validation
this.isError = false;
try {
// Undertake validation
Iterator> iterator = this.validators.iterator();
while ((!this.isError) && (iterator.hasNext())) {
ValueValidator validator = iterator.next();
validator.validate(this);
}
} catch (Throwable ex) {
// Provide error message
String message = ex.getMessage();
if ((message == null) || (message.length() == 0)) {
// Provide message from exception
ex = new Exception("Thrown exception " + ex.getClass().getName(), ex);
}
// Flag error and override with exception
this.isError = true;
this.error.setValue(ex);
}
if (!this.isError) {
// No error in validate, so clear error
this.setError(null);
}
}
/*
* ============ ValueInputContext ================
*/
@Override
public M getModel() {
return this.context.getModel();
}
@Override
public Property getInputValue() {
return this.value;
}
@Override
public void addValidator(ValueValidator validator) {
this.validators.add(validator);
// Run validation immediately
this.validate();
}
@Override
public void refreshError() {
this.context.refreshError();
}
@Override
public Actioner getOptionalActioner() {
return this.context.getOptionalActioner();
}
@Override
public Property dirtyProperty() {
return this.context.dirtyProperty();
}
@Override
public Property validProperty() {
return this.context.validProperty();
}
@Override
public ErrorListener getErrorListener() {
return this.context.getErrorListener();
}
/*
* =============== ValueRenderer =================
*/
@Override
public I createInput() {
return AbstractBuilder.this.createInput(this);
}
@Override
public String getLabel(I valueInput) {
return AbstractBuilder.this.getLabel();
}
@Override
public Node createLabel(String labelText, I valueInput) {
return AbstractBuilder.this.createLabel(labelText, valueInput);
}
@Override
public Node createErrorFeedback(I valueInput) {
return AbstractBuilder.this.createErrorFeedback(valueInput, this.error);
}
@Override
public Throwable getError(I valueInput) {
return AbstractBuilder.this.getError(valueInput, this.error);
}
/*
* ========== ValueValidatorContext =============
*/
@Override
public ReadOnlyProperty getValue() {
return this.value;
}
@Override
public void setError(String message) {
// Blank string considered no exception
if ((message != null) && (message.length() == 0)) {
message = null;
}
// Update the error feedback
this.isError = (message != null);
this.error.setValue(this.isError ? new MessageOnlyException(message) : null);
}
@Override
public void reload(Builder, ?, ?> builder) {
this.context.reload(builder);
}
@Override
public void reloadIf(Builder, ?, ?> builder) {
if (AbstractBuilder.this == builder) {
// Require reloading this value
this.loadValue();
}
}
}
/*
* ============ ColumnRenderer =================
*/
@Override
public TableColumn createTableColumn(TableView table, Callback> callback) {
TableColumn column = new TableColumn<>(this.label);
this.configureTableColumn(table, column, callback);
return column;
}
@Override
public boolean isEditable() {
return (this.valueLoader != null);
}
@Override
public CellRenderer createCellRenderer(ValueRendererContext context) {
return new CellRendererImpl(context);
}
/**
* {@link CellRenderer} implementation.
*/
private class CellRendererImpl implements CellRenderer {
/**
* {@link ObservableValue}.
*/
private final Property value;
/**
* {@link ValueRendererContext}.
*/
private final ValueRendererContext context;
/**
* Instantiate.
*
* @param context
* {@link ValueRendererContext}.
*/
private CellRendererImpl(ValueRendererContext context) {
this.context = context;
// Obtain the cell value property
this.value = AbstractBuilder.this.createCellProperty();
// Load initial value
if ((AbstractBuilder.this.getInitialValue != null) && (this.context.getModel() != null)) {
V value = AbstractBuilder.this.getInitialValue.apply(this.context.getModel());
this.value.setValue(value);
}
// Always load value to model
if (AbstractBuilder.this.valueLoader != null) {
this.value.addListener((event) -> AbstractBuilder.this.valueLoader.loadValue(this.context.getModel(),
this.value.getValue()));
}
}
/*
* ============ CellRenderer ====================
*/
@Override
public Property getValue() {
return this.value;
}
}
} © 2015 - 2025 Weber Informatics LLC | Privacy Policy