BeanTableModel.java

// @formatter:off
 /*******************************************************************************
 *
 * This file is part of JMad.
 * 
 * Copyright (c) 2008-2011, CERN. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 ******************************************************************************/
// @formatter:on

package cern.accsoft.steering.util.gui.table;

import javax.swing.table.AbstractTableModel;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * this class is a simple table model, which enables the display of an object, that provides java-bean like properties
 * in a table. the table then has as many rows as the bean has properties and 2 columns: one for the name of the
 * property and one for the value.
 * 
 * @author Kajetan Fuchsberger (kajetan.fuchsberger at cern.ch)
 */
public class BeanTableModel extends AbstractTableModel {
    /** the logger for the class */
    private final static Logger logger = LoggerFactory.getLogger(BeanTableModel.class);

    /** the amount of Columns */
    private final static int COL_COUNT = 3;

    /* the indizes for the columns */
    private final static int COL_INDEX_SELECTED = 0;
    private final static int COL_INDEX_NAME = 1;
    private final static int COL_INDEX_VALUE = 2;

    /* the prefixes we use to identify a getter- and setter- methods */
    private final static String GETTER_PREFIX_DEFAULT = "get";
    private final static String GETTER_PREFIX_BOOLEAN = "is";
    private final static String SETTER_PREFIX = "set";

    /**
     * the classes, which are allowed for the value-column. One has to take care, that they are compatible, since table
     * only supports one class for the column. To tell the table, which renderer to use, we use the first entry in this
     * array. By default we support Double and double.
     */
    private Class<?>[] valueClasses = new Class<?>[] { Double.class, double.class };

    /** the explicit bean-class to use */
    private Class<?> beanClass;

    /** the actually displayed bean */
    private Object bean;

    /** the names of the getters corresponding to the properties */
    private List<Method> getters = new ArrayList<>();

    /** determines, if the values are editable */
    private boolean editable = false;

    /** an edit-handler, which enables to check/uncheck certain properties */
    private BeanTableEditHandler editHandler = null;

    public BeanTableModel(Class<?> beanClass) {
        this.beanClass = beanClass;
    }

    /**
     * the according getter-prefix for the configured type.
     * 
     * @return the prefix for getters.
     */
    private String getGetterPrefix() {
        if (Boolean.class.equals(valueClasses[0])) {
            return GETTER_PREFIX_BOOLEAN;
        } else {
            return GETTER_PREFIX_DEFAULT;
        }
    }

    @Override
    public int getColumnCount() {
        return COL_COUNT;
    }

    @Override
    public int getRowCount() {
        return this.getters.size();
    }

    @Override
    public Object getValueAt(int row, int col) {
        if (col == COL_INDEX_SELECTED) {
            if (getEditHandler() != null) {
                return getEditHandler().getCheckValue(this.bean, getPropertyName(row));
            } else {
                return false;
            }
        } else if (col == COL_INDEX_NAME) {
            return getPropertyName(row);
        } else if (col == COL_INDEX_VALUE) {
            return getPropertyValue(row);
        } else {
            logger.warn("unknown column number '" + col + "'");
            return null;
        }
    }

    @Override
    public Class<?> getColumnClass(int col) {
        switch (col) {
        case COL_INDEX_SELECTED:
            return Boolean.class;
        case COL_INDEX_NAME:
            return String.class;
        case COL_INDEX_VALUE:
            return getValueClasses()[0];
        default:
            return null;
        }
    }

    @Override
    public String getColumnName(int col) {
        switch (col) {
        case COL_INDEX_SELECTED:
            if (getEditHandler() != null) {
                return getEditHandler().getCheckableColumnHeader();
            } else {
                return "select";
            }
        case COL_INDEX_NAME:
            return "property name";
        case COL_INDEX_VALUE:
            return "value";
        default:
            return null;
        }
    }

    @Override
    public void setValueAt(Object value, int row, int col) {
        switch (col) {
        case COL_INDEX_SELECTED:
            if (getEditHandler() != null) {
                getEditHandler().setCheckValue(getBean(), getPropertyName(row), (Boolean) value);
            }
            break;
        case COL_INDEX_VALUE:
            setPropertyValue(value, row);
            break;
        default:
            super.setValueAt(value, row, col);
            break;
        }
    }

    @Override
    public boolean isCellEditable(int row, int col) {
        if (COL_INDEX_SELECTED == col) {
            return ((getEditHandler() != null) && (getEditHandler().isEditable()));
        } else if (COL_INDEX_VALUE == col) {
            return isEditable();
        } else {
            return false;
        }
    }

    /**
     * sets the given value to the property of the given index
     * 
     * @param value the value to set
     * @param index the index of the property to which to set the value
     */
    private void setPropertyValue(Object value, int index) {
        if (getBean() == null) {
            return;
        }

        Method setter = getSetter(index);
        if (setter == null) {
            logger.error("no setter method for property '" + getPropertyName(index) + "' could be found.");
            return;
        }

        try {
            setter.invoke(getBean(), value);
        } catch (IllegalArgumentException e) {
            logger.error("Error while invoking setter '" + setter.getName() + "' of bean '" + getBean().toString(), e);
        } catch (IllegalAccessException e) {
            logger.error("Error while invoking setter '" + setter.getName() + "' of bean '" + getBean().toString(), e);
        } catch (InvocationTargetException e) {
            logger.error("Error while invoking setter '" + setter.getName() + "' of bean '" + getBean().toString(), e);
        }
    }

    /**
     * searches in the bean for a getter of property with given index
     * 
     * @param index the index for which to find the getter
     * @return the getter method
     */
    private Method getSetter(int index) {
        String getterName = this.getters.get(index).getName();

        String setterName = SETTER_PREFIX + getterName.substring(getGetterPrefix().length());
        Method setter = null;
        for (int i = 0; i < valueClasses.length; i++) {
            Class<?> valueClass = this.valueClasses[i];
            try {
                setter = this.bean.getClass().getMethod(setterName, valueClass);
                if (setter != null) {
                    break;
                }
            } catch (SecurityException e) {
                logger.error("Error while searching for method '" + setterName + "'.", e);
            } catch (NoSuchMethodException e) {
                /* do nothing, just search for another one. */
            }
        }
        return setter;
    }

    /**
     * return the name of the property for a given index
     * 
     * @param index the index in the getterNames - list
     * @return the name of the property
     */
    private String getPropertyName(int index) {
        String getterName = this.getters.get(index).getName();

        int startIndex = getGetterPrefix().length();
        String propertyName = getterName.substring(startIndex, startIndex + 1).toLowerCase();
        if (getterName.length() > getGetterPrefix().length() + 1) {
            propertyName += getterName.substring(startIndex + 1);
        }
        return propertyName;
    }

    /**
     * return the value of the property
     * 
     * @param index the index in the getterNames - list
     * @return the value
     */
    private Object getPropertyValue(int index) {
        if (getBean() == null) {
            return null;
        }
        Method method = this.getters.get(index);
        try {
            return method.invoke(getBean());
        } catch (IllegalArgumentException e) {
            logger.error("Error while invoking getter '" + method.getName() + "' of bean '" + getBean().toString(), e);
        } catch (IllegalAccessException e) {
            logger.error("Error while invoking getter '" + method.getName() + "' of bean '" + getBean().toString(), e);
        } catch (InvocationTargetException e) {
            logger.error("Error while invoking getter '" + method.getName() + "' of bean '" + getBean().toString(), e);
        }
        return null;
    }

    /**
     * searches for all getters in the bean. These are methods which begin with the prefix. Additionaly we only keep
     * such, which return the classes which we support.
     */
    private void initGetterNames() {
        this.getters.clear();

        for (Method method : this.beanClass.getMethods()) {
            /*
             * we are only interested in getters, that take no argument.
             */
            if (method.getParameterTypes().length > 0) {
                continue;
            }
            String methodName = method.getName();
            if (methodName.startsWith(getGetterPrefix()) && methodName.length() > getGetterPrefix().length()) {
                for (int i = 0; i < getValueClasses().length; i++) {
                    if (method.getReturnType().equals(getValueClasses()[i])) {
                        this.getters.add(method);
                        break;
                    }
                }
            }
        }
    }

    /**
     * set the bean
     * 
     * @param bean the bean, which shall be displayed.
     */
    public void setBean(Object bean) {
        this.bean = bean;
        initGetterNames();
        fireTableDataChanged();
    }

    /**
     * @return the actual displayed bean
     */
    private Object getBean() {
        return this.bean;
    }

    /**
     * @param editable the editable to set
     */
    public void setEditable(boolean editable) {
        this.editable = editable;
    }

    /**
     * @return the editable
     */
    public boolean isEditable() {
        return editable;
    }

    /**
     * @param valueClasses the valueClasses to set
     */
    public void setValueClasses(Class<?>[] valueClasses) {
        this.valueClasses = valueClasses;
    }

    /**
     * @return the valueClasses
     */
    public Class<?>[] getValueClasses() {
        return valueClasses;
    }

    /**
     * @param editHandler the editHandler to set
     */
    public void setEditHandler(BeanTableEditHandler editHandler) {
        this.editHandler = editHandler;
    }

    /**
     * @return the editHandler
     */
    public BeanTableEditHandler getEditHandler() {
        return editHandler;
    }

}