/****************************************************************************** * Product: Posterita Ajax UI * * Copyright (C) 2007 Posterita Ltd. All Rights Reserved. * * This program is free software; you can redistribute it and/or modify it * * under the terms version 2 of the GNU General Public License as published * * by the Free Software Foundation. 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 General Public License for more details. * * You should have received a copy of the GNU General Public License along * * with this program; if not, write to the Free Software Foundation, Inc., * * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. * * For the text or an alternative of this public license, you may reach us * * Posterita Ltd., 3, Draper Avenue, Quatre Bornes, Mauritius * * or via info@posterita.org or http://www.posterita.org/ * *****************************************************************************/ package org.adempiere.webui.component; import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import org.adempiere.webui.event.TableValueChangeEvent; import org.adempiere.webui.event.TableValueChangeListener; import org.adempiere.webui.event.WTableModelEvent; import org.adempiere.webui.event.WTableModelListener; import org.adempiere.webui.exception.ApplicationException; import org.compiere.minigrid.ColumnInfo; import org.compiere.minigrid.IDColumn; import org.compiere.minigrid.IMiniTable; import org.compiere.minigrid.MiniTable; import org.compiere.model.MRole; import org.compiere.model.PO; import org.compiere.util.CLogger; import org.compiere.util.Env; import org.compiere.util.KeyNamePair; import org.compiere.util.Util; import org.zkoss.zul.ListModel; /** * Replacement for the Swing client minigrid component * * ZK Listbox extension for Adempiere Web UI. * The listbox contains a model and a renderer. * The model holds the underlying data objects, while the * renderer deals with displaying the data objects. * The renderer will render data objects using a variety of components. * These components can then be edited if they are not readonly. * * @author Andrew Kimball * @author Sendy Yagambrum */ public class WListbox extends Listbox implements IMiniTable, TableValueChangeListener, WTableModelListener { /** * */ private static final long serialVersionUID = 8717707799347994189L; /** Logger. */ private static CLogger logger = CLogger.getCLogger(MiniTable.class); /** Model Index of Key Column. */ protected int m_keyColumnIndex = -1; /** List of R/W columns. */ private ArrayList<Integer> m_readWriteColumn = new ArrayList<Integer>(); // TODO this duplicates other info held on columns. Needs rationalising. /** Layout set in prepareTable and used in loadTable. */ private ColumnInfo[] m_layout = null; /** column class types (e.g. Boolean) */ private ArrayList<Class> m_modelHeaderClass = new ArrayList<Class>(); /** Color Column Index of Model. */ private int m_colorColumnIndex = -1; /** Color Column compare data. */ private Object m_colorDataCompare = Env.ZERO; /** * Default constructor. * * Sets a row renderer and an empty model */ public WListbox() { super(); WListItemRenderer rowRenderer = new WListItemRenderer(); rowRenderer.addTableValueChangeListener(this); setItemRenderer(rowRenderer); setModel(new ListModelTable()); } /** * Set the data model and column header names for the Listbox. * * @param model The data model to assign to the table * @param columnNames The names of the table columns */ public void setData(ListModelTable model, List< ? extends String> columnNames) { WListItemRenderer rowRenderer = null; if (columnNames != null && columnNames.size() > 0) { // instantiate our custom row renderer rowRenderer = new WListItemRenderer(columnNames); // add listener for listening to component changes rowRenderer.addTableValueChangeListener(this); } // assign the model and renderer this.setModel(model); if (rowRenderer != null) { getModel().setNoColumns(columnNames.size()); this.setItemRenderer(rowRenderer); //recreate listhead if needed ListHead head = super.getListHead(); if (head != null) { head.getChildren().clear(); rowRenderer.renderListHead(head); } } // re-render this.repaint(); return; } public void setModel(ListModel model) { super.setModel(model); if (model instanceof ListModelTable) { // TODO need to remove listener before adding, but how to do this without // causing ConcurrentModificationException //((ListModelTable)model).removeTableModelListener(this); ((ListModelTable)model).addTableModelListener(this); } } /** * Create the listbox header by fetching it from the renderer and adding * it to the Listbox. * */ private void initialiseHeader() { ListHead head = null; head = super.getListHead(); //init only once if (head != null) { return; } head = new ListHead(); // render list head if (this.getItemRenderer() instanceof WListItemRenderer) { ((WListItemRenderer)this.getItemRenderer()).renderListHead(head); } else { throw new ApplicationException("Rendering of the ListHead is unsupported for " + this.getItemRenderer().getClass().getSimpleName()); } //attach the listhead head.setParent(this); return; } /** * Is the cell at the specified row and column editable? * * @param row row index of cell * @param column column index of cell * @return true if cell is editable, false otherwise */ public boolean isCellEditable(int row, int column) { // if the first column holds a boolean and it is false, it is not editable if (column != 0 && (getValueAt(row, 0) instanceof Boolean) && !((Boolean)getValueAt(row, 0)).booleanValue()) { return false; } // is the column read/write? if (m_readWriteColumn.contains(new Integer(column))) { return true; } return false; } // isCellEditable /** * Returns the cell value at <code>row</code> and <code>column</code>. * <p> * <b>Note</b>: The column is specified in the table view's display * order, and not in the <code>TableModel</code>'s column * order. This is an important distinction because as the * user rearranges the columns in the table, * the column at a given index in the view will change. * Meanwhile the user's actions never affect the model's * column ordering. * * @param row the index of the row whose value is to be queried * @param column the index of the column whose value is to be queried * @return the Object at the specified cell */ public Object getValueAt(int row, int column) { return getModel().getDataAt(row, convertColumnIndexToModel(column)); } /** * Return the <code>ListModelTable</code> associated with this table. * * @return The <code>ListModelTable</code> associated with this table. */ public ListModelTable getModel() { if (super.getModel() instanceof ListModelTable) { return (ListModelTable)super.getModel(); } else { throw new IllegalArgumentException("Model must be instance of " + ListModelTable.class.getName()); } } /** * Set the cell value at <code>row</code> and <code>column</code>. * * @param value The value to set * @param row the index of the row whose value is to be set * @param column the index of the column whose value is to be set */ public void setValueAt(Object value, int row, int column) { getModel().setDataAt(value, row, convertColumnIndexToModel(column)); } /** * Convert the index for a column from the display index to the * corresponding index in the underlying model. * <p> * This is unused for this implementation because the column ordering * cannot be dynamically changed. * * @param viewColumnIndex the index of the column in the view * @return the index of the corresponding column in the model * * @see #convertColumnIndexToVi */ public int convertColumnIndexToModel(int viewColumnIndex) { return viewColumnIndex; } /** * Set Column at the specified <code>index</code> to read-only or read/write. * * @param index index of column to set as read-only (or not) * @param readOnly Read only value. If <code>true</code> column is read only, * if <code>false</code> column is read-write */ public void setColumnReadOnly (int index, boolean readOnly) { Integer indexObject = new Integer(index); // Column is ReadWrite if (m_readWriteColumn.contains(indexObject)) { // Remove from list if (readOnly) { m_readWriteColumn.remove(indexObject); } // ReadOnly } // current column is R/O - ReadWrite - add to list else if (!readOnly) { m_readWriteColumn.add(indexObject); } return; } // setColumnReadOnly /** * Prepare Table and return SQL required to get resultset to * populate table. * * @param layout array of column info * @param from SQL FROM content * @param where SQL WHERE content * @param multiSelection multiple selections * @param tableName table name * @return SQL statement to use to get resultset to populate table */ public String prepareTable(ColumnInfo[] layout, String from, String where, boolean multiSelection, String tableName) { return prepareTable(layout, from, where, multiSelection, tableName, true); } // prepareTable /** * Prepare Table and return SQL required to get resultset to * populate table * * @param layout array of column info * @param from SQL FROM content * @param where SQL WHERE content * @param multiSelection multiple selections * @param tableName multiple selections * @param addAccessSQL specifies whether to addAcessSQL * @return SQL statement to use to get resultset to populate table */ public String prepareTable(ColumnInfo[] layout, String from, String where, boolean multiSelection, String tableName,boolean addAccessSQL) { int columnIndex = 0; StringBuffer sql = new StringBuffer ("SELECT "); setLayout(layout); clearColumns(); setMultiSelection(multiSelection); // add columns & sql for (columnIndex = 0; columnIndex < layout.length; columnIndex++) { // create sql if (columnIndex > 0) { sql.append(", "); } sql.append(layout[columnIndex].getColSQL()); // adding ID column if (layout[columnIndex].isKeyPairCol()) { sql.append(",").append(layout[columnIndex].getKeyPairColSQL()); } // add to model addColumn(layout[columnIndex].getColHeader()); // set the colour column if (layout[columnIndex].isColorColumn()) { setColorColumn(columnIndex); } if (layout[columnIndex].getColClass() == IDColumn.class) { m_keyColumnIndex = columnIndex; } } // set editors (two steps) for (columnIndex = 0; columnIndex < layout.length; columnIndex++) { setColumnClass(columnIndex, layout[columnIndex].getColClass(), layout[columnIndex].isReadOnly(), layout[columnIndex].getColHeader()); } sql.append( " FROM ").append(from); sql.append(" WHERE ").append(where); if (from.length() == 0) { return sql.toString(); } // if (addAccessSQL) { String finalSQL = MRole.getDefault().addAccessSQL(sql.toString(), tableName, MRole.SQL_FULLYQUALIFIED, MRole.SQL_RO); logger.finest(finalSQL); return finalSQL; } else { return sql.toString(); } } // prepareTable /** * Clear the table columns from both the model and renderer */ private void clearColumns() { ((WListItemRenderer)getItemRenderer()).clearColumns(); getModel().setNoColumns(0); return; } /** * Add Table Column and specify the column header. * * @param header name of column header */ public void addColumn (String header) { WListItemRenderer renderer = (WListItemRenderer)getItemRenderer(); renderer.addColumn(Util.cleanAmp(header)); getModel().addColumn(); return; } // addColumn /** * Set the attributes of the column. * * @param index The index of the column to be modified * @param classType The class of data that the column will contain * @param readOnly Whether the data in the column is read only * @param header The header text for the column * * @see #setColumnClass(int, Class, boolean) */ public void setColumnClass (int index, Class classType, boolean readOnly, String header) { WListItemRenderer renderer = (WListItemRenderer)getItemRenderer(); setColumnReadOnly(index, readOnly); renderer.setColumnHeader(index, header); renderer.setColumnClass(index, classType); if (index < m_modelHeaderClass.size()) m_modelHeaderClass.set(index, classType); else m_modelHeaderClass.add(classType); return; } /** * Set the attributes of the column. * * @param index The index of the column to be modified * @param classType The class of data that the column will contain * @param readOnly Whether the data in the column is read only * * @see #setColumnClass(int, Class, boolean, String) */ public void setColumnClass (int index, Class classType, boolean readOnly) { setColumnReadOnly(index, readOnly); WListItemRenderer renderer = (WListItemRenderer)getItemRenderer(); renderer.setColumnClass(index, classType); m_modelHeaderClass.add(classType); return; } /** * Set the attributes of the column. * * @param classType The class of data that the column will contain * @param readOnly Whether the data in the column is read only * @param header The header text for the column * * @see #setColumnClass(int, Class, boolean) * @see #addColumn(String) */ public void addColumn(Class classType, boolean readOnly, String header) { m_modelHeaderClass.add(classType); setColumnReadOnly(m_modelHeaderClass.size() - 1, readOnly); addColumn(header); WListItemRenderer renderer = (WListItemRenderer)getItemRenderer(); renderer.setColumnClass((renderer.getNoColumns() - 1), classType); return; } /** * Set the Column to determine the color of the row (based on model index). * @param modelIndex the index of the column used to decide the colour */ public void setColorColumn (int modelIndex) { m_colorColumnIndex = modelIndex; } // setColorColumn /** * Load Table from ResultSet - The ResultSet is not closed. * * @param rs ResultSet containing data t enter int the table. * The contents must conform to the column layout defined in * {@link #prepareTable(ColumnInfo[], String, String, boolean, String)} */ public void loadTable(ResultSet rs) { int row = 0; // model row int col = 0; // model column Object data = null; int rsColIndex = 0; // index into result set int rsColOffset = 1; // result set columns start with 1 Class columnClass; // class of the column if (getLayout() == null) { throw new UnsupportedOperationException("Layout not defined"); } clearTable(); try { while (rs.next()) { row = getItemCount(); setRowCount(row + 1); rsColOffset = 1; for (col = 0; col < m_layout.length; col++) { //reset the data value data=null; columnClass = m_layout[col].getColClass(); rsColIndex = col + rsColOffset; if (isColumnClassMismatch(col, columnClass)) { throw new ApplicationException("Cannot enter a " + columnClass.getName() + " in column " + col + ". " + "An object of type " + m_modelHeaderClass.get(col).getSimpleName() + " was expected."); } if (columnClass == IDColumn.class) { data = new IDColumn(rs.getInt(rsColIndex)); } else if (columnClass == Boolean.class) { data = new Boolean(rs.getString(rsColIndex).equals("Y")); } else if (columnClass == Timestamp.class) { data = rs.getTimestamp(rsColIndex); } else if (columnClass == BigDecimal.class) { data = rs.getBigDecimal(rsColIndex); } else if (columnClass == Double.class) { data = new Double(rs.getDouble(rsColIndex)); } else if (columnClass == Integer.class) { data = new Integer(rs.getInt(rsColIndex)); } else if (columnClass == KeyNamePair.class) { // TODO factor out this generation String display = rs.getString(rsColIndex); int key = rs.getInt(rsColIndex + 1); data = new KeyNamePair(key, display); rsColOffset++; } else { // TODO factor out this cleanup String s = rs.getString(rsColIndex); if (s != null) { data = s.trim(); // problems with NCHAR } else { data=null; } } // store in underlying model getModel().setDataAt(data, row, col); } } } catch (SQLException exception) { logger.log(Level.SEVERE, "", exception); } // TODO implement this //autoSize(); // repaint the table this.repaint(); logger.config("Row(rs)=" + getRowCount()); return; } // loadTable /** * @param col * @param columnClass * @return */ private boolean isColumnClassMismatch(int col, Class columnClass) { return !columnClass.equals(m_modelHeaderClass.get(col)); } /** * Load Table from Object Array. * @param pos array of Persistent Objects */ public void loadTable(PO[] pos) { int row = 0; int col = 0; int poIndex = 0; // index into the PO array String columnName; Object data; Class columnClass; if (m_layout == null) { throw new UnsupportedOperationException("Layout not defined"); } // Clear Table clearTable(); for (poIndex = 0; poIndex < pos.length; poIndex++) { PO myPO = pos[poIndex]; row = getRowCount(); setRowCount(row + 1); for (col = 0; col < m_layout.length; col++) { columnName = m_layout[col].getColSQL(); data = myPO.get_Value(columnName); if (data != null) { columnClass = m_layout[col].getColClass(); if (isColumnClassMismatch(col, columnClass)) { throw new ApplicationException("Cannot enter a " + columnClass.getName() + " in column " + col + ". " + "An object of type " + m_modelHeaderClass.get(col).getSimpleName() + " was expected."); } if (columnClass == IDColumn.class) { data = new IDColumn(((Integer)data).intValue()); } else if (columnClass == Double.class) { data = new Double(((BigDecimal)data).doubleValue()); } } // store getModel().setDataAt(data, row, col); } } // TODO implement this //autoSize(); // repaint the table this.repaint(); logger.config("Row(array)=" + getRowCount()); return; } // loadTable /** * Clear the table components. */ public void clear() { this.getChildren().clear(); } /** * Get the key of currently selected row based on layout defined in * {@link #prepareTable(ColumnInfo[], String, String, boolean, String)}. * * @return ID if key */ public Integer getSelectedRowKey() { int row = 0; final int noSelection = -1; final int noIndex = -1; Object data; if (m_layout == null) { throw new UnsupportedOperationException("Layout not defined"); } row = getSelectedRow(); // TODO factor out the two parts of this guard statement if (row != noSelection && m_keyColumnIndex != noIndex) { data = getModel().getDataAt(row, m_keyColumnIndex); if (data instanceof IDColumn) { data = ((IDColumn)data).getRecord_ID(); } if (data instanceof Integer) { return (Integer)data; } } return null; } // getSelectedRowKey /** * Returns the index of the first selected row, -1 if no row is selected. * * @return the index of the first selected row */ public int getSelectedRow() { return this.getSelectedIndex(); } /** * Set the size of the underlying data model. * * @param rowCount number of rows */ public void setRowCount (int rowCount) { getModel().setNoRows(rowCount); return; } // setRowCount /** * Get Layout. * * @return Array of ColumnInfo */ public ColumnInfo[] getLayoutInfo() { return getLayout(); } // getLayout /** * Removes all data stored in the underlying model. * */ public void clearTable() { WListItemRenderer renderer = null; // First clear the model getModel().clear(); // Then the renderer if (getItemRenderer() instanceof WListItemRenderer) { renderer = (WListItemRenderer)getItemRenderer(); renderer.clearSelection(); } else { throw new IllegalArgumentException("Renderer must be instance of WListItemRenderer"); } return; } /** * Get the number of rows in this table's model. * * @return the number of rows in this table's model * */ public int getRowCount() { return getModel().getSize(); } /** * Set whether or not multiple rows can be selected. * * @param multiSelection are multiple selections allowed */ public void setMultiSelection(boolean multiSelection) { this.setMultiple(multiSelection); return; } // setMultiSelection /** * Query whether multiple rows can be selected in the table. * * @return true if multiple rows can be selected */ public boolean isMultiSelection() { return this.isMultiple(); } // isMultiSelection /** * Set ColorColumn comparison criteria. * * @param dataCompare object encapsualating comparison criteria */ public void setColorCompare (Object dataCompare) { m_colorDataCompare = dataCompare; return; } // /** * Get ColorCode for Row. * <pre> * If numerical value in compare column is * negative = -1, * positive = 1, * otherwise = 0 * If Timestamp * </pre> * @param row row * @return color code */ public int getColorCode (int row) { // TODO expose these through interface // (i.e. make public class member variables) final int valPositive = 1; final int valNegative = -1; final int valOtherwise = 0; Object data; int cmp = valOtherwise; if (m_colorColumnIndex == -1) { return valOtherwise; } data = getModel().getDataAt(row, m_colorColumnIndex); // We need to have a Number if (data == null) { return valOtherwise; } try { if (data instanceof Timestamp) { if ((m_colorDataCompare == null) || !(m_colorDataCompare instanceof Timestamp)) { m_colorDataCompare = new Timestamp(System.currentTimeMillis()); } cmp = ((Timestamp)m_colorDataCompare).compareTo((Timestamp)data); } else { if ((m_colorDataCompare == null) || !(m_colorDataCompare instanceof BigDecimal)) { m_colorDataCompare = Env.ZERO; } if (!(data instanceof BigDecimal)) { data = new BigDecimal(data.toString()); } cmp = ((BigDecimal)m_colorDataCompare).compareTo((BigDecimal)data); } } catch (Exception exception) { return valOtherwise; } if (cmp > 0) { return valNegative; } else if (cmp < 0) { return valPositive; } return valOtherwise; } // getColorCode /* (non-Javadoc) * @see org.adempiere.webui.event.TableValueChangeListener#tableValueChange * (org.adempiere.webui.event.TableValueChangeEvent) */ public void tableValueChange(TableValueChangeEvent event) { int col = event.getColumn(); // column of table field which caused the event int row = event.getRow(); // row of table field which caused the event boolean newBoolean; IDColumn idColumn; // if the event was caused by an ID Column and the value is a boolean // then set the IDColumn's select field if (col >= 0 && row >=0) { if (this.getValueAt(row, col) instanceof IDColumn && event.getNewValue() instanceof Boolean) { newBoolean = ((Boolean)event.getNewValue()).booleanValue(); idColumn = (IDColumn)this.getValueAt(row, col); idColumn.setSelected(newBoolean); this.setValueAt(idColumn, row, col); } // othewise just set the value in the model to the new value else { this.setValueAt(event.getNewValue(), row, col); } } return; } /** * Repaint the Table. */ public void repaint() { // create the head initialiseHeader(); // this causes re-rendering of the Listbox this.setModel(this.getModel()); return; } /** * Get the table layout. * * @return the layout of the table * @see #setLayout(ColumnInfo[]) */ public ColumnInfo[] getLayout() { return m_layout; } /** * Set the column information for the table. * * @param layout The new layout to set for the table * @see #getLayout() */ private void setLayout(ColumnInfo[] layout) { this.m_layout = layout; getModel().setNoColumns(m_layout.length); return; } /** * Respond to a change in the table's model. * * If the event indicates that the entire table has changed, the table is repainted. * * @param event The event fired to indicate a change in the table's model */ public void tableChanged(WTableModelEvent event) { if ((event.getType() == WTableModelEvent.CONTENTS_CHANGED) && (event.getColumn() == WTableModelEvent.ALL_COLUMNS) && (event.getFirstRow() == WTableModelEvent.ALL_ROWS)) { this.repaint(); } else if ((event.getType() == WTableModelEvent.CONTENTS_CHANGED) && event.getFirstRow() != WTableModelEvent.ALL_ROWS && !m_readWriteColumn.isEmpty()) { int[] indices = this.getSelectedIndices(); ListModelTable model = this.getModel(); if (event.getLastRow() > event.getFirstRow()) model.updateComponent(event.getFirstRow(), event.getLastRow()); else model.updateComponent(event.getFirstRow()); if (indices != null && indices.length > 0) { this.setSelectedIndices(indices); } } return; } /** * no op, to ease porting of swing form */ public void autoSize() { //no op } public int getColumnCount() { return getModel() != null ? getModel().getNoColumns() : 0; } public int getKeyColumnIndex() { return m_keyColumnIndex; } }