/****************************************************************************** * Product: Adempiere ERP & CRM Smart Business Solution * * Copyright (C) 1999-2006 ComPiere, Inc. 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 * * ComPiere, Inc., 2620 Augustine Dr. #245, Santa Clara, CA 95054, USA * * or via info@compiere.org or http://www.compiere.org/license.html * *****************************************************************************/ package org.compiere.model; import java.beans.PropertyChangeEvent; import java.beans.PropertyVetoException; import java.beans.VetoableChangeListener; import java.beans.VetoableChangeSupport; import java.io.Serializable; import java.math.BigDecimal; import java.sql.Blob; import java.sql.Clob; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.logging.Level; import javax.swing.event.TableModelListener; import javax.swing.table.AbstractTableModel; import org.adempiere.exceptions.AdempiereException; import org.adempiere.exceptions.DBException; import org.adempiere.util.ServerContext; import org.compiere.Adempiere; import org.compiere.util.CLogger; import org.compiere.util.DB; import org.compiere.util.DisplayType; import org.compiere.util.Env; import org.compiere.util.MSort; import org.compiere.util.Msg; import org.compiere.util.SecureEngine; import org.compiere.util.Trx; import org.compiere.util.Util; import org.compiere.util.ValueNamePair; /** * Grid Table Model for JDBC table access, including buffering. *
 *		The following data types are handled
 *			Integer		for all IDs
 *			BigDecimal	for all Numbers
 *			Timestamp	for all Dates
 *			String		for all others
 *  The data is read via r/o resultset and cached in m_buffer. Writes/updates
 *  are via dynamically constructed SQL INSERT/UPDATE statements. The record
 *  is re-read via the resultset to get results of triggers.
 *
 *  
* The model maintains and fires the requires TableModelEvent changes, * the DataChanged events (loading, changed, etc.) as well as Vetoable Change event "RowChange" * (for row changes initiated by moving the row in the table grid). * * @author Jorg Janke * @version $Id: GridTable.java,v 1.9 2006/08/09 16:38:25 jjanke Exp $ * * @author Teo Sarca, SC ARHIPAC SERVICE SRL *
  • BF [ 1901192 ] LogMigrationScripts: GridTable.dataSave: manual update *
  • BF [ 1943682 ] Copy Record should not copy IsApproved and IsGenerated *
  • BF [ 1949543 ] Window freeze if there is a severe exception *
  • BF [ 1984310 ] GridTable.getClientOrg() doesn't work for AD_Client/AD_Org * @author victor.perez@e-evolution.com,www.e-evolution.com *
  • BF [ 2910358 ] Error in context when a field is found in different tabs. * https://sourceforge.net/p/adempiere/bugs/2255/ *
  • BF [ 2910368 ] Error in context when IsActive field is found in different * https://sourceforge.net/p/adempiere/bugs/2256/ */ public class GridTable extends AbstractTableModel implements Serializable { /** * */ private static final long serialVersionUID = 3948220810042370826L; protected static final String SORTED_DSE_EVENT = "Sorted"; public static final int DEFAULT_GRIDTABLE_LOAD_TIMEOUT_IN_SECONDS = 30; public static final int DEFAULT_GRIDTABLE_COUNT_TIMEOUT_IN_SECONDS = 1; public static final String LOAD_TIMEOUT_ERROR_MESSAGE = "GridTabLoadTimeoutError"; public static final String DATA_REFRESH_MESSAGE = "Refreshed"; public static final String DATA_UPDATE_COPIED_MESSAGE = "UpdateCopied"; public static final String DATA_INSERTED_MESSAGE = "Inserted"; public static final String DATA_IGNORED_MESSAGE = "Ignored"; public static final String DATA_SAVED_MESSAGE = "Saved"; /** * JDBC Based Buffered Table * * @param ctx Properties * @param AD_Table_ID table id * @param TableName table name * @param WindowNo window no * @param TabNo tab no * @param withAccessControl if true adds AD_Client/Org restrictions */ public GridTable(Properties ctx, int AD_Table_ID, String TableName, int WindowNo, int TabNo, boolean withAccessControl) { this(ctx, AD_Table_ID, TableName, WindowNo, TabNo, withAccessControl, false); } /** * JDBC Based Buffered Table * * @param ctx Properties * @param AD_Table_ID table id * @param TableName table name * @param WindowNo window no * @param TabNo tab no * @param withAccessControl if true adds AD_Client/Org restrictuins * @param virtual use virtual table mode if table is mark as high volume */ public GridTable(Properties ctx, int AD_Table_ID, String TableName, int WindowNo, int TabNo, boolean withAccessControl, boolean virtual) { super(); log.info(TableName); m_ctx = ctx; m_AD_Table_ID = AD_Table_ID; setTableName(TableName); m_WindowNo = WindowNo; m_TabNo = TabNo; m_withAccessControl = withAccessControl; m_virtual = virtual; } // MTable private static CLogger log = CLogger.getCLogger(GridTable.class); private Properties m_ctx; private int m_AD_Table_ID; private String m_tableName = ""; private int m_WindowNo; /** Tab No 0.. */ private int m_TabNo; private boolean m_withAccessControl; private boolean m_readOnly = true; private boolean m_deleteable = true; //virtual table state variables private boolean m_virtual; public static final String CTX_KeyColumnName = "KeyColumnName"; // /** Rowcount */ private int m_rowCount = 0; private boolean m_rowCountTimeout = false; private boolean m_rowLoadTimeout = false; /** Has Data changed? */ private boolean m_changed = false; /** Index of changed row via SetValueAt */ private int m_rowChanged = -1; /** Insert mode active */ private boolean m_inserting = false; /** Inserted Row number */ private int m_newRow = -1; /** Is the Resultset open? */ private boolean m_open = false; /** Compare to DB before save */ @Deprecated private boolean m_compareDB = true; // set to true after every save /** Data buffer */ private volatile ArrayList m_buffer = new ArrayList(100); /** Sort order for {@link #m_buffer} */ private volatile ArrayList m_sort = new ArrayList(100); /** Record ID:Values */ private volatile Map m_virtualBuffer = new HashMap(100); /** Original row data */ private Object[] m_rowData = null; /** Original data [row,col,data] */ private Object[] m_oldValue = null; // private Loader m_loader = null; /** Columns */ private ArrayList m_fields = new ArrayList(30); private ArrayList m_parameterSELECT = new ArrayList(5); private ArrayList m_parameterWHERE = new ArrayList(5); /** Complete SQL statement */ private String m_SQL; /** SQL Statement for Row Count */ private String m_SQL_Count; /** The SELECT clause with FROM */ private String m_SQL_Select; /** The static where clause */ private String m_whereClause = "2=3"; /** Show only Processed='N' and last 24h records */ private boolean m_onlyCurrentRows = false; /** Show only Not processed and x days */ private int m_onlyCurrentDays = 1; /** Static ORDER BY clause */ private String m_orderClause = ""; /** Max Rows to query or 0 for all */ private int m_maxRows = 0; /** Index of Key Column */ private int m_indexKeyColumn = -1; /** Index of UUID Column */ private int m_indexUUIDColumn = -1; /** Index of Color Column */ private int m_indexColorColumn = -1; /** Index of Processed Column */ private int m_indexProcessedColumn = -1; /** Index of IsActive Column */ private int m_indexActiveColumn = -1; /** Index of AD_Client_ID Column */ private int m_indexClientColumn = -1; /** Index of AD_Org_ID Column */ private int m_indexOrgColumn = -1; /** Vetoable Change Bean support */ private VetoableChangeSupport m_vetoableChangeSupport = new VetoableChangeSupport(this); private Future m_loaderFuture; /** Property of Vetoable Bean support "RowChange" */ public static final String PROPERTY = "MTable-RowSave"; private final static Integer NEW_ROW_ID = Integer.valueOf(-1); private static final int DEFAULT_FETCH_SIZE = 200; /** Keep track of last sorted column index and sort direction */ private int m_lastSortColumnIndex = -1; private boolean m_lastSortedAscending = true; /** * Set Table Name * @param newTableName table name */ public void setTableName(String newTableName) { if (m_open) { log.log(Level.SEVERE, "Table already open - ignored"); return; } if (newTableName == null || newTableName.length() == 0) return; m_tableName = newTableName; } // setTableName /** * Get Table Name * @return table name */ public String getTableName() { return m_tableName; } // getTableName /** * Set Where Clause (w/o the WHERE keyword and w/o History). * @param newWhereClause sql where clause * @param onlyCurrentRows only current rows * @param onlyCurrentDays how many days back for current * @return true if where clase set */ public boolean setSelectWhereClause(String newWhereClause, boolean onlyCurrentRows, int onlyCurrentDays) { if (m_open) { log.log(Level.SEVERE, "Table already open - ignored"); return false; } // m_whereClause = newWhereClause; m_onlyCurrentRows = onlyCurrentRows; m_onlyCurrentDays = onlyCurrentDays; if (m_whereClause == null) m_whereClause = ""; return true; } // setWhereClause /** * Get Where Clause (w/o the WHERE keyword and w/o History) * @return where clause */ public String getSelectWhereClause() { return m_whereClause; } // getWhereClause /** * Is History displayed * @return true if history displayed */ public boolean isOnlyCurrentRowsDisplayed() { return !m_onlyCurrentRows; } // isHistoryDisplayed /** * Set Order Clause (w/o the ORDER BY keyword) * @param newOrderClause sql order by clause */ public void setOrderClause(String newOrderClause) { m_orderClause = newOrderClause; if (m_orderClause == null) m_orderClause = ""; } // setOrderClause /** * Get Order By Clause (w/o the ORDER BY keyword) * @return order by clause */ public String getOrderClause() { return m_orderClause; } // getOrderClause /** * Assemble & store {@link #m_SQL}, {@link #m_SQL_Select} and {@link #m_SQL_Count}. * @return m_SQL */ private String createSelectSql() { if (m_fields.size() == 0 || m_tableName == null || m_tableName.equals("")) return ""; // Create SELECT Part StringBuilder select = new StringBuilder("SELECT "); for (int i = 0; i < m_fields.size(); i++) { if (i > 0) select.append(","); GridField field = (GridField)m_fields.get(i); select.append(field.isVirtualColumn() ? field.getColumnSQL(true) : DB.getDatabase().quoteColumnName(field.getColumnSQL(true))); // ColumnName or Virtual Column } // select.append(" FROM ").append(m_tableName); m_SQL_Select = select.toString(); m_SQL_Count = "SELECT COUNT(*) FROM " + m_tableName; //BF [ 2910358 ] //Restore the Original Value for Key Column Name based in Tab Context Value int parentTabNo = getParentTabNo(); String parentKey = Env.getContext(m_ctx, m_WindowNo, parentTabNo, GridTab.CTX_KeyColumnName, true); String valueKey = null; String currKey = null; if (parentKey != null && parentKey.length() > 0) { valueKey = Env.getContext(m_ctx, m_WindowNo, parentTabNo, parentKey, true); currKey = Env.getContext(m_ctx, m_WindowNo, parentKey); if (currKey == null) currKey = new String(""); if (valueKey != null && valueKey.length() > 0 && parentKey != null && parentKey.length() > 0 && ! currKey.equals(valueKey)) { Env.setContext(m_ctx, m_WindowNo, parentKey, valueKey); } } StringBuilder where = new StringBuilder(""); // WHERE if (m_whereClause.length() > 0) { where.append(" WHERE ("); if (m_whereClause.indexOf('@') == -1) where.append(m_whereClause); else // replace variables { String context = Env.parseContext(m_ctx, m_WindowNo, m_whereClause, false); if(context != null && context.trim().length() > 0) { where.append(context); } else { log.log(Level.WARNING, "Failed to parse where clause. whereClause="+m_whereClause); where.append(" 1 = 2 "); } } where.append(")"); } if (m_onlyCurrentRows && m_TabNo == 0) { if (where.toString().indexOf(" WHERE ") == -1) where.append(" WHERE "); else where.append(" AND "); // Show only unprocessed or the one updated within x days where.append("(Processed='N' OR Updated>"); where.append("getDate()-1"); where.append(")"); } // RO/RW Access m_SQL = m_SQL_Select + where.toString(); m_SQL_Count += where.toString(); if (m_withAccessControl) { m_SQL = MRole.getDefault(m_ctx, false).addAccessSQL(m_SQL, m_tableName, MRole.SQL_FULLYQUALIFIED, MRole.SQL_RO); m_SQL_Count = MRole.getDefault(m_ctx, false).addAccessSQL(m_SQL_Count, m_tableName, MRole.SQL_FULLYQUALIFIED, MRole.SQL_RO); } // ORDER BY if (!m_orderClause.equals("")) { m_SQL += " ORDER BY " + m_orderClause; } //IDEMPIERE-5193 Add Limit to Query if(m_maxRows > 0 && DB.getDatabase().isPagingSupported()) { // set to maxRows plus one to trigger FindOverMax on overflow m_SQL = DB.getDatabase().addPagingSQL(m_SQL, 1, m_maxRows+1); } // if (log.isLoggable(Level.FINE)) log.fine(m_SQL_Count); Env.setContext(m_ctx, m_WindowNo, m_TabNo, GridTab.CTX_SQL, m_SQL); return m_SQL; } // createSelectSql /** * Add Field to Table * @param field field */ public void addField (GridField field) { if (log.isLoggable(Level.FINE)) log.fine("(" + m_tableName + ") - " + field.getColumnName()); if (m_open) { log.log(Level.SEVERE, "Table already open - ignored: " + field.getColumnName()); return; } if (!MRole.getDefault(m_ctx, false).isColumnAccess (m_AD_Table_ID, field.getAD_Column_ID(), true)) { if (log.isLoggable(Level.FINE)) log.fine("No Column Access " + field.getColumnName()); return; } // Set Index for Key column if (field.isKey()) m_indexKeyColumn = m_fields.size(); else if (field.isUUID()) m_indexUUIDColumn = m_fields.size(); // Set Index of other standard columns if (field.getColumnName().equals("IsActive")) m_indexActiveColumn = m_fields.size(); else if (field.getColumnName().equals("Processed")) m_indexProcessedColumn = m_fields.size(); else if (field.getColumnName().equals("AD_Client_ID")) m_indexClientColumn = m_fields.size(); else if (field.getColumnName().equals("AD_Org_ID")) m_indexOrgColumn = m_fields.size(); // m_fields.add(field); } // addColumn /** * Returns database column name * * @param index column index * @return column name or empty string (if index is invalid) */ public String getColumnName (int index) { if (index < 0 || index > m_fields.size()) { log.log(Level.SEVERE, "Invalid index=" + index); return ""; } // GridField field = (GridField)m_fields.get(index); return field.getColumnName(); } // getColumnName /** * Find column index via column name. * * @param columnName string containing name of column to be located * @return the column index with columnName, or -1 if not found */ public int findColumn (String columnName) { for (int i = 0; i < m_fields.size(); i++) { GridField field = (GridField)m_fields.get(i); if (columnName.equals(field.getColumnName())) return i; } return -1; } // findColumn /** * Returns Class of database column/field * * @param index column index * @return Java class for column */ public Class getColumnClass (int index) { if (index < 0 || index >= m_fields.size()) { log.log(Level.SEVERE, "Invalid index=" + index); return null; } GridField field = (GridField)m_fields.get(index); return DisplayType.getClass(field.getDisplayType(), false); } // getColumnClass /** * Set Select Clause Parameter. * Assumes that you set parameters starting from index zero * @param index index * @param parameter parameter */ public void setParameterSELECT (int index, Object parameter) { if (index >= m_parameterSELECT.size()) m_parameterSELECT.add(parameter); else m_parameterSELECT.set(index, parameter); } // setParameterSELECT /** * Set Where Clause Parameter. * Assumes that you set parameters starting from index zero * @param index index * @param parameter parameter */ public void setParameterWHERE (int index, Object parameter) { if (index >= m_parameterWHERE.size()) m_parameterWHERE.add(parameter); else m_parameterWHERE.set(index, parameter); } // setParameterWHERE /** * Get GridField at index * @param index index * @return GridField */ public GridField getField (int index) { if (index < 0 || index >= m_fields.size()) return null; return (GridField)m_fields.get(index); } // getField /** * Get GridField with ColumnName * @param identifier column name * @return GridField */ protected GridField getField (String identifier) { if (identifier == null || identifier.length() == 0) return null; int cols = m_fields.size(); for (int i = 0; i < cols; i++) { GridField field = (GridField)m_fields.get(i); if (identifier.equalsIgnoreCase(field.getColumnName())) return field; } return null; } // getField /** * Get all Fields * @return GridField[] */ public GridField[] getFields () { GridField[] retValue = new GridField[m_fields.size()]; m_fields.toArray(retValue); return retValue; } // getField /** * Open connection to db and load data from table. * If already opened, data is refreshed.
    * Loading of data is perform asynchronously in background thread. * @param maxRows maximum number of rows or 0 for all * @return true if success */ public boolean open (int maxRows) { if (log.isLoggable(Level.INFO)) log.info("MaxRows=" + maxRows); m_maxRows = maxRows; if (m_open) { if (log.isLoggable(Level.FINE)) log.fine("already open"); dataRefreshAll(); return true; } if (m_virtual) { verifyVirtual(); } // create m_SQL and m_countSQL createSelectSql(); if (m_SQL == null || m_SQL.equals("")) { log.log(Level.SEVERE, "No SQL"); return false; } if (!m_open) { m_open = true; } // Start Loading m_loader = new Loader(); m_loaderFuture = null; m_rowCount = m_loader.open(maxRows); if (m_virtual) { m_buffer = null; m_virtualBuffer = new HashMap(210); } else { m_buffer = new ArrayList(m_rowCount+10); } m_sort = new ArrayList(m_rowCount+10); // actual row count or timeout if (m_rowCount > 0 || m_rowCountTimeout) { m_loader.setContext(ServerContext.getCurrentInstance()); m_loaderFuture = Adempiere.getThreadPoolExecutor().submit(m_loader); } else m_loader.close(); // m_changed = false; m_rowChanged = -1; m_inserting = false; return true; } // open /** * Verify whether use of virtual buffer is supported */ private void verifyVirtual() { if (m_indexKeyColumn == -1) { m_virtual = false; return; } GridField[] fields = getFields(); for(int i = 0; i < fields.length; i++) { if (fields[i].isKey() && i != m_indexKeyColumn) { m_virtual = false; return; } } } /** * Wait until asynchronous loading of Table and Lookup Fields is complete. */ public void loadComplete() { // Wait for loader if (m_loaderFuture != null) { if (!m_loaderFuture.isDone()) { try { m_loaderFuture.get(); } catch (Exception ie) { log.log(Level.SEVERE, "Interrupted", ie); } } } // wait for field lookup loaders for (int i = 0; i < m_fields.size(); i++) { GridField field = (GridField)m_fields.get(i); field.lookupLoadComplete(); } } // loadComplete /** * Is Loading * @return true if loading is in progress */ public boolean isLoading() { if (m_loaderFuture != null && !m_loaderFuture.isDone()) return true; return false; } // isLoading /** * wait for the loading of data * @param timeout timeout in milisecond. pass 0 or negative value for infinite wait * @throws InterruptedException * @throws ExecutionException * @throws TimeoutException */ public void waitLoading(long timeout) throws InterruptedException, ExecutionException, TimeoutException { if (m_loaderFuture != null && !m_loaderFuture.isDone()) { if (timeout > 0) m_loaderFuture.get(timeout, TimeUnit.MILLISECONDS); else m_loaderFuture.get(); } } /** * Is it open? * @return true if opened */ public boolean isOpen() { return m_open; } // isOpen /** * Close Resultset * @param finalCall true for final call and perform clean up */ public void close (boolean finalCall) { if (!m_open) return; if (log.isLoggable(Level.FINE)) log.fine("final=" + finalCall); // remove listeners if (finalCall) { DataStatusListener evl[] = (DataStatusListener[])listenerList.getListeners(DataStatusListener.class); for (int i = 0; i < evl.length; i++) listenerList.remove(DataStatusListener.class, evl[i]); TableModelListener ev2[] = (TableModelListener[])listenerList.getListeners(TableModelListener.class); for (int i = 0; i < ev2.length; i++) listenerList.remove(TableModelListener.class, ev2[i]); VetoableChangeListener vcl[] = m_vetoableChangeSupport.getVetoableChangeListeners(); for (int i = 0; i < vcl.length; i++) m_vetoableChangeSupport.removeVetoableChangeListener(vcl[i]); } // Stop loader while (m_loaderFuture != null && !m_loaderFuture.isDone()) { if (log.isLoggable(Level.FINE)) log.fine("Interrupting Loader ..."); m_loaderFuture.cancel(true); try { Thread.sleep(200); // .2 second } catch (InterruptedException ie) {} m_loaderFuture = null; } if (!m_inserting) dataSave(false); // not manual if (m_buffer != null) { m_buffer.clear(); } if (m_sort != null) { m_sort.clear(); } if (m_virtualBuffer != null) { m_virtualBuffer.clear(); } if (finalCall) { dispose(); m_buffer = null; m_sort = null; m_virtualBuffer = null; } // Fields are disposed from MTab if (log.isLoggable(Level.FINE)) log.fine(""); m_open = false; } // close /** * Clean up. * Called by close-final. */ private void dispose() { // GridFields for (int i = 0; i < m_fields.size(); i++) ((GridField)m_fields.get(i)).dispose(); m_fields.clear(); m_fields = null; // m_vetoableChangeSupport = null; // m_parameterSELECT.clear(); m_parameterSELECT = null; m_parameterWHERE.clear(); m_parameterWHERE = null; // clear data arrays m_buffer = null; m_virtualBuffer = null; m_sort = null; m_rowData = null; m_oldValue = null; m_loader = null; m_loaderFuture = null; } // dispose /** * Get column count * @return column count */ public int getColumnCount() { return m_fields.size(); } // getColumnCount /** * Get field count * @return field count */ public int getFieldCount() { return m_fields.size(); } // getFieldCount /** * Return number of rows * @return Number of rows or 0 if not opened */ public int getRowCount() { return m_rowCount; } // getRowCount /** * Set the Column to determine the color of the row * @param columnName column name */ public void setColorColumn (String columnName) { m_indexColorColumn = findColumn(columnName); } // setColorColumn /** * Get ColorCode for Row. *
    	 *	If numerical value in compare column is
    	 *		negative = -1,
    	 *      positive = 1,
    	 *      otherwise = 0
    	 *  
    * @see #setColorColumn * @param row row * @return color code */ public int getColorCode (int row) { if (m_indexColorColumn == -1) return 0; Object data = getValueAt(row, m_indexColorColumn); // We need to have a Number if (data == null || !(data instanceof BigDecimal)) return 0; BigDecimal bd = (BigDecimal)data; return bd.signum(); } // getColorCode /** * Sort records by Column. *

    * Actually the rows are not sorted, just the access pointer ArrayList * with the same size as m_buffer ({@link #m_sort}). * @param col col * @param ascending ascending */ public void sort (int col, boolean ascending) { if (log.isLoggable(Level.INFO)) log.info("#" + col + " " + ascending); if (col < 0) { return; } if (getRowCount() == 0) return; GridField field = getField(col); // Ignoring new record while sorting if (field.getGridTab().isQuickForm()) dataIgnore(); boolean isSameSortEntries = (col == m_lastSortColumnIndex && ascending == m_lastSortedAscending); if (!isSameSortEntries) { m_lastSortColumnIndex = col; m_lastSortedAscending = ascending; } //cache changed row MSort changedRow = m_rowChanged >= 0 ? (MSort)m_sort.get(m_rowChanged) : null; if (m_rowChanged == m_newRow) changedRow = null; Object[] changedRowData = changedRow != null ? getDataAtRow(m_rowChanged) : null; MSort newRow = m_newRow >= 0 ? (MSort)m_sort.get(m_newRow) : null; MSort currentRow = m_currentRow >= 0 && m_currentRow < m_sort.size() ? (MSort)m_sort.get(m_currentRow) : null; // RowIDs are not sorted if (field.getDisplayType() == DisplayType.RowID) return; boolean isLookup = DisplayType.isLookup(field.getDisplayType()); boolean isASI = DisplayType.PAttribute == field.getDisplayType(); // fill MSort entities with data entity for (int i = 0; i < m_sort.size(); i++) { MSort sort = (MSort)m_sort.get(i); Object[] rowData = getDataAtRow(i); if (rowData[col] == null) sort.data = null; else if (isLookup || isASI) sort.data = field.getLookup().getDisplay(rowData[col]); // lookup else sort.data = rowData[col]; // data } if (log.isLoggable(Level.INFO)) log.info(field.toString() + " #" + m_sort.size()); // sort it MSort sort = new MSort(0, null); sort.setSortAsc(ascending); Collections.sort(m_sort, sort); if (m_virtual) { Object[] newRowData = newRow != null ? m_virtualBuffer.get(NEW_ROW_ID) : null; m_virtualBuffer.clear(); if (newRow != null) m_virtualBuffer.put(NEW_ROW_ID, newRowData); if (changedRow != null) { for(int i = 0; i < m_sort.size(); i++) { if (m_sort.get(i) == changedRow) { m_rowChanged = i; m_virtualBuffer.put(changedRow.index, changedRowData); break; } } } //release sort memory for (int i = 0; i < m_sort.size(); i++) { m_sort.get(i).data = null; if (newRow != null && m_sort.get(i) == newRow) { if (m_rowChanged == m_newRow) m_rowChanged = i; m_newRow = i; } if (currentRow != null && m_sort.get(i) == currentRow) m_currentRow = i; } } else { for (int i = 0; i < m_sort.size(); i++) { if (newRow != null && m_sort.get(i) == newRow) { if (m_rowChanged == m_newRow) m_rowChanged = i; m_newRow = i; } if (currentRow != null && m_sort.get(i) == currentRow) m_currentRow = i; } } if (!isSameSortEntries) { // Info detected by MTab.dataStatusChanged and current row set to 0 fireDataStatusIEvent(SORTED_DSE_EVENT, "#" + m_sort.size()); // update UI fireTableDataChanged(); } } // sort /** * Get Key ID or -1 of none * @param row row index * @return ID or -1 */ public int getKeyID (int row) { if (m_indexKeyColumn != -1) { try { Integer ii = (Integer)getValueAt(row, m_indexKeyColumn); if (ii == null) return -1; return ii.intValue(); } catch (Exception e) // Alpha Key { return -1; } } return -1; } // getKeyID /** * Get Key UUID or null of none * @param row row index * @return UUID or null */ public String getKeyUUID (int row) { if (m_indexUUIDColumn != -1) { try { String ii = (String)getValueAt(row, m_indexUUIDColumn); if (ii == null) return null; return ii; } catch (Exception e) { return null; } } return null; } // getKeyUUID /** * Get UUID or null of none * @param row row index * @return UUID or null */ public UUID getUUID (int row) { String keyUUID = getKeyUUID(row); if (keyUUID != null) return UUID.fromString(keyUUID); return null; } // getUUID /** * Get Key ColumnName * @return key column name */ public String getKeyColumnName() { if (m_indexKeyColumn != -1) return getColumnName(m_indexKeyColumn); return ""; } // getKeyColumnName /** * Get Value at row and column * @param row row * @param col col * @return Value at row/column */ public Object getValueAt (int row, int col) { if (!m_open || row < 0 || col < 0 || row >= m_rowCount) { return null; } waitLoadingForRow(row); // empty buffer if (row >= m_sort.size()) { return null; } // return Data item Object[] rowData = getDataAtRow(row); // out of bounds if (rowData == null || col > rowData.length) { return null; } return rowData[col]; } // getValueAt /** * wait for loading of row * @param row */ public void waitLoadingForRow(int row) { // need to wait for data read into buffer int loops = 0; //wait for [timeout] seconds int timeout = MSysConfig.getIntValue(MSysConfig.GRIDTABLE_LOAD_TIMEOUT_IN_SECONDS, DEFAULT_GRIDTABLE_LOAD_TIMEOUT_IN_SECONDS, Env.getAD_Client_ID(Env.getCtx())); while (row >= m_sort.size() && m_loaderFuture != null && !m_loaderFuture.isDone() && loops < timeout) { if (log.isLoggable(Level.FINE)) log.fine("Waiting for loader row=" + row + ", size=" + m_sort.size()); try { m_loaderFuture.get(1000, TimeUnit.MILLISECONDS); } catch (Exception ie) {} loops++; } if (m_sort.size() == 0) { // check if there is a DB error saved to show Exception savedEx = CLogger.retrieveException(); if (savedEx != null) throw new IllegalStateException(savedEx); } // zero rows found without load timeout if (row == 0 && m_sort.size() == 0 && m_rowCountTimeout && !m_rowLoadTimeout) throw new AdempiereException(Msg.getMsg(Env.getCtx(), "FindZeroRecords")); if (row >= m_sort.size()) { log.warning("Reached " + timeout + " seconds timeout loading row " + (row+1) + " for SQL=" + m_SQL); //adjust row count m_rowCount = m_sort.size(); throw new AdempiereException(Msg.getMsg(Env.getCtx(), LOAD_TIMEOUT_ERROR_MESSAGE)); } } /** * @param row row index * @return data at row index */ private Object[] getDataAtRow(int row) { return getDataAtRow(row, true); } /** * @param row row index * @param fetchIfNotFound * @return data at row index */ private Object[] getDataAtRow(int row, boolean fetchIfNotFound) { waitLoadingForRow(row); MSort sort = (MSort)m_sort.get(row); Object[] rowData = null; if (m_virtual) { if (sort.index != NEW_ROW_ID && !(m_virtualBuffer.containsKey(sort.index)) && fetchIfNotFound) { fillBuffer(row, DEFAULT_FETCH_SIZE); } rowData = (Object[])m_virtualBuffer.get(sort.index); } else { rowData = (Object[])m_buffer.get(sort.index); } return rowData; } /** * @param row row index * @param rowData */ private void setDataAtRow(int row, Object[] rowData) { MSort sort = m_sort.get(row); if (m_virtual) { if (sort.index != NEW_ROW_ID && !(m_virtualBuffer.containsKey(sort.index))) { fillBuffer(row, DEFAULT_FETCH_SIZE); } m_virtualBuffer.put(sort.index, rowData); } else { m_buffer.set(sort.index, rowData); } } /** * Fill virtual buffer ({@link #m_virtualBuffer}. * @param start * @param fetchSize */ private void fillBuffer(int start, int fetchSize) { //adjust start if needed if (start > 0) { if (start + fetchSize >= m_sort.size()) { start = start - (fetchSize - ( m_sort.size() - start )); if (start < 0) start = 0; } } StringBuilder sql = new StringBuilder(); sql.append(m_SQL_Select) .append(" WHERE ") .append(getKeyColumnName()) .append(" IN ("); Maprowmap = new LinkedHashMap(DEFAULT_FETCH_SIZE); int count = 0; for(int i = start; i < start+fetchSize && i < m_sort.size(); i++) { if (m_sort.get(i).index == NEW_ROW_ID) continue; if(count > 0) sql.append(","); sql.append(m_sort.get(i).index); rowmap.put(m_sort.get(i).index, i); count++; } sql.append(")"); Object[] newRow = m_virtualBuffer.get(NEW_ROW_ID); //cache changed row Object[] changedRow = m_rowChanged >= 0 ? getDataAtRow(m_rowChanged, false) : null; m_virtualBuffer = new HashMap(210); if (newRow != null && newRow.length > 0) m_virtualBuffer.put(NEW_ROW_ID, newRow); PreparedStatement stmt = null; ResultSet rs = null; m_rowLoadTimeout = false; try { stmt = DB.prepareStatement(sql.toString(), null); int timeout = MSysConfig.getIntValue(MSysConfig.GRIDTABLE_LOAD_TIMEOUT_IN_SECONDS, DEFAULT_GRIDTABLE_LOAD_TIMEOUT_IN_SECONDS, Env.getAD_Client_ID(Env.getCtx())); if (timeout > 0) stmt.setQueryTimeout(timeout); rs = stmt.executeQuery(); while(rs.next()) { Object[] data = readData(rs); rowmap.remove(data[m_indexKeyColumn]); m_virtualBuffer.put((Integer)data[m_indexKeyColumn], data); } if (!rowmap.isEmpty()) { List toremove = new ArrayList(); for(Map.Entry entry : rowmap.entrySet()) { toremove.add(entry.getValue()); } Collections.reverse(toremove); for(Integer row : toremove) { m_sort.remove(row.intValue()); } } if (changedRow != null && changedRow.length > 0) { if (changedRow[m_indexKeyColumn] != null && (Integer)changedRow[m_indexKeyColumn] > 0) { m_virtualBuffer.put((Integer)changedRow[m_indexKeyColumn], changedRow); } } } catch (SQLException e) { if (DB.getDatabase().isQueryTimeout(e)) m_rowLoadTimeout = true; log.log(Level.SEVERE, e.getLocalizedMessage(), e); } finally { DB.close(rs, stmt); } } /** * Indicate that there will be a change * @param changed changed */ public void setChanged (boolean changed) { // Can we edit? if (!m_open || m_readOnly) return; // Indicate Change m_changed = changed; if (!changed) m_rowChanged = -1; } // setChanged /** * Set value at row and column * * @param value value to assign to cell * @param row row index of cell * @param col column index of cell */ public final void setValueAt (Object value, int row, int col) { setValueAt (value, row, col, false, false); } // setValueAt /** * call {@link #setValueAt(Object, int, int, boolean, boolean)} with isInitEdit = false * * @param value value to assign to cell * @param row row index of cell * @param col column index of cell * @param force force setting new value */ public final void setValueAt (Object value, int row, int col, boolean force) { setValueAt (value, row, col, force, false); } // setValueAt /** * Set value in row data and update GridField. * * @param value value to assign to cell * @param row row index of cell * @param col column index of cell * @param force force setting new value * @param isInitEdit indicate event rise by start edit a field. just want change status to edit, don't change anything else */ public final void setValueAt (Object value, int row, int col, boolean force, boolean isInitEdit) { // Can we edit? if (!m_open || m_readOnly // not accessible || row < 0 || col < 0 // invalid index || m_rowCount == 0 // no rows || row >= m_rowCount ) //invalid row { if (log.isLoggable(Level.FINEST)) log.finest("r=" + row + " c=" + col + " - R/O=" + m_readOnly + ", Rows=" + m_rowCount + " - Ignored"); return; } dataSave(row, false); // Has anything changed? Object oldValue = getValueAt(row, col); if (!force && !isInitEdit && !isValueChanged(oldValue, value) ) { if (log.isLoggable(Level.FINEST)) log.finest("r=" + row + " c=" + col + " - New=" + value + "==Old=" + oldValue + " - Ignored"); return; } if (log.isLoggable(Level.FINE)) log.fine("r=" + row + " c=" + col + " = " + value + " (" + oldValue + ")"); // Save old value m_oldValue = new Object[3]; m_oldValue[0] = Integer.valueOf(row); m_oldValue[1] = Integer.valueOf(col); m_oldValue[2] = oldValue; // Set Data item Object[] rowData = getDataAtRow(row); m_rowChanged = row; // save original value - shallow copy if (m_rowData == null) { int size = m_fields.size(); m_rowData = new Object[size]; for (int i = 0; i < size; i++) m_rowData[i] = rowData[i]; } // save & update rowData[col] = value; setDataAtRow(row, rowData); // update Table fireTableCellUpdated(row, col); // update GridField GridField field = getField(col); field.setValue(value, m_inserting); // inform DataStatusEvent evt = createDSE(); evt.setIsInitEdit(isInitEdit); evt.setChangedColumn(col, field.getColumnName()); fireDataStatusChanged(evt); } // setValueAt /** * Get Old Value * @param row row * @param col col * @return old value */ public Object getOldValue (int row, int col) { if (m_oldValue == null) return null; if (((Integer)m_oldValue[0]).intValue() == row && ((Integer)m_oldValue[1]).intValue() == col) return m_oldValue[2]; return null; } // getOldValue /** * Check if {@link #m_rowChanged} needs to be saved. * @param onlyRealChange if true the value of a field was actually changed * (e.g. for new records, which have not been changed) - default false * @return true if needs to be saved */ public boolean needSave(boolean onlyRealChange) { return needSave(m_rowChanged, onlyRealChange); } // needSave /** * Check if {@link #m_rowChanged} needs to be saved. * @return true if needs to be saved */ public boolean needSave() { return needSave(m_rowChanged, false); } // needSave /** * Check if newRow needs to be saved. * @param newRow to check * @return true if needs to be saved */ public boolean needSave(int newRow) { return needSave(newRow, false); } // needSave /** * Check if the row needs to be saved. * - only when row changed * - only if nothing was changed * @param newRow to check * @param onlyRealChange if true, only if the value of a field was actually changed * (e.g. for new record with default value, which have not been changed) - default false * @return true it needs to be saved */ public boolean needSave(int newRow, boolean onlyRealChange) { if (log.isLoggable(Level.FINE)) log.fine("Row=" + newRow + ", Changed=" + m_rowChanged + "/" + m_changed); // m_rowChanged set in setValueAt // nothing done if (!m_changed && m_rowChanged == -1) return false; // E.g. New unchanged records if (m_changed && m_rowChanged == -1 && onlyRealChange) return false; // same row if (newRow == m_rowChanged) return false; return true; } // needSave /*************************************************************************/ /** Save OK - O */ public static final char SAVE_OK = 'O'; // the only OK condition /** Save Error - E */ public static final char SAVE_ERROR = 'E'; /** Save Access Error - A */ public static final char SAVE_ACCESS = 'A'; /** Save Mandatory Error - M */ public static final char SAVE_MANDATORY = 'M'; /** Save Abort Error - U */ public static final char SAVE_ABORT = 'U'; /** * Check if it needs to be saved and save it. * @param newRow row index * @param manualCmd true if initiated from user action * @return true if not needed to be saved or successful saved */ public boolean dataSave (int newRow, boolean manualCmd) { if (log.isLoggable(Level.FINE)) log.fine("Row=" + newRow + ", Changed=" + m_rowChanged + "/" + m_changed); // m_rowChanged set in setValueAt // nothing done if (!m_changed && m_rowChanged == -1) return true; // same row, don't save yet if (newRow == m_rowChanged) return true; return (dataSave(manualCmd) == SAVE_OK); } // dataSave /** * Save changes. * @param manualCmd if true (i.e initiated from user action), no vetoable PropertyChange will be fired for save confirmation * @return OK Or Error condition * Error info (Access*, FillMandatory, SaveErrorNotUnique, * SaveErrorRowNotFound, SaveErrorDataChanged) is saved in the log */ public char dataSave (boolean manualCmd) { // cannot save if (!m_open) { log.warning ("Error - Open=" + m_open); return SAVE_ERROR; } // no need - not changed - row not positioned - no Value changed if (m_rowChanged == -1) { if (log.isLoggable(Level.CONFIG)) log.config("NoNeed - Changed=" + m_changed + ", Row=" + m_rowChanged); if (!manualCmd) return SAVE_OK; } // Value not changed if (m_rowData == null) { //reset out of sync variable m_rowChanged = -1; if (log.isLoggable(Level.FINE)) log.fine("No Changes"); return SAVE_ERROR; } if (m_readOnly) // If Processed - not editable (Find always editable) -> ok for changing payment terms, etc. { if (log.isLoggable(Level.WARNING)) log.warning("IsReadOnly - ignored"); dataIgnore(); return SAVE_ACCESS; } // row not positioned - no Value changed if (m_rowChanged == -1) { if (m_newRow != -1) // new row and nothing changed - might be OK m_rowChanged = m_newRow; else { fireDataStatusEEvent("SaveErrorNoChange", "", true); return SAVE_ERROR; } } // Can we change? int[] co = getClientOrg(m_rowChanged); int AD_Client_ID = co[0]; int AD_Org_ID = co[1]; if (!MRole.getDefault(m_ctx, false).canUpdate(AD_Client_ID, AD_Org_ID, m_AD_Table_ID, 0, true)) { fireDataStatusEEvent(CLogger.retrieveError()); dataIgnore(); return SAVE_ACCESS; } if (log.isLoggable(Level.INFO)) log.info("Row=" + m_rowChanged); // inform about data save action, if not manually initiated try { if (!manualCmd) m_vetoableChangeSupport.fireVetoableChange(PROPERTY, -1, m_rowChanged); } catch (PropertyVetoException pve) { log.warning(pve.getMessage()); return SAVE_ABORT; } // get updated row data Object[] rowData = getDataAtRow(m_rowChanged); // Check Mandatory String missingColumns = getMandatory(rowData); if (missingColumns.length() != 0) { fireDataStatusEEvent("FillMandatory", missingColumns + "\n", true); return SAVE_MANDATORY; } /** * Update row ***** */ int Record_ID = 0; if (!m_inserting) Record_ID = getKeyID(m_rowChanged); try { return dataSavePO (Record_ID); } catch (Throwable e) { if (e instanceof ClassNotFoundException) log.warning(m_tableName + " - " + e.getLocalizedMessage()); else { log.log(Level.SEVERE, "Persistency Issue - " + m_tableName + ": " + e.getLocalizedMessage(), e); log.saveError("Error", e.getLocalizedMessage()); } } return SAVE_ERROR; } // dataSave /** * Save via PO * @param Record_ID * @return SAVE_ERROR or SAVE_OK * @throws Exception */ private char dataSavePO (int Record_ID) throws Exception { if (log.isLoggable(Level.FINE)) log.fine("ID=" + Record_ID); // Object[] rowData = getDataAtRow(m_rowChanged); // MTable table = MTable.get (m_ctx, m_AD_Table_ID); PO po = null; if (! m_importing) // Just use trx when importing m_trxName = null; if (Record_ID != -1) { if (Record_ID == 0 && !m_inserting && MTable.isZeroIDTable(table.getTableName())) { String uuidFromZeroID = table.getUUIDFromZeroID(); po = table.getPOByUU(uuidFromZeroID, m_trxName); } else { po = table.getPO(Record_ID, m_trxName); } } else // Multi - Key po = table.getPO(getWhereClause(rowData), m_trxName); // No Persistent Object if (po == null) throw new ClassNotFoundException ("No Persistent Object"); if (!po.is_new()) { if (hasChanged(po)) { // return error stating that current record has changed and it cannot be saved String adMessage = "CurrentRecordModified"; String msg = Msg.getMsg(Env.getCtx(), adMessage); fireDataStatusEEvent(adMessage, msg, true); return SAVE_ERROR; } } int size = m_fields.size(); for (int col = 0; col < size; col++) { GridField field = (GridField)m_fields.get (col); if (field.isVirtualColumn()) continue; String columnName = field.getColumnName (); Object value = rowData[col]; Object oldValue = m_rowData[col]; // RowID if (field.getDisplayType() == DisplayType.RowID) ; // ignore // Nothing changed & null else if (oldValue == null && value == null) ; // ignore // *** Data changed *** else if (m_inserting || isValueChanged(oldValue, value) ) { // Check existence int poIndex = po.get_ColumnIndex(columnName); if (poIndex < 0) { // Custom Fields not in PO po.set_CustomColumn(columnName, value); // log.log(Level.SEVERE, "Column not found: " + columnName); continue; } Object dbValue = po.get_Value(poIndex); if (m_inserting || !m_compareDB // Original == DB || (oldValue == null && dbValue == null) || (oldValue != null && oldValue.equals (dbValue)) // Target == DB (changed by trigger to new value already) || (value == null && dbValue == null) || (value != null && value.equals (dbValue)) || ((oldValue != null && dbValue != null && oldValue.getClass().equals(byte[].class) && dbValue.getClass().equals(byte[].class)) && Arrays.equals((byte[])oldValue, (byte[])dbValue)) || ((value != null && dbValue != null && value.getClass().equals(byte[].class) && dbValue.getClass().equals(byte[].class)) && Arrays.equals((byte[])oldValue, (byte[])dbValue)) ) { if (!po.set_ValueNoCheck (columnName, value)) { ValueNamePair lastError = CLogger.retrieveError(); if (lastError != null) { String adMessage = lastError.getValue(); String adMessageArgument = lastError.getName().trim(); StringBuilder info = new StringBuilder(adMessageArgument); if (!adMessageArgument.endsWith(";")) info.append(";"); info.append(field.getHeader()); fireDataStatusEEvent(adMessage, info.toString(), true); } else { fireDataStatusEEvent("Set value failed", field.getHeader(), true); } return SAVE_ERROR; } } // Original != DB else { String msg = columnName + "= " + oldValue + (oldValue==null ? "" : "(" + oldValue.getClass().getName() + ")") + " != DB: " + dbValue + (dbValue==null ? "" : "(" + dbValue.getClass().getName() + ")") + " -> New: " + value + (value==null ? "" : "(" + value.getClass().getName() + ")"); dataRefresh(m_rowChanged); fireDataStatusEEvent("SaveErrorDataChanged", msg, true); return SAVE_ERROR; } } // Data changed } // for every column if (!po.save()) { String msg = "SaveError"; String info = ""; ValueNamePair ppE = CLogger.retrieveError(); if (ppE != null) { msg = ppE.getValue(); info = ppE.getName(); if ("DBExecuteError".equals(msg)) info = "DBExecuteError:" + info; } fireDataStatusEEvent(msg, info, true); return SAVE_ERROR; } else if (m_virtual && po.get_ID() > 0) { //update ID MSort sort = m_sort.get(m_rowChanged); int oldid = sort.index; if (oldid != po.get_ID()) { sort.index = po.get_ID(); Object[] data = m_virtualBuffer.remove(oldid); data[m_indexKeyColumn] = sort.index; m_virtualBuffer.put(sort.index, data); } } // Refresh - update buffer String whereClause = po.get_WhereClause(true); if (log.isLoggable(Level.FINE)) log.fine("Reading ... " + whereClause); StringBuilder refreshSQL = new StringBuilder(m_SQL_Select) .append(" WHERE ").append(whereClause); PreparedStatement pstmt = null; ResultSet rs = null; try { pstmt = DB.prepareStatement(refreshSQL.toString(), get_TrxName()); rs = pstmt.executeQuery(); if (rs.next()) { Object[] rowDataDB = readData(rs); // update buffer setDataAtRow(m_rowChanged, rowDataDB); fireTableRowsUpdated(m_rowChanged, m_rowChanged); } } catch (SQLException e) { String msg = "SaveError"; log.log(Level.SEVERE, refreshSQL.toString(), e); fireDataStatusEEvent(msg, e.getLocalizedMessage(), true); return SAVE_ERROR; } finally { DB.close(rs, pstmt); rs = null; pstmt = null; } // everything ok m_rowData = null; m_changed = false; m_compareDB = true; m_rowChanged = -1; m_newRow = -1; m_inserting = false; // ValueNamePair pp = CLogger.retrieveWarning(); if (pp != null) { String msg = pp.getValue(); String info = pp.getName(); fireDataStatusEEvent(msg, info, false); } else { pp = CLogger.retrieveInfo(); String msg = "Saved"; String info = ""; if (pp != null) { msg = pp.getValue(); info = pp.getName(); } fireDataStatusIEvent(msg, info); } // log.config("fini"); return SAVE_OK; } // dataSavePO /** * Get Record Where Clause from data (single key or multi-key) * @param rowData data * @return where clause or null */ private String getWhereClause (Object[] rowData) { int size = m_fields.size(); StringBuilder singleRowWHERE = null; StringBuilder singleRowUUWHERE = null; StringBuilder multiRowWHERE = null; String tableName = getTableName(); int uidColumn = -1; for (int col = 0; col < size; col++) { GridField field = (GridField)m_fields.get (col); String columnName = field.getColumnName(); if (field.isKey()) { Object value = rowData[col]; if (value == null) { log.log(Level.WARNING, "PK data is null - " + columnName); return null; } if (columnName.endsWith ("_ID")) singleRowWHERE = new StringBuilder(tableName).append(".").append(columnName) .append ("=").append (value); else singleRowWHERE = new StringBuilder(tableName).append(".").append(columnName) .append ("=").append (DB.TO_STRING(value.toString())); break; } else if (field.isParentColumn()) { Object value = rowData[col]; if (value == null) { if (log.isLoggable(Level.INFO))log.log(Level.INFO, "FK data is null - " + columnName); continue; } if (multiRowWHERE == null) multiRowWHERE = new StringBuilder(); else multiRowWHERE.append(" AND "); if (columnName.endsWith ("_ID")) multiRowWHERE.append (tableName).append(".").append(columnName) .append ("=").append (value); else if (value instanceof Timestamp) { multiRowWHERE.append (tableName).append(".").append(columnName) .append ("=").append (DB.TO_DATE((Timestamp)value, false)); }else multiRowWHERE.append (tableName).append(".").append(columnName) .append ("=").append (DB.TO_STRING(value.toString())); } else if (columnName.equals(PO.getUUIDColumnName(tableName))) { uidColumn = col; } } // for all columns if (singleRowWHERE != null) return singleRowWHERE.toString(); if (multiRowWHERE != null) return multiRowWHERE.toString(); if (uidColumn >= 0) { Object value = rowData[uidColumn]; if (value == null && multiRowWHERE == null) { log.log(Level.WARNING, "UUID data is null - " + uidColumn); return null; } else { singleRowUUWHERE = new StringBuilder(tableName).append(".").append(PO.getUUIDColumnName(tableName)) .append ("=").append (DB.TO_STRING(value.toString())); } } if (singleRowUUWHERE != null) return singleRowUUWHERE.toString(); log.log(Level.WARNING, "No key Found"); return null; } // getWhereClause /*************************************************************************/ /** * Get Mandatory empty columns * @param rowData row data * @return Mandatory columns that's empty (separated by comma) */ private String getMandatory(Object[] rowData) { // see also => ProcessParameter.saveParameter StringBuilder sb = new StringBuilder(); // Check all columns int size = m_fields.size(); for (int i = 0; i < size; i++) { GridField field = (GridField)m_fields.get(i); if (field.isMandatory(true)) // check context { if (rowData[i] == null || rowData[i].toString().length() == 0) { field.setInserting (true); // set editable otherwise deadlock field.setError(true); if (sb.length() > 0) sb.append(", "); sb.append(field.getHeader()); } else field.setError(false); } } if (sb.length() == 0) return ""; return sb.toString(); } // getMandatory /** * @return true if need save and all mandatory field has value */ public boolean isNeedSaveAndMandatoryFill() { if (!m_open) { return false; } // no need - not changed - row not positioned - no Value changed if (m_rowChanged == -1) { return false; } // Value not changed if (m_rowData == null) { return false; } if (m_readOnly) { return false; } // row not positioned - no Value changed if (m_rowChanged == -1) { if (m_newRow != -1) // new row and nothing changed - might be OK m_rowChanged = m_newRow; else { return false; } } // get updated row data Object[] rowData = getDataAtRow(m_rowChanged); // Check Mandatory String missingColumns = getMandatory(rowData); if (missingColumns.length() != 0) { return false; } return true; } /*************************************************************************/ // IDEMPIERE-454 Easy import private boolean m_importing = false; private String m_trxName = null; private int m_currentRow = -1; /** * Append new row after current row * @param currentRow row * @param copyCurrent true to copy value from current row * @return true if success - * Error info (Access*, AccessCannotInsert) is saved in the log */ public boolean dataNew (int currentRow, boolean copyCurrent) { if (log.isLoggable(Level.INFO)) log.info("Current=" + currentRow + ", Copy=" + copyCurrent); // Read only if (m_readOnly) { fireDataStatusEEvent("AccessCannotInsert", "", true); return false; } // see if we need to save dataSave(-2, false); m_inserting = true; // Setup the buffer first so that event will be handle properly // Create default data int size = m_fields.size(); m_rowData = new Object[size]; // "original" data Object[] rowData = new Object[size]; m_changed = true; m_compareDB = true; m_newRow = currentRow + 1; // if there is no record, the current row could be 0 (and not -1) if (m_sort.size() < m_newRow) m_newRow = m_sort.size(); // add Data at end of buffer MSort newSort = m_virtual ? new MSort(NEW_ROW_ID, null) : new MSort(m_sort.size(), null); // index if (m_virtual) { m_virtualBuffer.put(NEW_ROW_ID, rowData); } else { m_buffer.add(rowData); } // add Sort pointer m_sort.add(m_newRow, newSort); m_rowCount++; // fill data if (copyCurrent) { boolean hasDocTypeTargetField = (getField("C_DocTypeTarget_ID") != null); Object[] origData = getDataAtRow(currentRow); for (int i = 0; i < size; i++) { GridField field = (GridField)m_fields.get(i); MColumn column = null; if (field.getAD_Column_ID() > 0) column = MColumn.get(m_ctx, field.getAD_Column_ID()); if (field.isVirtualColumn()) ; else if ( field.isKey() // KeyColumn || (column != null && column.isUUIDColumn()) // IDEMPIERE-67 || (column != null && column.isStandardColumn() && !column.getColumnName().equals("AD_Org_ID")) // AD_Org_ID can be copied // Bug [ 1807947 ] || (hasDocTypeTargetField && field.getColumnName().equals("C_DocType_ID")) || ! field.isAllowCopy()) { Object value = field.getDefault(); field.setValue(value, m_inserting); field.validateValueNoDirect(); rowData[i] = field.getValue(); } else { Object value = origData[i]; field.setValue(value, m_inserting); field.validateValueNoDirect(); rowData[i] = field.getValue(); } } } else // new { for (int i = 0; i < size; i++) { GridField field = (GridField)m_fields.get(i); if (field.getGridTab() != null) { //avoid getting default from previous row String key = field.getVO().WindowNo+"|"+field.getVO().TabNo+"|"+field.getVO().ColumnName; field.getVO().ctx.remove(key); } Object value = field.getDefault(); field.setValue(value, m_inserting); field.validateValueNoDirect(); rowData[i] = field.getValue(); } } m_rowChanged = -1; // only changed in setValueAt // inform if (log.isLoggable(Level.FINE)) log.fine("Current=" + currentRow + ", New=" + m_newRow); fireTableRowsInserted(m_newRow, m_newRow); fireDataStatusIEvent(copyCurrent ? DATA_UPDATE_COPIED_MESSAGE : DATA_INSERTED_MESSAGE, ""); if (log.isLoggable(Level.FINE)) log.fine("Current=" + currentRow + ", New=" + m_newRow + " - complete"); return true; } // dataNew /** * Delete data at row index * @param row row index * @return true if success - * Error info (Access*, AccessNotDeleteable, DeleteErrorDependent, * DeleteError) is saved in the log */ public boolean dataDelete (int row) { if (log.isLoggable(Level.INFO)) log.info("Row=" + row); if (row < 0) return false; // Tab R/O if (m_readOnly) { fireDataStatusEEvent("AccessCannotDelete", "", true); // privileges return false; } // Is this record deletable? if (!m_deleteable) { fireDataStatusEEvent("AccessNotDeleteable", "", true); // audit return false; } // Processed Column and not an Import Table if (m_indexProcessedColumn > 0 && !m_tableName.startsWith("I_")) { Boolean processed = (Boolean)getValueAt(row, m_indexProcessedColumn); if (processed != null && processed.booleanValue()) { fireDataStatusEEvent("CannotDeleteTrx", "", true); return false; } } // Carlos Ruiz - globalqss - IDEMPIERE-111 // Check if the role has access to this client // Can we change? int[] co = getClientOrg(row); int AD_Client_ID = co[0]; int AD_Org_ID = co[1]; if (!MRole.getDefault(m_ctx, false).canUpdate(AD_Client_ID, AD_Org_ID, m_AD_Table_ID, 0, true)) { fireDataStatusEEvent("AccessCannotDelete", "", true); return false; } MSort sort = (MSort)m_sort.get(row); Object[] rowData = getDataAtRow(row); // PO po = getPO(row); // Delete via PO if (po != null) { boolean ok = false; try { ok = po.delete(false); } catch (Throwable t) { log.log(Level.SEVERE, "Delete", t); } if (!ok) { ValueNamePair vp = CLogger.retrieveError(); if (vp != null && !(Util.isEmpty(vp.getValue()) || Util.isEmpty(vp.getName()))) fireDataStatusEEvent(vp); else fireDataStatusEEvent("DeleteError", "", true); return false; } } else // Delete via SQL { StringBuilder sql = new StringBuilder("DELETE FROM "); sql.append(m_tableName).append(" WHERE ").append(getWhereClause(rowData)); int no = 0; PreparedStatement pstmt = null; try { pstmt = DB.prepareStatement (sql.toString(), ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE, null); no = pstmt.executeUpdate(); } catch (SQLException e) { log.log(Level.SEVERE, sql.toString(), e); String msg = "DeleteError"; String dbMsg = DBException.getDefaultDBExceptionMessage(e); if (!Util.isEmpty(dbMsg)) msg = dbMsg; fireDataStatusEEvent(msg, e.getLocalizedMessage(), true); return false; } finally { DB.close(pstmt); pstmt = null; } // Check Result if (no != 1) { log.log(Level.SEVERE, "Number of deleted rows = " + no); return false; } } // Get Sort if (m_virtual) { m_virtualBuffer.remove(sort.index); } else { // Delete row in Buffer and shifts all below up m_buffer.remove(sort.index); } m_rowCount--; // Delete row in Sort m_sort.remove(row); if (!m_virtual) { // Correct pointer in Sort for (int i = 0; i < m_sort.size(); i++) { MSort ptr = (MSort)m_sort.get(i); if (ptr.index > sort.index) ptr.index--; // move up } } // inform m_changed = false; m_rowChanged = -1; fireTableRowsDeleted(row, row); fireDataStatusIEvent("Deleted", ""); if (log.isLoggable(Level.FINE)) log.fine("Row=" + row + " complete"); return true; } // dataDelete /** * Ignore/Undo changes */ public void dataIgnore() { if (!m_inserting && !m_changed && m_rowChanged < 0) { if (log.isLoggable(Level.FINE)) log.fine("Nothing to ignore"); m_newRow = -1; return; } if (log.isLoggable(Level.INFO)) log.info("Inserting=" + m_inserting); // Inserting - delete new row if (m_inserting) { // Get Sort MSort sort = (MSort)m_sort.get(m_newRow); if (m_virtual) { m_virtualBuffer.remove(NEW_ROW_ID); } else { // Delete row in Buffer and shifts all below up m_buffer.remove(sort.index); } m_rowCount--; // Delete row in Sort m_sort.remove(m_newRow); // pintint to the last column, so no adjustment // m_changed = false; m_rowData = null; m_rowChanged = -1; m_inserting = false; // inform fireTableRowsDeleted(m_newRow, m_newRow); } else { // update buffer if (m_rowData != null) { setDataAtRow(m_rowChanged, m_rowData); } m_changed = false; m_rowData = null; m_rowChanged = -1; m_inserting = false; // inform // fireTableRowsUpdated(m_rowChanged, m_rowChanged); >> messes up display?? (clearSelection) } m_newRow = -1; fireDataStatusIEvent(DATA_IGNORED_MESSAGE, ""); } // dataIgnore /** * Refresh Row - ignore changes * @param row row */ public void dataRefresh (int row) { dataRefresh(row, true); } /** * get where clause for row * @param row * @return where clause */ public String getWhereClause(int row) { if (row < 0 || m_sort.size() == 0 || m_inserting) return null; Object[] rowData = getDataAtRow(row); if (rowData == null) return null; String where = getWhereClause(rowData); return where; } /** * Refresh Row - ignore changes * @param row row * @param fireStatusEvent */ public void dataRefresh (int row, boolean fireStatusEvent) { if (log.isLoggable(Level.INFO)) log.info("Row=" + row); if (row < 0 || m_sort.size() == 0 || m_inserting) return; Object[] rowData = getDataAtRow(row); // ignore dataIgnore(); // Create SQL String where = getWhereClause(rowData); if (where == null || where.length() == 0) where = "1=2"; String sql = m_SQL_Select + " WHERE " + where; Object[] rowDataDB = null; PreparedStatement pstmt = null; ResultSet rs = null; try { pstmt = DB.prepareStatement(sql,get_TrxName()); rs = pstmt.executeQuery(); // only one row if (rs.next()) rowDataDB = readData(rs); } catch (SQLException e) { log.log(Level.SEVERE, sql, e); fireTableRowsUpdated(row, row); fireDataStatusEEvent("RefreshError", sql, true); return; } finally { DB.close(rs, pstmt); rs = null; pstmt = null; } // update buffer if (rowDataDB!=null) setDataAtRow(row, rowDataDB); // info m_rowData = null; m_changed = false; m_rowChanged = -1; m_inserting = false; fireTableRowsUpdated(row, row); if (fireStatusEvent) fireDataStatusIEvent(DATA_REFRESH_MESSAGE, ""); } // dataRefresh /** * Refresh all Rows - ignore changes */ public void dataRefreshAll() { dataRefreshAll(true); } /** * Refresh all Rows - ignore changes * @param fireStatusEvent */ public void dataRefreshAll(boolean fireStatusEvent) { dataRefreshAll(fireStatusEvent, -1); } /** * Refresh all Rows - ignore changes * @param fireStatusEvent * @param rowToRetained */ public void dataRefreshAll(boolean fireStatusEvent, int rowToRetained) { m_inserting = false; // should not happen dataIgnore(); String retainedWhere = null; if (rowToRetained >= 0) { retainedWhere = getWhereClause(rowToRetained); } close(false); if (retainedWhere != null) { if (m_whereClause != null && m_whereClause.trim().length() > 0) { StringBuilder orRetainedWhere = new StringBuilder(") OR (").append(retainedWhere).append(")) "); if (! m_whereClause.contains(orRetainedWhere.toString())) m_whereClause = "((" + m_whereClause + orRetainedWhere.toString(); } open(m_maxRows); } else { open(m_maxRows); } // Info m_rowData = null; m_changed = false; m_rowChanged = -1; m_inserting = false; if (m_lastSortColumnIndex >= 0) { loadComplete(); sort(m_lastSortColumnIndex, m_lastSortedAscending); } fireTableDataChanged(); if (fireStatusEvent) fireDataStatusIEvent(DATA_REFRESH_MESSAGE, ""); } // dataRefreshAll /** * Re-query with new whereClause * @param whereClause sql where clause * @param onlyCurrentRows only current rows * @param onlyCurrentDays how many days back * @param fireEvents if tabledatachanged and datastatusievent must be fired * @return true if success */ public boolean dataRequery (String whereClause, boolean onlyCurrentRows, int onlyCurrentDays, boolean fireEvents) { if (log.isLoggable(Level.INFO)) log.info(whereClause + "; OnlyCurrent=" + onlyCurrentRows); close(false); m_onlyCurrentDays = onlyCurrentDays; setSelectWhereClause(whereClause, onlyCurrentRows, m_onlyCurrentDays); open(m_maxRows); // Info m_rowData = null; m_changed = false; m_rowChanged = -1; m_inserting = false; if (m_lastSortColumnIndex >= 0) { loadComplete(); sort(m_lastSortColumnIndex, m_lastSortedAscending); } if (fireEvents) { fireTableDataChanged(); fireDataStatusIEvent(DATA_REFRESH_MESSAGE, ""); } return true; } // dataRequery /** * Delegate to {@link #dataRequery(String, boolean, int, boolean)} with fireEvents=true * @param whereClause * @param onlyCurrentRows * @param onlyCurrentDays * @return true if success */ public boolean dataRequery (String whereClause, boolean onlyCurrentRows, int onlyCurrentDays) { return dataRequery (whereClause, onlyCurrentRows, onlyCurrentDays, true); } // dataRequery /** * Is Cell Editable. * @param row row index * @param col column index * @return true, if editable */ public boolean isCellEditable (int row, int col) { // Entire Table not editable if (m_readOnly) return false; // Key not editable if (col == m_indexKeyColumn) return false; /** @todo check link columns */ // Check column range if (col < 0 && col >= m_fields.size()) return false; // IsActive Column always editable if no processed exists if (col == m_indexActiveColumn && m_indexProcessedColumn == -1) return true; // Row if (!isRowEditable(row)) return false; // Column return ((GridField)m_fields.get(col)).isEditable(false); } // IsCellEditable /** * Is row editable * @param row row index * @return true if editable */ public boolean isRowEditable (int row) { // Entire Table not editable or no row if (m_readOnly || row < 0) return false; // If not Active - not editable if (m_indexActiveColumn > 0) // && m_TabNo != Find.s_TabNo) { Object value = getValueAt(row, m_indexActiveColumn); if (value instanceof Boolean) { if (!((Boolean)value).booleanValue()) return false; } else if ("N".equals(value)) return false; } // If Processed - not editable (Find always editable) if (m_indexProcessedColumn > 0) // && m_TabNo != Find.s_TabNo) { Object processed = getValueAt(row, m_indexProcessedColumn); if (processed instanceof Boolean) { if (((Boolean)processed).booleanValue()) return false; } else if ("Y".equals(processed)) return false; } // int[] co = getClientOrg(row); int AD_Client_ID = co[0]; int AD_Org_ID = co[1]; int Record_ID = getKeyID(row); return MRole.getDefault(m_ctx, false).canUpdate (AD_Client_ID, AD_Org_ID, m_AD_Table_ID, Record_ID, false); } // isRowEditable /** * Get Client Org for row * @param row row index * @return array [0] = Client [1] = Org - a value of -1 is not defined/found */ private int[] getClientOrg (int row) { int AD_Client_ID = -1; if (m_indexClientColumn != -1) { Integer ii = (Integer)getValueAt(row, m_indexClientColumn); if (ii != null) AD_Client_ID = ii.intValue(); } int AD_Org_ID = 0; if (m_indexOrgColumn != -1) { Integer ii = (Integer)getValueAt(row, m_indexOrgColumn); if (ii != null) AD_Org_ID = ii.intValue(); } return new int[] {AD_Client_ID, AD_Org_ID}; } // getClientOrg /** * Set entire table as read only * @param value new read only value */ public void setReadOnly (boolean value) { if (log.isLoggable(Level.FINE)) log.fine("ReadOnly=" + value); m_readOnly = value; } // setReadOnly /** * Is entire Table Read/Only * @return true if read only */ public boolean isReadOnly() { return m_readOnly; } // isReadOnly /** * Is inserting * @return true if inserting */ public boolean isInserting() { return m_inserting; } // isInserting /** * Set Compare DB. * If Set to false, save overwrites the record, regardless of DB changes. * (When a payment is changed in Sales Order, the payment reversal clears the payment id) * @param compareDB compare DB - false forces overwrite * @deprecated */ @Deprecated public void setCompareDB (boolean compareDB) { m_compareDB = compareDB; } // setCompareDB /** * Get Compare DB. * @return false if save overwrites the record, regardless of DB changes * (false forces overwrite). * @deprecated */ @Deprecated public boolean getCompareDB () { return m_compareDB; } // getCompareDB /** * Can Table rows be deleted * @param value new deleteable value */ public void setDeleteable (boolean value) { if (log.isLoggable(Level.FINE)) log.fine("Deleteable=" + value); m_deleteable = value; } // setDeleteable /** * Read data from result set * @param rs result set * @return Data Array */ private Object[] readData (ResultSet rs) { int size = m_fields.size(); Object[] rowData = new Object[size]; String columnName = null; int displayType = 0; // Types see also GridField.createDefault try { // get row data for (int j = 0; j < size; j++) { // Column Info GridField field = (GridField)m_fields.get(j); columnName = field.getColumnName(); displayType = field.getDisplayType(); // Integer, ID, Lookup if (displayType == DisplayType.Integer || (DisplayType.isID(displayType) && !(columnName.equals("EntityType") || columnName.equals("AD_Language")))) { rowData[j] = Integer.valueOf(rs.getInt(j+1)); // Integer if (rs.wasNull()) rowData[j] = null; } // Number else if (DisplayType.isNumeric(displayType)) rowData[j] = rs.getBigDecimal(j+1); // BigDecimal // Date else if (DisplayType.isDate(displayType)) rowData[j] = rs.getTimestamp(j+1); // Timestamp // RowID or Key (and Selection) else if (displayType == DisplayType.RowID) rowData[j] = null; // YesNo else if (displayType == DisplayType.YesNo) { String str = rs.getString(j+1); if (field.isEncryptedColumn()) str = (String)decrypt(str, getAD_Client_ID(rs)); rowData[j] = Boolean.valueOf("Y".equals(str)); // Boolean } // LOB else if (DisplayType.isLOB(displayType)) { Object value = rs.getObject(j+1); if (rs.wasNull()) rowData[j] = null; else if (value instanceof Clob) { Clob lob = (Clob)value; long length = lob.length(); rowData[j] = lob.getSubString(1, (int)length); } else if (value instanceof Blob) { Blob lob = (Blob)value; long length = lob.length(); rowData[j] = lob.getBytes(1, (int)length); } else if (value instanceof String) rowData[j] = value; else if (value instanceof byte[]) rowData[j] = value; } // String else rowData[j] = rs.getString(j+1); // String // Encrypted if (field.isEncryptedColumn() && displayType != DisplayType.YesNo) rowData[j] = decrypt(rowData[j], getAD_Client_ID(rs)); } } catch (SQLException e) { log.log(Level.SEVERE, columnName + ", DT=" + displayType, e); } return rowData; } // readData /** * Decrypt * @param yy encrypted data * @param AD_Client_ID * @return clear data */ private Object decrypt (Object yy, int AD_Client_ID) { if (yy == null) return null; return SecureEngine.decrypt(yy, AD_Client_ID); } // decrypt /** * @param rs * @return AD_Client_ID or -1 */ private int getAD_Client_ID(ResultSet rs) { int AD_Client_ID = -1; try { AD_Client_ID = rs.getInt("AD_Client_ID"); if (rs.wasNull()) AD_Client_ID = -1; } catch (SQLException e) { AD_Client_ID = -1; } if (AD_Client_ID == -1) AD_Client_ID = getAD_Client_ID(); return AD_Client_ID; } /** * @return AD_Client_ID */ private int getAD_Client_ID() { int AD_Client_ID = Env.getAD_Client_ID(Env.getCtx()); GridField field = getField("AD_Client_ID"); if (field != null && field.getValue() != null) { AD_Client_ID = ((Number)field.getValue()).intValue(); } return AD_Client_ID; } /** * Remove Data Status Listener * @param l listener */ public synchronized void removeDataStatusListener(DataStatusListener l) { listenerList.remove(DataStatusListener.class, l); } // removeDataStatusListener /** * Add Data Status Listener * @param l listener */ public synchronized void addDataStatusListener(DataStatusListener l) { listenerList.add(DataStatusListener.class, l); } // addDataStatusListener /** * Fire data status changed event * @param e event */ private void fireDataStatusChanged (DataStatusEvent e) { DataStatusListener[] listeners = listenerList.getListeners(DataStatusListener.class); for (int i = 0; i < listeners.length; i++) listeners[i].dataStatusChanged(e); } // fireDataStatusChanged /** * Create new Data Status Event instance * @return new data status event instance */ private DataStatusEvent createDSE() { boolean changed = m_changed; if (m_rowChanged != -1) changed = true; DataStatusEvent dse = new DataStatusEvent(this, m_rowCount, changed, Env.isAutoCommit(m_ctx, m_WindowNo), m_inserting); dse.AD_Table_ID = m_AD_Table_ID; dse.Record_ID = null; return dse; } // createDSE /** * Create and fire Data Status Info Event * @param AD_Message message * @param info additional info */ protected void fireDataStatusIEvent (String AD_Message, String info) { DataStatusEvent e = createDSE(); e.setInfo(AD_Message, info, false,false); if (SORTED_DSE_EVENT.equals(AD_Message) && m_currentRow >= 0) e.setCurrentRow(m_currentRow); fireDataStatusChanged (e); } // fireDataStatusIEvent /** * Create and fire Data Status Error Event * @param AD_Message message * @param info info * @param isError error */ protected void fireDataStatusEEvent (String AD_Message, String info, boolean isError) { // DataStatusEvent e = createDSE(); if (info != null && info.startsWith("DBExecuteError:")) { String firstline; int nl = info.indexOf("\n"); if (nl > 0) firstline = info.substring(0, nl); else firstline = info; String newinfo = Msg.getMsg(m_ctx, firstline); if (firstline.equals(newinfo)) newinfo = info.substring(15); // size of "DBExecuteError:" e.setInfo(AD_Message, newinfo, isError, !isError); } else { e.setInfo(AD_Message, info, isError, !isError); } if (isError) log.saveWarning(AD_Message, info); fireDataStatusChanged (e); } // fireDataStatusEEvent /** * Create and fire Data Status Error Event (from Error Log) * @param errorLog error log info */ protected void fireDataStatusEEvent (ValueNamePair errorLog) { if (errorLog != null) fireDataStatusEEvent (errorLog.getValue(), errorLog.getName(), true); } // fireDataStatusEEvent /** * Remove Vetoable change listener for row changes * @param l listener */ public synchronized void removeVetoableChangeListener(VetoableChangeListener l) { m_vetoableChangeSupport.removeVetoableChangeListener(l); } // removeVetoableChangeListener /** * Add Vetoable change listener for row changes * @param l listener */ public synchronized void addVetoableChangeListener(VetoableChangeListener l) { m_vetoableChangeSupport.addVetoableChangeListener(l); } // addVetoableChangeListener /** * Fire Vetoable change listener for row changes * @param e event * @throws PropertyVetoException */ protected void fireVetoableChange(PropertyChangeEvent e) throws java.beans.PropertyVetoException { m_vetoableChangeSupport.fireVetoableChange(e); } // fireVetoableChange /** * toString * @return String representation */ public String toString() { return new StringBuilder("MTable[").append(m_tableName) .append(",WindowNo=").append(m_WindowNo) .append(",Tab=").append(m_TabNo).append("]").toString(); } // toString /** * * @return new row added */ public int getNewRow() { return m_newRow; } /** * Asynchronous Loader */ protected class Loader implements Serializable, Runnable { /** * generated serial id */ private static final long serialVersionUID = -6866671239509705988L; /** * Construct Loader */ public Loader() { super(); } // Loader private PreparedStatement m_pstmt = null; private ResultSet m_rs = null; private Trx trx = null; private Properties m_context = null; private int maxRows; private int rows; public void setContext(Properties context) { m_context = context; } /** * Open ResultSet * @param maxRows maximum number of rows or 0 for all * @return number of records */ protected int open (int maxRows) { this.maxRows = maxRows; // Get Number of Rows rows = 0; PreparedStatement pstmt = null; ResultSet rs = null; m_rowCountTimeout = false; try { pstmt = DB.prepareStatement(m_SQL_Count, get_TrxName()); setParameter (pstmt, true); int timeout = MSysConfig.getIntValue(MSysConfig.GRIDTABLE_INITIAL_COUNT_TIMEOUT_IN_SECONDS, DEFAULT_GRIDTABLE_COUNT_TIMEOUT_IN_SECONDS, Env.getAD_Client_ID(Env.getCtx())); if (timeout > 0) pstmt.setQueryTimeout(timeout); rs = pstmt.executeQuery(); if (rs.next()) rows = rs.getInt(1); } catch (SQLException e0) { if (DB.getDatabase().isQueryTimeout(e0)) { m_rowCountTimeout = true; return 0; } else throw new DBException(e0); } finally { DB.close(rs, pstmt); } StringBuilder info = new StringBuilder("Rows="); info.append(rows); if (rows == 0) info.append(" - ").append(m_SQL_Count); if (maxRows > 0 && rows > maxRows) { info.append(" - MaxRows=").append(maxRows); rows = maxRows; } if (log.isLoggable(Level.FINE)) log.fine(info.toString()); return rows; } // open private void openResultSet() { String trxName = get_TrxName(); //postgresql need trx to use cursor based resultset //https://jdbc.postgresql.org/documentation/head/query.html#query-with-cursor if (trxName == null) { trxName = m_virtual ? Trx.createTrxName("Loader") : null; trx = trxName != null ? Trx.get(trxName, true) : null; if (trx != null) trx.setDisplayName(getClass().getName()+"_openResultSet"); } // open Statement (closed by Loader.close) try { m_pstmt = DB.prepareStatement(m_SQL, trxName); //ensure not all rows are fetch into memory for virtual table if (m_virtual) m_pstmt.setFetchSize(100); setParameter (m_pstmt, false); int timeout = MSysConfig.getIntValue(MSysConfig.GRIDTABLE_LOAD_TIMEOUT_IN_SECONDS, DEFAULT_GRIDTABLE_LOAD_TIMEOUT_IN_SECONDS, Env.getAD_Client_ID(Env.getCtx())); if (timeout > 0) m_pstmt.setQueryTimeout(timeout); m_rs = m_pstmt.executeQuery(); } catch (SQLException e) { if (DB.getDatabase().isQueryTimeout(e)) { m_rowLoadTimeout = true; throw new AdempiereException(Msg.getMsg(Env.getCtx(), LOAD_TIMEOUT_ERROR_MESSAGE), e); } else { log.saveError(e.getLocalizedMessage(), e); throw new DBException(e); } } } /** * Close RS and Statement */ private void close() { DB.close(m_rs, m_pstmt); m_rs = null; m_pstmt = null; if (trx != null) { trx.close(); trx = null; } } // close /** * Fill Buffer to include Row */ public void run() { try { if (m_context != null) ServerContext.setCurrentInstance(m_context); doRun(); } finally { if (m_context != null) ServerContext.dispose(); } } // run /** * Fill buffer from result set */ private void doRun() { boolean isFindOverMax = false; try { openResultSet(); if (m_rs == null) return; while (m_rs.next()) { if (maxRows > 0 && m_sort.size() == maxRows) { isFindOverMax = true; break; } if (Thread.interrupted()) { if (log.isLoggable(Level.FINE)) log.fine("Interrupted"); close(); return; } // Get Data int recordId = 0; Object[] rowData = null; if (m_virtual) recordId = m_rs.getInt(getKeyColumnName()); else rowData = readData(m_rs); // add Data MSort sort = m_virtual ? new MSort(recordId, null) : new MSort(m_buffer.size(), null); // index if (!m_virtual) { m_buffer.add(rowData); } m_sort.add(sort); // Start with rowCount=0, inform loading of first row if (m_rowCountTimeout) { m_rowCount++; if (m_rowCount == 1) { DataStatusEvent evt = createDSE(); evt.setLoading(m_sort.size()); evt.setInfo("CountQueryTimeoutLoadBackground", null, false, true); fireDataStatusChanged(evt); } } // Statement all 1000 rows & sleep if (m_sort.size() % 1000 == 0) { DataStatusEvent evt = createDSE(); evt.setLoading(m_sort.size()); fireDataStatusChanged(evt); // give the other processes a chance try { Thread.yield(); Thread.sleep(10); // .01 second } catch (InterruptedException ie) { if (log.isLoggable(Level.FINE)) log.fine("Interrupted while sleeping"); close(); return; } } } // while(rs.next()) } catch (Exception e) { log.log(Level.SEVERE, "run", e); } finally { close(); } // Background loading without initial rowCount, inform final loaded rows if (m_rowCountTimeout && m_sort.size() > 0) { DataStatusEvent evt = createDSE(); evt.setLoading(m_sort.size()); if (isFindOverMax) evt.setInfo("FindOverMax", " > " + m_sort.size(), false, true); fireDataStatusChanged(evt); } fireDataStatusIEvent("", ""); } /** * Set Parameter for Query. * elements must be Integer, BigDecimal, String (default) * @param pstmt prepared statement * @param countSQL count */ private void setParameter (PreparedStatement pstmt, boolean countSQL) { if (m_parameterSELECT.size() == 0 && m_parameterWHERE.size() == 0) return; try { int pos = 1; // position in Statement // Select Clause Parameters for (int i = 0; !countSQL && i < m_parameterSELECT.size(); i++) { Object para = m_parameterSELECT.get(i); if (para != null) if (log.isLoggable(Level.FINE)) log.fine("Select " + i + "=" + para); // if (para == null) ; else if (para instanceof Integer) { Integer ii = (Integer)para; pstmt.setInt (pos++, ii.intValue()); } else if (para instanceof BigDecimal) pstmt.setBigDecimal (pos++, (BigDecimal)para); else pstmt.setString(pos++, para.toString()); } // Where Clause Parameters for (int i = 0; i < m_parameterWHERE.size(); i++) { Object para = m_parameterWHERE.get(i); if (para != null) if (log.isLoggable(Level.FINE)) log.fine("Where " + i + "=" + para); // if (para == null) ; else if (para instanceof Integer) { Integer ii = (Integer)para; pstmt.setInt (pos++, ii.intValue()); } else if (para instanceof BigDecimal) pstmt.setBigDecimal (pos++, (BigDecimal)para); else pstmt.setString(pos++, para.toString()); } } catch (SQLException e) { log.log(Level.SEVERE, "parameter", e); } } // setParameter } // Loader /** * Feature Request [1707462] * Enable runtime change of VFormat * @param identifier column name * @param strNewFormat new input mask * author fer_luck */ protected void setFieldVFormat (String identifier, String strNewFormat) { int cols = m_fields.size(); for (int i = 0; i < cols; i++) { GridField field = (GridField)m_fields.get(i); if (identifier.equalsIgnoreCase(field.getColumnName())){ field.setVFormat(strNewFormat); m_fields.set(i, field); break; } } } // setFieldVFormat /** * Verify if the record at row has been changed at DB (by other user or process) * @param row row index * @return true if record at row has been changed at DB */ public boolean hasChanged(int row) { // not so aggressive (it can has still concurrency problems) // compare Updated, IsProcessed if (getKeyID(row) > 0) { int colUpdated = findColumn("Updated"); int colProcessed = findColumn("Processed"); boolean hasUpdated = (colUpdated >= 0); boolean hasProcessed = (colProcessed >= 0); String columns = null; if (hasUpdated && hasProcessed) { columns = new String("Updated, Processed"); } else if (hasUpdated) { columns = new String("Updated"); } else if (hasProcessed) { columns = new String("Processed"); } else { // no columns updated or processed to compare return false; } // todo: temporary fix for carlos assumption that all windows have _id column if ( findColumn(m_tableName + "_ID") == -1) return false; Timestamp dbUpdated = null; String dbProcessedS = null; PreparedStatement pstmt = null; ResultSet rs = null; String sql = "SELECT " + columns + " FROM " + m_tableName + " WHERE " + m_tableName + "_ID=?"; try { pstmt = DB.prepareStatement(sql, get_TrxName()); pstmt.setInt(1, getKeyID(row)); rs = pstmt.executeQuery(); if (rs.next()) { int idx = 1; if (hasUpdated) dbUpdated = rs.getTimestamp(idx++); if (hasProcessed) dbProcessedS = rs.getString(idx++); } else if (log.isLoggable(Level.INFO)) log.info("No Value " + sql); } catch (SQLException e) { throw new DBException(e, sql); } finally { DB.close(rs, pstmt); rs = null; pstmt = null; } if (hasUpdated) { Timestamp memUpdated = null; memUpdated = (Timestamp) getOldValue(row, colUpdated); if (memUpdated == null) memUpdated = (Timestamp) getValueAt(row, colUpdated); if (memUpdated != null && ! memUpdated.equals(dbUpdated)) return true; } if (hasProcessed) { Boolean memProcessed = null; memProcessed = (Boolean) getOldValue(row, colProcessed); if (memProcessed == null){ if(getValueAt(row, colProcessed) instanceof Boolean ) memProcessed = (Boolean) getValueAt(row, colProcessed); else if (getValueAt(row, colProcessed) instanceof String ) memProcessed = Boolean.valueOf((String)getValueAt(row, colProcessed)); } Boolean dbProcessed = Boolean.TRUE; if (! dbProcessedS.equals("Y")) dbProcessed = Boolean.FALSE; if (memProcessed != null && ! memProcessed.equals(dbProcessed)) return true; } } // @TODO: configurable aggressive - compare each column with the DB return false; } /** * Verify if po has been changed at DB (by other user or process) * @param po * @return true if po has been changed at DB */ private boolean hasChanged(PO po) { if (m_rowChanged < 0) return false; // not so aggressive (it can has still concurrency problems) // compare Updated, IsProcessed int colUpdated = findColumn("Updated"); int colProcessed = findColumn("Processed"); boolean hasUpdated = colUpdated >= 0; boolean hasProcessed = colProcessed >= 0; if (!hasUpdated && !hasProcessed) { // no columns updated or processed to compare return false; } Timestamp dbUpdated = (Timestamp) po.get_Value("Updated"); if (hasUpdated) { Timestamp memUpdated = null; memUpdated = (Timestamp) getOldValue(m_rowChanged, colUpdated); if (memUpdated == null) memUpdated = (Timestamp) getValueAt(m_rowChanged, colUpdated); if (memUpdated != null && ! memUpdated.equals(dbUpdated)) return true; } if (hasProcessed) { Boolean memProcessed = null; memProcessed = (Boolean) getOldValue(m_rowChanged, colProcessed); if (memProcessed == null){ if(getValueAt(m_rowChanged, colProcessed) instanceof Boolean ) memProcessed = (Boolean) getValueAt(m_rowChanged, colProcessed); else if (getValueAt(m_rowChanged, colProcessed) instanceof String ) memProcessed = Boolean.valueOf((String)getValueAt(m_rowChanged, colProcessed)); } Boolean dbProcessed = po.get_ValueAsBoolean("Processed"); if (memProcessed != null && ! memProcessed.equals(dbProcessed)) return true; } return false; } /** * get Parent Tab No * @return Parent Tab No */ private int getParentTabNo() { int tabNo = m_TabNo; int currentLevel = Env.getContextAsInt(m_ctx, m_WindowNo, tabNo, GridTab.CTX_TabLevel); int parentLevel = currentLevel-1; if (parentLevel < 0) return tabNo; while (parentLevel != currentLevel) { tabNo--; currentLevel = Env.getContextAsInt(m_ctx, m_WindowNo, tabNo, GridTab.CTX_TabLevel); if (tabNo == 0) break; } return tabNo; } /** * get Tab No * @return Tab No */ public int getTabNo() { return m_TabNo; } /** * @param value * @return true if value is not null and is empty string */ private boolean isNotNullAndIsEmpty (Object value) { if (value != null && (value instanceof String) && value.toString().equals("") ) { return true; } else { return false; } } /** * @param oldValue * @param value * @return true if oldValue and value is different */ @SuppressWarnings("unchecked") private boolean isValueChanged(Object oldValue, Object value) { if ( isNotNullAndIsEmpty(oldValue) ) { oldValue = null; } if ( isNotNullAndIsEmpty(value) ) { value = null; } boolean bChanged = (oldValue == null && value != null) || (oldValue != null && value == null); if (!bChanged && oldValue != null) { if (oldValue.getClass().equals(value.getClass())) { if (oldValue instanceof Comparable) { bChanged = (((Comparable)oldValue).compareTo(value) != 0); } else { bChanged = !oldValue.equals(value); } } else if(value != null) { bChanged = !(oldValue.toString().equals(value.toString())); } } return bChanged; } /** * Load PO for row * @param row row index * @return PO */ public PO getPO(int row) { MTable table = MTable.get (m_ctx, m_AD_Table_ID); PO po = null; int Record_ID = getKeyID(row); if (Record_ID != -1) { if (Record_ID == 0 && MTable.isZeroIDTable(table.getTableName())) { String uuidFromZeroID = table.getUUIDFromZeroID(); po = table.getPOByUU(uuidFromZeroID, m_trxName); } else { po = table.getPO(Record_ID, m_trxName); } } else // Multi - Key po = table.getPO(getWhereClause(getDataAtRow(row)), m_trxName); return po; } /** * @param importing import mode * @param trxName optional trx name */ public void setImportingMode(boolean importing, String trxName) { m_importing = importing; m_trxName = trxName; } /** * @return true if it is in import mode */ public boolean isImporting() { return m_importing; } /** * @return trx name */ public String get_TrxName() { return m_trxName; } /** * reset the cache sort state ( sort column and sort ascending ) */ public void resetCacheSortState() { m_lastSortColumnIndex = -1; m_lastSortedAscending = true; } /** * @return index of primary key column */ public int getKeyColumnIndex() { return m_indexKeyColumn; } /** * Index of change row */ public int getRowChanged() { return m_rowChanged; } /** * reset to empty */ public void reset() { if (m_buffer != null) m_buffer.clear(); m_changed = false; m_rowChanged = -1; if (m_sort != null) m_sort.clear(); if (m_virtualBuffer != null) m_virtualBuffer.clear(); m_rowCount = 0; m_rowData = null; m_oldValue = null; m_inserting = false; m_lastSortColumnIndex = -1; m_lastSortedAscending = false; } /** * set current row of gridtable container (gridtab). use in sort to create dse event with new current row (after sort) data * @param m_currentRow */ protected void setCurrentRow(int m_currentRow) { this.m_currentRow = m_currentRow; } }