JavaFX TableView for JOOQ

297 views
Skip to first unread message

Julian Decker

unread,
May 20, 2017, 3:29:18 AM5/20/17
to jOOQ User Group
Dear JOOQ Community,

after a lot of work i've created a JavaFX tableView dynamically showing a JOOQ Result / Record including the posibillity to edit and save the entries - i want to share it with you maybe it'll help some others too. There's an option to show multiple entries by a JOOQ Result (like a normal tableView) and also the possibility to show a single Record (inverted).
It's coded quick and dirty so there'll be a lot of potential to improve the code - any recommendations welcome. 

My Question: how to get the name of the table out of the record? during Debugging it's possible to see the table name but is there a function to get it?





---
The "Main" class:
public JooqRecordTableView(Table table, Result result)
= creating a TableView with multiple entries

public JooqRecordTableView(Table table, Record bRecord)
= creating a TableView with a single Record entry - inverted View - adding also a comment column





import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.Alert;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import org.jooq.*;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

import static java.util.stream.Collectors.toList;

public class JooqRecordTableView<B extends Record> extends TableView {

    private static PropertiesHandle propertiesHandle;
    private Table<?> table;
    private ObservableList<B> records = FXCollections.observableArrayList();
    private String propertyDirectory = "./resources/tables/";
    private B templateRecord = null;

    /* Constructor for standard tableView with JOOQ Result */
    public JooqRecordTableView(Table table, Result result) {
        this.table = table;
        tableRecords(result);
        loadPropertiesForTable();
        tableColumns();
        this.setEditable(true);
    }
    /* generate Table Columns */
    private <R extends Record, O extends Object> void tableColumns() {

        List<TableColumn<R, ?>> tableColumns = new ArrayList<>();

        if (templateRecord != null) {
            tableColumns.addAll(Stream.of(templateRecord.fields())
                    .map((Field<?> f) -> {
                        TableColumn<R, ?> column = new TableColumn<>(f.getName());
                        column.setVisible((boolean) propertiesHandle.getProperty(f.getName() + "-visible", boolean.class));
                        column.setEditable((boolean) propertiesHandle.getProperty(f.getName() + "-editable", boolean.class));
                        column.setText(propertiesHandle.getProperty(f.getName(), String.class).toString());
                        column.setCellValueFactory(d -> new SimpleObjectProperty(d.getValue().get(f)));
                        List<?> preloadValues = propertiesHandle.getPropertyValueList(f.getName(), String.class);
                        column.setCellFactory(param -> new CellUniversalEditor(f, preloadValues));
                        column.setOnEditCommit(
                                t -> {
                                    Field columnField = f;
                                    Object newValue = t.getNewValue();

                                    if (f.getDataType().hasLength()) {
                                        String s = (String) newValue;
                                        if (f.getDataType().length() > s.length()) {
                                            records.get(t.getTablePosition().getRow()).set(columnField, newValue);
                                            ((UpdatableRecord) records.get(t.getTablePosition().getRow())).update();
                                            System.out.println("Updating: " + f.getName() + "/" + newValue);
                                        } else {
                                            Alert alert = new Alert(Alert.AlertType.ERROR);
                                            alert.setHeaderText("Error: too much characters");
                                            alert.setContentText("Max.: " + f.getDataType().length());
                                            alert.showAndWait();
                                        }
                                    } else {
                                        records.get(t.getTablePosition().getRow()).set(columnField, newValue);
                                        ((UpdatableRecord) records.get(t.getTablePosition().getRow())).update();
                                        System.out.println("Updating: " + f.getName() + "/" + newValue);
                                    }
                                    this.refresh();
                                }
                        );
                        return column;
                    })
                    .collect(toList()));

            for (TableColumn tableColumn : tableColumns) {
                this.getColumns().add(tableColumn);
            }
        }
    }
    private void tableRecords(Result result) {
        for (Object o : result) {
            B record = (B) o;
            records.add(record);
        }
        if (records.size() > 0) {
            templateRecord = records.get(0);
        }
        this.setItems(records);
        this.refresh();
    }

    /* Constructor for inverted >single< Record tableView with JOOQ Record */
    public JooqRecordTableView(Table table, Record bRecord) {
        this.setEditable(true);
        this.table = table;
        loadPropertiesForTable();
        Record dataRecord = bRecord;

        ObservableList<ColumnInformation> data = FXCollections.observableArrayList();

        //prepare ColumnInformation for each Field
        for (Field field : bRecord.fields()) {
            String comment = propertiesHandle.getProperty(field.getName() + "-comment", String.class).toString();
            String tooltip = propertiesHandle.getProperty(field.getName() + "-tooltip", String.class).toString();
            Boolean visible = (Boolean) propertiesHandle.getProperty(field.getName() + "-visible", Boolean.class);
            Boolean editable = (Boolean) propertiesHandle.getProperty(field.getName() + "-editable", Boolean.class);
            if (comment.equals(field.getName() + "-comment")) {
                comment = "";
            }
            if (tooltip.equals(field.getName() + "-tooltip")) {
                tooltip = "";
            }
            List<?> preloadObjects = propertiesHandle.getPropertyValueList(field.getName(), String.class);

            if (visible) {
                data.add(new ColumnInformation(propertiesHandle.getProperty(field.getName(), String.class).toString(), field.getValue(bRecord), comment, field, tooltip,
                        editable, visible, preloadObjects));
            }
        }
        getItems().addAll(data);

        // table definition
        TableColumn<ColumnInformation, String> nameColumn = new TableColumn<>("Parameter");
        nameColumn.setCellValueFactory(d -> new SimpleObjectProperty(d.getValue().getName()));
        nameColumn.setEditable(false);

        TableColumn<ColumnInformation, Object> valueColumn = new TableColumn<>("Wert");
        valueColumn.setEditable(true);
        valueColumn.setCellValueFactory(d -> new SimpleObjectProperty(d.getValue().getValue()));
        CellUniversalEditor cellUniversalEditor = new CellUniversalEditor();
        valueColumn.setCellFactory(param -> new CellUniversalEditor());
        valueColumn.setOnEditCommit(
                t -> {
                    ColumnInformation cellData = data.get(t.getTablePosition().getRow());
                    Field columnField = cellData.getField();
                    Object newValue = t.getNewValue();

                    if (columnField.getDataType().hasLength()) {
                        String s = (String) newValue;
                        if (columnField.getDataType().length() > s.length()) {
                            data.get(t.getTablePosition().getRow()).setValue(newValue);
                            dataRecord.set(columnField, newValue);
                            ((UpdatableRecord) dataRecord).update();
                            System.out.println("Updating: " + columnField.getName() + "/" + newValue);
                        } else {
                            Alert alert = new Alert(Alert.AlertType.ERROR);
                            alert.setHeaderText("Error: too much characters");
                            alert.setContentText("Max.: " + f.getDataType().length());
                            alert.showAndWait();
                        }
                    } else {
                        data.get(t.getTablePosition().getRow()).setValue(newValue);
                        dataRecord.set(columnField, newValue);
                        ((UpdatableRecord) dataRecord).update();
                        System.out.println("Updating: " + columnField.getName() + "/" + newValue);
                    }
                    this.refresh();
                }
        );

        TableColumn<ColumnInformation, Object> commentColumn = new TableColumn<>("Kommentar");
        commentColumn.setCellValueFactory(d -> new SimpleObjectProperty(d.getValue().getComment()));
        commentColumn.setEditable(false);
        valueColumn.setSortable(false);
        getColumns().setAll(nameColumn, valueColumn, commentColumn);
    }

    /* Load Properties File for Table*/
    private void loadPropertiesForTable() {
        String propertyPath = propertyDirectory + table.getName();
        propertiesHandle = new PropertiesHandle(propertyPath);
    }

}

class ColumnInformation {
    private String name;
    private Object value;
    private String comment;
    private Field<?> field;
    private String tooltipText;
    private List<?> preloadValues;

    private Boolean editable;
    private Boolean visible;

    ColumnInformation(String name, Object value, String comment, Field<?> field, String tooltipText, Boolean editable, Boolean visible, List<?> preloadValues) {
        this.setName(name);
        this.setValue(value);
        this.setComment(comment);
        this.setField(field);
        this.setTooltipText(tooltipText);
        this.setVisible(visible);
        this.setEditable(editable);
        this.setPreloadValues(preloadValues);
    }

    public String getTooltipText() {
        return tooltipText;
    }

    public void setTooltipText(String tooltipText) {
        this.tooltipText = tooltipText;
    }

    public Field<?> getField() {
        return field;
    }

    public void setField(Field<?> field) {
        this.field = field;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Object getValue() {
        return value;
    }

    public void setValue(Object value) {
        this.value = value;
    }

    public String getComment() {
        return comment;
    }

    public void setComment(String comment) {
        this.comment = comment;
    }

    public Boolean getEditable() {
        return editable;
    }

    public void setEditable(Boolean editable) {
        this.editable = editable;
    }

    public Boolean getVisible() {
        return visible;
    }

    public void setVisible(Boolean visible) {
        this.visible = visible;
    }

    public List<?> getPreloadValues() {
        return preloadValues;
    }

    public void setPreloadValues(List<?> preloadValues) {
        this.preloadValues = preloadValues;
    }
}


The "universall cell" class representing a TableCell and dynamically adapts to record column Datatype

import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.util.StringConverter;
import org.jooq.Field;
import org.jooq.Record;
import org.jooq.Table;
import tornadofx.control.DateTimePicker;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

class CellUniversalEditor<R extends Record> extends TableCell<Table<R>, Object> {

    private TextField textField;
    private CheckBox checkBox;
    private DateTimePicker dateTimePicker;
    private DatePicker datePicker;
    private Label valueLabel = new Label();
    private Spinner<Integer> integerSpinner;
    private ComboBox comboBox;
    private Field field = null;

    private Object graphicElement;

    private List<?> preloadValues = new ArrayList<>();
    private Boolean cellEditable = false;

    public CellUniversalEditor() {
    }

    /*
    Constructor with preloadValues
     */
    public CellUniversalEditor(Field field, List<?> preloadValues) {
        this.field = field;
        this.preloadValues = preloadValues;
    }

    public Boolean getCellEditable() {
        return cellEditable;
    }

    public void setCellEditable(Boolean cellEditable) {
        this.cellEditable = cellEditable;
    }

    /*
    Override Functions for TableCell
     */
    @Override
    public void startEdit() {
        String dataType = null;
        Object value = null;
        ColumnInformation columnInformation = null;
        value = getValue();

        if (field == null) {
            columnInformation = (ColumnInformation) getTableRow().getItem();
            dataType = columnInformation.getField().getDataType().getType().getSimpleName();
            preloadValues = columnInformation.getPreloadValues();
            cellEditable = columnInformation.getEditable();
        } else {
            dataType = field.getDataType().getType().getSimpleName();
            cellEditable = getTableColumn().isEditable();
        }

        if (cellEditable) {
            super.startEdit();
            switch (dataType) {
                case "Boolean":
                    checkBox = new CheckBox();
                    Boolean aBoolean = value == null ? false : (boolean) value;
                    checkBox.setSelected(aBoolean);
                    checkBox.setOnAction(event -> commitEdit(checkBox.isSelected()));
                    graphicElement = checkBox;
                    break;

                case "LocalDateTime":
                    dateTimePicker = new DateTimePicker();
                    dateTimePicker.setDateTimeValue((LocalDateTime) getValue());
                    dateTimePicker.setOnAction(event -> commitEdit(dateTimePicker.getDateTimeValue()));
                    graphicElement = dateTimePicker;
                    break;

                case "LocalDate":
                    datePicker = new DateTimePicker();
                    datePicker.setValue((LocalDate) getValue());
                    datePicker.setOnAction(event -> commitEdit(datePicker.getValue()));
                    graphicElement = datePicker;
                    break;

                case "Integer":
                    integerSpinner = new Spinner<>();
                    Integer integer = value == null ? 0 : (int) value;
                    SpinnerValueFactory<Integer> valueFactory = new SpinnerValueFactory.IntegerSpinnerValueFactory(0, Integer.MAX_VALUE, integer);
                    integerSpinner.setValueFactory(valueFactory);
                    integerSpinner.setEditable(true);
                    integerSpinner.getEditor().setOnMouseClicked(event -> integerSpinner.getEditor().selectAll());
                    integerSpinner.getEditor().setOnKeyPressed(t -> {
                        if (t.getCode() == KeyCode.ENTER) {
                            String integerString = integerSpinner.getEditor().getText();
                            if (valueFactory != null) {
                                StringConverter<Integer> converter = valueFactory.getConverter();
                                if (converter != null) {
                                    try {
                                        valueFactory.setValue(converter.fromString(integerString));
                                        commitEdit(integerSpinner.valueProperty().getValue());
                                    } catch (NumberFormatException nfe) {
                                        integerSpinner.getEditor().setText(converter.toString(valueFactory.getValue()));
                                    }
                                }
                            }
                        } else if (t.getCode() == KeyCode.ESCAPE) {
                            cancelEdit();
                        }
                    });
                    integerSpinner.getEditor().selectAll();
                    graphicElement = integerSpinner;
                    break;

                case "String":
                    if (preloadValues.size() > 0) {
                        comboBox = new ComboBox<>();
                        comboBox.getItems().add("");
                        comboBox.getItems().addAll(preloadValues);
                        if (getValue() != null) {
                            comboBox.getSelectionModel().select(getValue());
                        }
                        comboBox.setOnAction(event -> commitEdit(comboBox.getValue()));
                        graphicElement = comboBox;
                    } else {
                        String s = value == null ? "" : (String) value;
                        textField = new TextField(s);
                        textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
                        textField.setOnKeyPressed(t -> {
                            if (t.getCode() == KeyCode.ENTER) {
                                commitEdit(textField.getText());
                            } else if (t.getCode() == KeyCode.ESCAPE) {
                                cancelEdit();
                            }
                        });
                        graphicElement = textField;
                    }
                    break;

                default:
                    Alert alert = new Alert(Alert.AlertType.ERROR);
                    alert.setHeaderText("Form Error");
                    alert.setContentText("Datatype not supported!");
                    alert.showAndWait();
            }
            setGraphic((Node) graphicElement);
            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
        }
    }

    @Override
    public void cancelEdit() {
        super.cancelEdit();
        setGraphic(valueLabel);
    }

    @Override
    public void updateItem(Object item, boolean empty) {
        super.updateItem(item, empty);
        setGraphic(valueLabel);
        setContentDisplay(ContentDisplay.GRAPHIC_ONLY);

        if (empty || item == null) {
            setText("");
            valueLabel.setText("");
        } else {
            if (isEditing()) {
            } else {
                setText(getValue().toString());
                switch (item.getClass().getSimpleName()) {
                    case "Boolean":
                        if ((Boolean) item == true) {
                            valueLabel.setText("Yes");
                        } else {
                            valueLabel.setText("No");
                        }
                        break;
                    case "LocalDate":
                        valueLabel.setText(date2GermanFormat((LocalDate) item));
                        break;
                    case "LocalDateTime":
                        valueLabel.setText(datetime2GermanFormat((LocalDateTime) item));
                        break;
                    default:
                        valueLabel.setText(getValue().toString());
                }
            }
        }
    }

    private Object getValue() {
        return getItem() == null ? null : getItem();
    }
}

The Properties File - handling the entries for each Table / Column and creating - adding new Information if needed

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;

public class PropertiesHandle {

    private SortedProperties properties;
    private String propertyPath;

    public PropertiesHandle(String propertyPath) {
        this.propertyPath = propertyPath + ".properties";

        Path path = Paths.get(this.propertyPath);
        if (Files.exists(path)) {
            properties = new SortedProperties();
            try {
                properties.load(new FileInputStream(this.propertyPath));
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            properties = new SortedProperties();
            writeProperties2File();
        }
    }

    private void writeProperties2File() {
        File file = new File(propertyPath);
        try {
            FileOutputStream fileOut = FileUtils.openOutputStream(file);
            properties.store(fileOut, propertyPath);
            fileOut.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public Object getProperty(String key, Class aClass) {
        Object o = null;

        String value = properties.getProperty(key);
        //if key exists
        if (value != null) {
            //if key is empty
            if (!value.equals("")) {
                switch (aClass.getSimpleName()) {
                    case "boolean":
                        o = Boolean.valueOf(value);
                        break;

                    case "Boolean":
                        o = Boolean.valueOf(value);
                        break;

                    default:
                        o = aClass.cast(value);
                }
            } else {
                switch (aClass.getSimpleName()) {
                    case "boolean":
                        o = new Boolean(false);
                        break;
                    case "Boolean":
                        o = new Boolean(false);
                        break;
                    default:
                        o = key;
                }

            }
            //if key not exists
        } else {
            properties.setProperty(key, "");
            writeProperties2File();
            switch (aClass.toString()) {
                case "boolean":
                    o = new Boolean(false);
                    break;

                default:
                    o = key;
            }
        }

        return o;
    }

    public List getPropertyValueList(String key, Class aClass) {
        List objects = new ArrayList<>();
        String sValue = (String) getProperty(key + "-values", String.class);
        if (!sValue.equals(key + "-values")) {
            List<String> strings = Arrays.asList(sValue.split(","));
            for (String s : strings){
                objects.add(s.trim());
            }
        }
        return objects;
    }

}

class SortedProperties extends Properties {
    public Enumeration keys() {
        Enumeration keysEnum = super.keys();
        Vector<String> keyList = new Vector<String>();
        while (keysEnum.hasMoreElements()) {
            keyList.add((String) keysEnum.nextElement());
        }
        Collections.sort(keyList);
        return keyList.elements();
    }
}



Lukas Eder

unread,
May 22, 2017, 6:43:29 AM5/22/17
to jooq...@googlegroups.com
Hi Julian,

Thank you very much for sharing this. Oh wow, that does look like a lot of glue code. If you feel there are any specific areas / pain points where you had expected a bit less friction from the jOOQ side, do let me know and I'm happy to discuss.

Regarding your question:

2017-05-20 9:29 GMT+02:00 Julian Decker <juliand...@gmail.com>:
My Question: how to get the name of the table out of the record? during Debugging it's possible to see the table name but is there a function to get it?






You're looking for the TableRecord type, not the Record type. There's TableRecord.getTable()

A TableRecord is what you're getting when you're selecting using DSLContext.selectFrom():

These select statements are guaranteed not to contain any custom projections / joins, etc. but only columns from a single table. Another option is to use Result.into(Table) or ResultQuery.fetchInto(Table):


These will take any Result and pick only the columns from a specific table to create new TableRecords from them.

Hope this helps,
Lukas
Reply all
Reply to author
Forward
0 new messages