All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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