0

I have created a cell factory wrapper to enable custom configuration of a JavaFX table cell. See code below.

package idmas.controller.modules.tableview;

import idmas.model.Cml;
import idmas.model.CmlDAO;
import idmas.model.Damageloop;
import idmas.model.modules.general.dateMethods;
import java.util.Date;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.util.Callback;

/**
 * Generic cell factory that can be used to override specific properties of cells, such as formatting (text size, color etc.) and even values.
 * @author Joshua Adams
 * @param <S> = Data type of table view to be modified. E.g. Damageloop, Equipment, CML, Inspection etc.
 * @param <T> = Data type of specific cell being overridden. E.g. Date, Integer, Double etc.
 */
public class CellFactoryCustom<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>>  {

    //Represents the name of the column for the specific tableview, i.e. Last Internal Inspection Date etc.
    String colname;

    //Constructors for class wide objects
    CmlDAO cmldao = new CmlDAO();

    /**
     * Class constructor requires a column name to be passed so the specific formatting for that column's cells can be applied.
     * @param colname = Name of column to apply formatting to.
     */
    public CellFactoryCustom (String colname) {
        this.colname = colname;
    }

    /**
     * Main method to override the properties of the cells for a table column.
     * @param arg
     * @return 
     */
    @Override
    public TableCell<S, T> call(TableColumn<S, T> arg) {
        TableCell<S, T> cell = new TableCell<S, T>() {
            @Override
            protected void updateItem(T item, boolean empty) {
                //super allows reffering to methods in superclass
                super.updateItem(item, empty);
                //recommended syntax for overridding the updateItem method - Refer to update item javadoc.
                if (empty || item == null) {
                    setText(null);
                    setGraphic(null);
                    getStyleClass().removeAll("highlightCellBlack");
                    getStyleClass().add(getTableRow().getStyleClass().toString());
                } else {
                    setConditionalFormatting(this,item);
                }
            }
        };
        return cell;
    }

    /**
     * Specific formatting rules which are applied to a columns cells based on its column name.
     * @param cell
     * @param item 
     */
    public void setConditionalFormatting (TableCell<S,T> cell, T item) {
        //Constructors for reference classes
        dateMethods datemethods = new dateMethods();
        Date currentdateplus60 = new Date();
        Date date;
        Damageloop damageloop;
        Cml cml;
        ComboBox combobox;
        //Current styles need to be removed to ensure all specific styles for each cell are applied correctly
        cell.getStyleClass().removeAll("highlightCellBlack");
        //Switch statement selected over if statement to improve performance of code and readibility
        switch (colname) {
            case "colNextExternalInspectionDate":
                damageloop = (Damageloop) cell.getTableRow().getItem();
                cell.setText(datemethods.getDateToString((Date) item,"dd/MM/yyyy"));
                currentdateplus60 = datemethods.getDatePlusDays(currentdateplus60, 60);
                date = (Date) item;
                if(date.before(currentdateplus60) && damageloop.getErrorcode() != 2 & damageloop.getErrorcode() != 3) {
                    cell.getStyleClass().add("highlightCellBlack");
                } else {
                    cell.getStyleClass().add(cell.getTableRow().getStyleClass().toString());
                }
                break;
            case "colNextInternalInspectionDate":
                damageloop = (Damageloop) cell.getTableRow().getItem();
                cell.setText(datemethods.getDateToString((Date) item,"dd/MM/yyyy"));
                currentdateplus60 = datemethods.getDatePlusDays(currentdateplus60, 60);
                date = (Date) item;
                if(date.before(currentdateplus60) && damageloop.getErrorcode() != 1 && damageloop.getErrorcode() != 3) {
                    cell.getStyleClass().add("highlightCellBlack");
                } else {
                    cell.getStyleClass().add(cell.getTableRow().getStyleClass().toString());
                }
                break;
            case "colCmlStatus":
                cml = (Cml) cell.getTableRow().getItem();
                String[] fieldsArray = new String[]{"C = Continue to Monitor", "S = Scoped", "X = Redundant"};
                String[] disabledFieldsArray = new String[]{"S = Scoped"};
                String SQLString = "UPDATE IDMAS.CML SET CMLSTATUS = '<NEW_STATUS>' WHERE EQUIPMENT_ID = '" + cml.getEquipmentId() + "' AND CML_NO = " + cml.getCmlNo();
                String replacestring = "<NEW_STATUS>";
                String defaultvalue = item.toString();
                ComboBoxCustom comboboxcustom = new ComboBoxCustom (fieldsArray, disabledFieldsArray, SQLString, replacestring, defaultvalue);
                comboboxcustom.setPrefWidth(10);
                cell.setGraphic(comboboxcustom);
                break;
            case "colCmlStatusRemediation":
                ObservableList<String> options = FXCollections.observableArrayList(
                        "A = Approved for Remediation",
                        "C = Continue to Monitor",
                        "F = Fit for Service",
                        "R = Recommend Remediation"
                );     
                cml = (Cml) cell.getTableRow().getItem();
                combobox = new ComboBox(options);
                combobox.setValue(item.toString());
                combobox.setPrefWidth(10);
                combobox.valueProperty().addListener(new ChangeListener<String>() {
                    @Override 
                    public void changed(ObservableValue ov, String oldvalue, String newvalue) {
                        cml.setCmlstatusremediation(newvalue.charAt(0));
                        cmldao.updateCml(cml);
                        //Platform runlater required to update the combox with the new value
                        Platform.runLater(() -> {
                            combobox.setValue(String.valueOf(newvalue.charAt(0)));
                        });
                    }    
                });
                cell.setGraphic(combobox);
                break;
            case "colTemporaryrepairinstalled":
                cml = (Cml) cell.getTableRow().getItem();
                CheckBox checkbox = new CheckBox();
                checkbox.setSelected(Boolean.valueOf(item.toString()));
                checkbox.selectedProperty().addListener(new ChangeListener<Boolean>() {
                    @Override
                    public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
                        cml.setTemporaryrepairinstalled(newValue);
                        cmldao.updateCml(cml);
                        checkbox.setSelected(newValue);
                    }
                });
                cell.setGraphic(checkbox);
                break;
            default:
                break;
        }   
    }
}

And this cellfactory wrapper will work on generally navigating through all of my tableviews. However, as soon as I perform another function in my system such as adding new records, editing records etc. The following error is thrown.

Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
    at idmas.controller.modules.tableview.CellFactoryCustom.setConditionalFormatting(CellFactoryCustom.java:110)
    at idmas.controller.modules.tableview.CellFactoryCustom$1.updateItem(CellFactoryCustom.java:60)
    at javafx.scene.control.TableCell.updateItem(TableCell.java:663)
    at javafx.scene.control.TableCell.indexChanged(TableCell.java:468)
    at javafx.scene.control.IndexedCell.updateIndex(IndexedCell.java:116)
    at com.sun.javafx.scene.control.skin.TableRowSkinBase.requestCellUpdate(TableRowSkinBase.java:659)
    at com.sun.javafx.scene.control.skin.TableRowSkinBase.handleControlPropertyChanged(TableRowSkinBase.java:234)
    at com.sun.javafx.scene.control.skin.TableRowSkin.handleControlPropertyChanged(TableRowSkin.java:70)
    at com.sun.javafx.scene.control.skin.BehaviorSkinBase.lambda$registerChangeListener$61(BehaviorSkinBase.java:197)
    at com.sun.javafx.scene.control.MultiplePropertyChangeListenerHandler$1.changed(MultiplePropertyChangeListenerHandler.java:55)
    at javafx.beans.value.WeakChangeListener.changed(WeakChangeListener.java:89)
    at com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:182)
    at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
    at javafx.beans.property.ReadOnlyIntegerPropertyBase.fireValueChangedEvent(ReadOnlyIntegerPropertyBase.java:72)
    at javafx.beans.property.ReadOnlyIntegerWrapper.fireValueChangedEvent(ReadOnlyIntegerWrapper.java:102)
    at javafx.beans.property.IntegerPropertyBase.markInvalid(IntegerPropertyBase.java:113)
    at javafx.beans.property.IntegerPropertyBase.set(IntegerPropertyBase.java:147)
    at javafx.scene.control.IndexedCell.updateIndex(IndexedCell.java:115)
    at com.sun.javafx.scene.control.skin.VirtualFlow.setCellIndex(VirtualFlow.java:1957)
    at com.sun.javafx.scene.control.skin.VirtualFlow.addTrailingCells(VirtualFlow.java:1344)
    at com.sun.javafx.scene.control.skin.VirtualFlow.layoutChildren(VirtualFlow.java:1197)
    at javafx.scene.Parent.layout(Parent.java:1087)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Parent.layout(Parent.java:1093)
    at javafx.scene.Scene.doLayoutPass(Scene.java:552)
    at javafx.scene.Scene$ScenePulseListener.pulse(Scene.java:2397)
    at com.sun.javafx.tk.Toolkit.lambda$runPulse$30(Toolkit.java:355)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.tk.Toolkit.runPulse(Toolkit.java:354)
    at com.sun.javafx.tk.Toolkit.firePulse(Toolkit.java:381)
    at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:510)
    at com.sun.javafx.tk.quantum.QuantumToolkit.pulse(QuantumToolkit.java:490)
    at com.sun.javafx.tk.quantum.QuantumToolkit.lambda$runToolkit$404(QuantumToolkit.java:319)
    at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
    at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at com.sun.glass.ui.win.WinApplication.lambda$null$148(WinApplication.java:191)
    at java.lang.Thread.run(Thread.java:745)

I cant seem to figure out why this wrapper thinks that my CML class has null values in it. Anyone have any ideas as to why this Null Pointer exception is being thrown?

Josh
  • 808
  • 1
  • 16
  • 35
  • 1
    Which line is "CellFactoryCustom.java:110"? (where the exception is thrown) – Itai Nov 24 '16 at 05:29
  • This Line... String SQLString = "UPDATE IDMAS.CML SET CMLSTATUS = '' WHERE EQUIPMENT_ID = '" + cml.getEquipmentId() + "' AND CML_NO = " + cml.getCmlNo(); – Josh Nov 24 '16 at 07:41
  • Can you please include the definition of your `Cml` class? Also - have a look at this question: http://stackoverflow.com/questions/218384/what-is-a-nullpointerexception-and-how-do-i-fix-it – Itai Nov 24 '16 at 09:09
  • @sillyfly That class is irrelevant here, since no part of the class occurs in the stacktrace and string concatenation with `null` never yields a NPE, so in that line the only source of the error could be `cml` being null. – fabian Nov 24 '16 at 11:56

1 Answers1

3

There is no guarantee that the TableCell is already associated with a TableRow or that the TableRow is already filled when the updateItem method is called the first time.

In your case probably getTableRow() returns null (which would mean however that cml = (Cml) cell.getTableRow().getItem(); is the line the exception is thrown, not String SQLString = "UPDATE IDMAS.CML SET CMLSTATUS = '<NEW_STATUS>' WHERE EQUIPMENT_ID = '" + cml.getEquipmentId() + "' AND CML_NO = " + cml.getCmlNo();); At least that happened when I tried to reproduce the error.

However if

String SQLString = "UPDATE IDMAS.CML SET CMLSTATUS = '<NEW_STATUS>' WHERE EQUIPMENT_ID = '" + cml.getEquipmentId() + "' AND CML_NO = " + cml.getCmlNo();

is indeed the line causing the error, then the only way for that to happen that results in a stacktrace like this is if cml is null.

A way to fix this would be getting the item from the table based on the index:

cml = (Cml) cell.getTableView().getItems().get(getIndex());
fabian
  • 80,457
  • 12
  • 86
  • 114
  • Thanks for the response. When you say "There is no guarantee that the TableCell is already associated..." I would think that the Override of updateItem I have that accounts for Nulls i.e. "if (empty || item == null) {..." would enforce a value to be populated in the table cell before the conditional formatting method is called. So can you elaborate a bit more here? Also agree with the rest of your conclusions. I'll test your recommended code and see if this resolves the issue and get back to you. – Josh Nov 24 '16 at 23:40
  • Ok this stopped the error from occurring however, I don't really understand why referring to the cells tableview first and then getting an indexed item always ensures that data is returned but referencing the cells row doesnt return any data. Can you please add a reason to the answer then I will mark this as accepted. Cheers! – Josh Nov 25 '16 at 02:02
  • @Josh: For this I'd need to dig through the source code ot the `TableView`'s skin, which would be a lot of work. However in the end it should come down to something like this: When the cells are initially created and added to the scene, `updateIndex`/`updateItem` are called before the `TableRow` is assigned. – fabian Nov 25 '16 at 07:56
  • no worries. We'll just leave it at your response for now then. – Josh Nov 27 '16 at 23:53
  • @fabian Did you do any research regarding to this? I have almost the same problem, and I cannot understand why `getTableRow().getItem()` returns null while `empty == false` and `item != null`. So the TableRow is constructed, but its getItem() returns null. Although I can use your suggestion as solution, but I think that works around some bug, or my mistake. – Sunflame Sep 27 '17 at 15:39