com.panemu.tiwulfx.table.BaseColumn.java Source code

Java tutorial

Introduction

Here is the source code for com.panemu.tiwulfx.table.BaseColumn.java

Source

/*
 * License GNU LGPL
 * Copyright (C) 2012 Amrullah <amrullah@panemu.com>.
 */
package com.panemu.tiwulfx.table;

import com.panemu.tiwulfx.common.TableCriteria;
import com.panemu.tiwulfx.common.TiwulFXUtil;
import com.panemu.tiwulfx.common.Validator;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.PopupControl;
import javafx.scene.control.Skin;
import javafx.scene.control.Skinnable;
import javafx.scene.control.TableColumn;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.util.Callback;
import javafx.util.StringConverter;
import org.apache.commons.beanutils.PropertyUtils;

/**
 * This is a parent class for columns that display value from a POJO object.
 * There are two variations of concrete column implementation in regards with
 * cell editor:
 * <ul>
 * <li>Cell editor is a single control: TextField, CheckBox</li>
 * <li>Cell editor is a composite of controls: ComboBox, DateField, LookupField.
 * They are composed from a TextField and a button
 * </ul>
 *
 * Please refer to {@link TextColumn} and {@link ComboBoxColumn} source code to
 * get reference on building your own column.
 *
 * Column that doesn't extend BaseColumn, i.e: {@link TickColumn} will be
 * skipped by export-to-excel and paste routine
 *
 * @author amrullah
 * @param <R> Record data type
 * @param <C> Column data type
 */
public class BaseColumn<R, C> extends TableColumn<R, C> {

    /**
     * used to get/set object method using introspection
     */
    private String propertyName;
    private final SimpleObjectProperty<TableCriteria> tableCriteria = new SimpleObjectProperty<>();
    private C searchValue;
    private Node filterImage = TiwulFXUtil.getGraphicFactory().createFilterGraphic();
    private Pos alignment = Pos.BASELINE_LEFT;
    private ObservableMap<R, String> mapInvalid = FXCollections.observableHashMap();
    private List<Validator<C>> lstValidator = new ArrayList<>();
    private String nullLabel = TiwulFXUtil.DEFAULT_NULL_LABEL;
    private Map<R, RecordChange<R, C>> mapChangedRecord = new HashMap<>();

    private StringConverter<C> stringConverter = new StringConverter<C>() {

        @Override
        public String toString(C t) {
            if (t == null) {
                return nullLabel;
            } else {
                return t.toString();
            }
        }

        @Override
        public C fromString(String string) {
            throw new UnsupportedOperationException(BaseColumn.class.getName()
                    + ". The implementation class of BaseColum should provide string converter by calling setStringConverter() in constructor.");
        }
    };

    TableCriteria<C> createSearchCriteria(TableCriteria.Operator operator, C value) {
        return new TableCriteria(propertyName, operator, value);
    }

    /**
     *
     * @param propertyName java bean property name to be used for get/set method
     * using introspection
     * @param prefWidth preferred column width
     */
    public BaseColumn(String propertyName, double prefWidth) {
        this(propertyName, prefWidth, TiwulFXUtil.getLiteral(propertyName));
    }

    /**
     *
     * @param propertyName java bean property name to be used for get/set method
     * using introspection
     * @param prefWidth preferred collumn width
     * @param columnHeader column header text. Default equals propertyName. This
     * text is localized
     */
    public BaseColumn(String propertyName, double prefWidth, String columnHeader) {
        super(columnHeader);
        setPrefWidth(prefWidth);
        this.propertyName = propertyName;
        //        setCellValueFactory(new PropertyValueFactory<S, T>(propertyName));
        tableCriteria.addListener(new InvalidationListener() {
            @Override
            public void invalidated(Observable observable) {
                if (tableCriteria.get() != null) {
                    BaseColumn.this.setGraphic(filterImage);
                } else {
                    BaseColumn.this.setGraphic(null);
                }
            }
        });
        setCellValueFactory(new Callback<CellDataFeatures<R, C>, ObservableValue<C>>() {
            private SimpleObjectProperty<C> propertyValue;

            @Override
            public ObservableValue<C> call(CellDataFeatures<R, C> param) {
                /**
                 * This code is adapted from {@link PropertyValueFactory#getCellDataReflectively(T)
                 */
                try {
                    Object cellValue;
                    if (getPropertyName().contains(".")) {
                        cellValue = PropertyUtils.getNestedProperty(param.getValue(), getPropertyName());
                    } else {
                        cellValue = PropertyUtils.getSimpleProperty(param.getValue(), getPropertyName());
                    }
                    propertyValue = new SimpleObjectProperty<>((C) cellValue);
                    return propertyValue;
                } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
                    throw new RuntimeException(ex);
                } catch (Exception ex) {
                    /**
                     * Ideally it catches
                     * org.apache.commons.beanutils.NestedNullException. However
                     * we need to import apachec bean utils library in FXML file
                     * to be able to display it in Scene Builder. So, I decided
                     * to catch Exception to avoid the import.
                     */
                    return new SimpleObjectProperty<>(null);
                }
            }
        });

    }

    public StringConverter<C> getStringConverter() {
        return this.stringConverter;
    }

    public void setStringConverter(StringConverter<C> stringConverter) {
        this.stringConverter = stringConverter;
    }

    /**
     * Property that holds applied criteria to column
     *
     * @return
     */
    public SimpleObjectProperty<TableCriteria> tableCriteriaProperty() {
        return tableCriteria;
    }

    public String getNullLabel() {
        return nullLabel;
    }

    public void setNullLabel(String nullLabel) {
        this.nullLabel = nullLabel;
    }

    /**
     * Get criteria applied to this column
     *
     * @return
     * @see #tableCriteriaProperty()
     */
    public TableCriteria getTableCriteria() {
        return tableCriteria.get();
    }

    /**
     * Set criteria to be applied to column. If you are going to set criteria to
     * multiple columns, it is encouraged to call {@link TableControl#setReloadOnCriteriaChange(boolean)
     * }
     * and pass FALSE as parameter. It will disable autoreload on criteria
     * change. After assign all criteria, call
     * {@link TableControl#reloadFirstPage()}. You might want to set {@link TableControl#setReloadOnCriteriaChange(boolean)
     * } back to true after that.
     *
     * @param crit
     * @see TableControl#setReloadOnCriteriaChange(boolean)
     */
    public void setTableCriteria(TableCriteria crit) {
        tableCriteria.set(crit);
    }

    /**
     * Gets propertyName passed in constructor
     *
     * @return
     */
    public String getPropertyName() {
        return propertyName;
    }

    public void setPropertyName(String propertyName) {
        this.propertyName = propertyName;
    }

    /**
     * Get Search Menu Item that is displayed in Table context menu
     *
     * @return
     */
    MenuItem getSearchMenuItem() {
        return null;
    }

    void setDefaultSearchValue(C searchValue) {
        this.searchValue = searchValue;
    }

    C getDefaultSearchValue() {
        return searchValue;
    }

    /**
     * Gets cell alignment
     *
     * @return
     */
    public Pos getAlignment() {
        return alignment;
    }

    /**
     * Sets cell alignment
     *
     * @param alignment
     */
    public void setAlignment(Pos alignment) {
        this.alignment = alignment;
    }

    private BooleanProperty filterable = new SimpleBooleanProperty(true);

    public BooleanProperty filterableProperty() {
        return filterable;
    }

    /**
     * Specifies whether right clicking the column will show menu item to do
     * filtering. If filterable is true, search menu item will be displayed in
     * context menu.
     */
    public void setFilterable(boolean filterable) {
        this.filterable.set(filterable);
    }

    public boolean isFilterable() {
        return this.filterable.get();
    }

    private BooleanProperty required = new SimpleBooleanProperty(false);

    /**
     * Set the field to required and cannot null. Some columns implementation
     * provide empty value that user can select if the column is not required.
     *
     * @param required
     */
    public void setRequired(boolean required) {
        this.required.set(required);
    }

    public boolean isRequired() {
        return required.get();
    }

    public BooleanProperty requiredProperty() {
        return required;
    }

    /**
     * Convert
     * <code>stringValue</code> to value that is acceptable by this column.
     * Avoid to override this method, override
     * {@link #convertFromString_Impl(java.lang.String) convertFromString_Impl}
     * instead. This method will throw runtime exception if stringValue
     * parameter is not empty but the conversion result is null. In case of
     * CheckBoxColumn, this method is overridden because CheckBoxColumn has
     * nullLabel for null value.
     *
     * @param stringValue
     * @return
     */
    public final C convertFromString(String stringValue) {
        return stringConverter.fromString(stringValue);
    }

    /**
     * Convert
     * <code>value</code> to String as represented in TableControl
     *
     * @param value
     * @return
     */
    public final String convertToString(C value) {
        return stringConverter.toString(value);
    }

    /**
     * Set the value displayed in this column for specified record to valid. To
     * set it to invalid call {@link #setInvalid}
     */
    public void setValid(R record) {
        mapInvalid.remove(record);
    }

    /**
     * Set the value displayed in this column for specified record to invalid.
     *
     * @see #setValid()
     * @param invalidMessage
     */
    public void setInvalid(R record, String invalidMessage) {
        mapInvalid.put(record, invalidMessage);
    }

    /**
     * Check whether specified record's value that displayed in this column is
     * valid. This checks against {@link #getInvalidRecordMap()}. If the record
     * is contained in the map that it is invalid.
     *
     * @param record
     * @return true for valid
     */
    public boolean isValid(R record) {
        return !mapInvalid.containsKey(record);
    }

    /**
     * Get a Record-InvalidErrorMessage map that is managed by calls to {@link #setValid()}
     * and {@link #setInvalid()}
     * @return 
     */
    public ObservableMap<R, String> getInvalidRecordMap() {
        return mapInvalid;
    }

    /**
     * Add validator. The validator will be called with the same sequence the
     * validators are added to input controls. One validator instance is
     * reusable across columns, but only can be added once in a column.
     *
     * @param validator
     */
    public void addValidator(Validator<C> validator) {
        if (!lstValidator.contains(validator)) {
            lstValidator.add(validator);
        }
    }

    public void removeValidator(Validator<C> validator) {
        lstValidator.remove(validator);
    }

    /**
     * Validate value contained in the input control. To make the input control
     * mandatory, call {@link #setRequired(boolean true)}
     *
     * @return false if invalid. True otherwise
     * @see #addValidator(com.panemu.tiwulfx.common.Validator) to add validator
     */
    public boolean validate(R record) {
        C value = getCellData(record);
        if (required.get()
                && (value == null || (value instanceof String && value.toString().trim().length() == 0))) {
            String msg = TiwulFXUtil.getLiteral("field.mandatory");
            setInvalid(record, msg);
            return false;
        }

        //do not trim
        if (value instanceof String && ((String) value).length() == 0) {
            value = null;
        }
        if (value != null) {
            for (Validator<C> validator : lstValidator) {
                String msg = validator.validate(value);
                if (msg != null && !"".equals(msg.trim())) {
                    setInvalid(record, msg);
                    return false;
                }
            }
        }
        setValid(record);
        if (popup != null && popup.isShowing()) {
            popup.hide();
        }
        return true;
    }

    private PopupControl popup;
    Label errorLabel = new Label();

    PopupControl getPopup(R record) {
        String msg = mapInvalid.get(record);
        if (popup == null) {
            popup = new PopupControl();
            final HBox pnl = new HBox();
            pnl.getChildren().add(errorLabel);
            pnl.getStyleClass().add("error-popup");
            popup.setSkin(new Skin() {
                @Override
                public Skinnable getSkinnable() {
                    return null;
                }

                @Override
                public Node getNode() {
                    return pnl;
                }

                @Override
                public void dispose() {
                }
            });
            popup.setHideOnEscape(true);
        }
        errorLabel.setText(msg);
        return popup;
    }

    private List<EditCommitListener<R, C>> lstEditCommitListener = new ArrayList<EditCommitListener<R, C>>();

    /**
     * Register a listener to editCommit event. The {@link TableColumn#setOnEditCommit(javafx.event.EventHandler) TableColumn.setOnEditCommit()}
     * doesn't work properly with TiwulFX's TableControl because there is no way to get
     * the old value.
     * @param listener 
     */
    public void addEditCommitListener(EditCommitListener<R, C> listener) {
        if (!lstEditCommitListener.contains(listener)) {
            lstEditCommitListener.add(listener);
        }
    }

    /**
     * Remove a listener of editCommit event.
     * @param listener 
     */
    public void removeEditCommitListener(EditCommitListener<R, C> listener) {
        lstEditCommitListener.remove(listener);
    }

    private void fireEditCommitChangeEvent(R record, C oldValue, C newValue) {
        for (EditCommitListener listener : lstEditCommitListener) {
            listener.editCommited(this, record, oldValue, newValue);
        }
    }

    void addRecordChange(R record, C oldValue, C newValue) {
        if (mapChangedRecord.containsKey(record)) {
            RecordChange<R, C> rc = mapChangedRecord.get(record);
            rc.setNewValue(newValue);
        } else {
            RecordChange<R, C> rc = new RecordChange<>(record, getPropertyName(), oldValue, newValue);
            mapChangedRecord.put(record, rc);
        }

        /**
         * At this point, the value of the record is still the oldValue. The newValue
         * is assigned to the record after this method call ends (See TableControl oneditcommit). 
         * In order the event is fired after the newValue is assigned, run the event in Platform.runLater
         */
        Platform.runLater(new Runnable() {

            @Override
            public void run() {
                fireEditCommitChangeEvent(record, oldValue, newValue);
            }
        });

    }

    //    public void revertRecordChange(R record) {
    //        RecordChange rc = mapChangedRecord.get(record);
    //        if (rc != null) {
    //            try {
    //                PropertyUtils.setSimpleProperty(record, getPropertyName(), rc.getOldValue());
    //                mapChangedRecord.remove(record);
    //            } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
    //                throw new RuntimeException(ex);
    //            }
    //        }
    //    }

    void clearRecordChange() {
        mapChangedRecord.clear();
    }

    Map<R, RecordChange<R, C>> getRecordChangeMap() {
        return mapChangedRecord;
    }

    private List<CellEditorListener<R, C>> lstValueChangeListener = new ArrayList<CellEditorListener<R, C>>();

    protected List<CellEditorListener<R, C>> getCellEditorListeners() {
        return lstValueChangeListener;
    }

    /**
     * Listen to cell editor's value change before it is committed to table cell.
     * To listen to commit event see {@link #addEditCommitListener(com.panemu.tiwulfx.table.EditCommitListener) addEditCommitListener()}
     * @param selectedValueListener 
     */
    public void addCellEditorListener(CellEditorListener<R, C> selectedValueListener) {
        if (!lstValueChangeListener.contains(selectedValueListener)) {
            lstValueChangeListener.add(selectedValueListener);
        }
    }

    public void removeCellEditorListener(CellEditorListener<R, C> selectedValueListener) {
        lstValueChangeListener.remove(selectedValueListener);
    }
}