/* This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2010 Servoy BV This program is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program; if not, see http://www.gnu.org/licenses or write to the Free Software Foundation,Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 */ package com.servoy.j2db.dataprocessing; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import com.servoy.j2db.Messages; import com.servoy.j2db.dataprocessing.SQLSheet.VariableInfo; import com.servoy.j2db.dataprocessing.ValueFactory.DbIdentValue; import com.servoy.j2db.persistence.Column; import com.servoy.j2db.persistence.IColumnTypes; import com.servoy.j2db.util.Debug; import com.servoy.j2db.util.SortedList; import com.servoy.j2db.util.StringComparator; import com.servoy.j2db.util.Utils; /** * Represents one row (containing all columns) from a table * * @author jblok */ public class Row { public enum ROLLBACK_MODE { OVERWRITE_CHANGES, UPDATE_CHANGES, KEEP_CHANGES } public static final Object UNINITIALIZED = new Object(); private Exception lastException; private final RowManager parent; private volatile Object[] columndata;//actual columndata private volatile Object[] oldValues; private final Map<String, Object> unstoredCalcCache; // dataProviderID -> Value private boolean existInDB; private String pkHashKey; private final WeakHashMap<IRowChangeListener, Object> listeners; private static Object dummy = new Object(); private final ConcurrentMap<String, Thread> calculatingThreads = new ConcurrentHashMap<String, Thread>(4); void register(IRowChangeListener r) { synchronized (listeners) { if (!listeners.containsKey(r)) listeners.put(r, dummy); } } public boolean hasListeners() { synchronized (listeners) { return listeners.size() != 0; } } void fireNotifyChange(String name, Object value, FireCollector collector) { ModificationEvent e = new ModificationEvent(name, value, this); Object[] array; synchronized (listeners) { array = listeners.keySet().toArray(); } for (Object element2 : array) { IRowChangeListener element = (IRowChangeListener)element2; element.notifyChange(e, collector); } } Row(RowManager parent, Object[] columndata, Map<String, Object> cc, boolean existInDB) { this.parent = parent; this.columndata = columndata; this.existInDB = existInDB; unstoredCalcCache = cc; listeners = new WeakHashMap<IRowChangeListener, Object>(); // walk over the column data's to see if there is a dbident // if it doesnt have a 'belong to' row yet that this is a // db ident for its pk. If it is set then that dbident comes // from another parent related record. for (Object element : columndata) { if (element instanceof DbIdentValue) { DbIdentValue value = (DbIdentValue)element; if (value.getRow() == null) { value.setRow(this); } } } } //if something is defined as calc it will return the calc value public Object getValue(String id) { Object obj; int columnIndex = parent.getSQLSheet().getColumnIndex(id); if (columnIndex != -1) { obj = getValue(columnIndex); } else { obj = unstoredCalcCache.get(id); } if (obj == UNINITIALIZED) { obj = null; } return obj; } /* * Get value unconverted */ public Object getRawValue(String id) { Object obj; int columnIndex = parent.getSQLSheet().getColumnIndex(id); if (columnIndex != -1) { obj = getRawValue(columnIndex, false); } else { obj = unstoredCalcCache.get(id); } if (obj == UNINITIALIZED) { obj = null; } return obj; } public boolean existInDB() { return existInDB; } public boolean containsCalculation(String id) { return parent.getSQLSheet().containsCalculation(id); } /** * Get row value, converted using column converter * @param columnIndex * @return */ Object getValue(int columnIndex) { // call this with false, else things like using a dbident in javascript or creating related records are going wrong. Object value = getRawValue(columnIndex, false); return parent.getSQLSheet().convertValueToObject(value, columnIndex, parent.getFoundsetManager().getColumnConverterManager()); } /** * Get row value, do not use column converter. * @param columnIndex * @param unwrapDbIdent * @return */ Object getRawValue(int columnIndex, boolean unwrapDbIdent) { if (columnIndex < 0 || columnIndex >= columndata.length) return null; Object obj = columndata[columnIndex]; if (obj instanceof ValueFactory.BlobMarkerValue) { obj = ((ValueFactory.BlobMarkerValue)obj).getCachedData();//see if this is soft cached if (obj == null) { try { Blob b = parent.getBlob(this, columnIndex); if (b != null && b.getBlobData() != null) { obj = b.getBlobData(); } } catch (Exception ex) { Debug.error(ex); } if (obj != null && ((byte[])obj).length > 50000) { columndata[columnIndex] = ValueFactory.createBlobMarkerValue((byte[])obj); if (oldValues != null) { oldValues[columnIndex] = ValueFactory.createBlobMarkerValue((byte[])obj); } } else { columndata[columnIndex] = obj; if (oldValues != null) { oldValues[columnIndex] = obj; } } } } else if (obj instanceof DbIdentValue) { if (((DbIdentValue)obj).getPkValue() != null) { // If the pk value is there just replace it obj = ((DbIdentValue)obj).getPkValue(); columndata[columnIndex] = obj; } else if (unwrapDbIdent) { obj = null; } } return obj; } void setDbIdentValue(Object value) { int identindex = parent.getSQLSheet().getIdentIndex(); if (identindex >= 0) { Object o = columndata[identindex]; // it should always be a db ident value?? if (o instanceof DbIdentValue) { ((DbIdentValue)o).setPkValue(value); } columndata[identindex] = value; } } Object getDbIdentValue() { int identindex = parent.getSQLSheet().getIdentIndex(); if (identindex >= 0) { Object o = columndata[identindex]; if (o == null) { o = ValueFactory.createDbIdentValue().setRow(this); columndata[identindex] = o; } if (o instanceof DbIdentValue) { return o; } } Debug.error("No DbIdent for this row: " + this); //$NON-NLS-1$ return null; } /** * @param dataProviderID * @return */ boolean containsDataprovider(String dataProviderID) { return (containsCalculation(dataProviderID) || parent.getSQLSheet().getColumnIndex(dataProviderID) != -1); } //returns the oldvalue, or value if no change public Object setValue(IRowChangeListener src, String dataProviderID, Object value) { Object o = getRawValue(dataProviderID); if (o instanceof DbIdentValue) return o; // this column is controlled by the database - so do not allow sets until the database chose a value Object convertedValue = value; SQLSheet sheet = parent.getSQLSheet(); int columnIndex = sheet.getColumnIndex(dataProviderID); VariableInfo variableInfo = sheet.getCalculationOrColumnVariableInfo(dataProviderID, columnIndex); if (convertedValue != null && !("".equals(convertedValue) && Column.mapToDefaultType(variableInfo.type) == IColumnTypes.TEXT))//do not convert null to 0 incase of numbers, this means the calcs the value whould change each time //$NON-NLS-1$ { convertedValue = sheet.convertObjectToValue(dataProviderID, convertedValue, parent.getFoundsetManager().getColumnConverterManager(), parent.getFoundsetManager().getColumnValidatorManager()); } else if (parent.getFoundsetManager().isNullColumnValidatorEnabled()) { //check for not null constraint Column c = null; try { c = sheet.getTable().getColumn(dataProviderID); } catch (Exception e) { Debug.error(e); } if (c != null && !c.getAllowNull()) { throw new IllegalArgumentException(Messages.getString("servoy.record.error.validation", new Object[] { dataProviderID, convertedValue })); //$NON-NLS-1$ } } boolean wasUNINITIALIZED = false; if (o == UNINITIALIZED) { o = null; wasUNINITIALIZED = true; } boolean isCalculation = containsCalculation(dataProviderID); //if we receive NULL from the db for Empty strings in Servoy calcs, return value if (o == null && "".equals(convertedValue) && isCalculation) //$NON-NLS-1$ { mustRecalculate(dataProviderID, false); return convertedValue; } if (!Utils.equalObjects(o, convertedValue)) { boolean mustStop = false; if (columnIndex != -1 && columnIndex < columndata.length) { mustStop = !parent.getFoundsetManager().getEditRecordList().isEditing(); if (src != null && existInDB && !wasUNINITIALIZED) //if not yet existInDB, leave startEdit to Foundset new/duplicateRecord code! { src.startEditing(false); } createOldValuesIfNeeded(); columndata[columnIndex] = convertedValue; } else if (isCalculation) { unstoredCalcCache.put(dataProviderID, convertedValue); } lastException = null; // Reset the mustRecalculate here, before setValue fires events, so if it is an every time changing calculation it will not be calculated again and again if (isCalculation) { mustRecalculate(dataProviderID, false); threadCalculationComplete(dataProviderID); } handleCalculationDependencies(sheet.getTable().getColumn(dataProviderID), dataProviderID); FireCollector collector = FireCollector.getFireCollector(); try { fireNotifyChange(dataProviderID, convertedValue, collector); } finally { collector.done(); } if (src != null && mustStop && existInDB && !wasUNINITIALIZED) { try { src.stopEditing(); } catch (Exception e) { Debug.error(e); } } return o; } else if (isCalculation) { // Reset the mustRecalculate here, before setValue fires events, so if it is an every time changing calculation it will not be calculated again and again mustRecalculate(dataProviderID, false); } return convertedValue;//is same so return } /* * Don't call this method if you want to update a value in this row that is completely tracked. Because this method does not create the oldValues backup * array. It just modifies directly the actual values. If isChanged() call is false before the call to this method then after this the isChanged() call is * still false.. */ void setRawValue(String dataProviderID, Object value) { SQLSheet sheet = parent.getSQLSheet(); int columnIndex = sheet.getColumnIndex(dataProviderID); if (columnIndex >= 0 && columnIndex < columndata.length) { columndata[columnIndex] = value; } handleCalculationDependencies(sheet.getTable().getColumn(dataProviderID), dataProviderID); } protected void handleCalculationDependencies(Column column, String dataProviderID) { if (column != null && (column.getFlags() & Column.IDENT_COLUMNS) != 0) { // PK update, recalc hash, update calculation dependencies and fire depending calcs getRowManager().fireDependingCalcsForPKUpdate(this, getPKHashKey()); } else { getRowManager().fireDependingCalcs(getPKHashKey(), dataProviderID, null); } } public Object getOldRequiredValue(String dataProviderID)// incase a primary key is changed { if (oldValues != null && oldValues.length != 0) { SQLSheet.SQLDescription desc = parent.getSQLSheet().getSQLDescription(SQLSheet.UPDATE); List<String> list = desc.getOldRequiredDataProviderIDs(); for (int i = 0; i < list.size(); i++) { String array_element = list.get(i); if (dataProviderID.equals(array_element)) { int columnIndex = parent.getSQLSheet().getColumnIndex(dataProviderID); if (columnIndex >= 0) { if (oldValues[columnIndex] != null) { return oldValues[columnIndex]; } break; } } } } return null;//getValue(dataProviderID, true); } synchronized void createOldValuesIfNeeded() { if (oldValues == null && existInDB) { //clone oldValues = new Object[columndata.length]; System.arraycopy(columndata, 0, oldValues, 0, columndata.length); //Note: what we assume here is that in the 'columndata' the primarey keys are infront and in same order as requiredDataProviderIDs(==primarey keys) //the SQLGenerator does this currently! } return; } //this makes it possible to validate the state before it is processed again due to some listner being fired void flagExistInDB() { if (!isRemoving) { existInDB = true; synchronized (this) { oldValues = null;//dump any old shit } softReferenceAllByteArrays(); } } void clearExistInDB() { existInDB = false; } private void softReferenceAllByteArrays() { for (int i = 0; i < columndata.length; i++) { if (columndata[i] instanceof byte[] && ((byte[])columndata[i]).length > 50000) { columndata[i] = ValueFactory.createBlobMarkerValue((byte[])columndata[i]); } } } String recalcPKHashKey() { pkHashKey = null; return getPKHashKey(); } //See ALSO RowManager.createPKHashKey public String getPKHashKey() { if (pkHashKey == null) { SQLSheet sheet = parent.getSQLSheet(); int[] pkpos = sheet.getPKIndexes(); Object[] pks = new Object[pkpos.length]; for (int i = 0; i < pkpos.length; i++) { if (oldValues != null) { pks[i] = oldValues[pkpos[i]]; } else { pks[i] = columndata[pkpos[i]]; } } pkHashKey = RowManager.createPKHashKey(pks); } return pkHashKey; } public Object[] getPK() { int[] pkpos = parent.getSQLSheet().getPKIndexes(); Object[] retval = new Object[pkpos.length]; for (int i = 0; i < pkpos.length; i++) { Object val = null; if (oldValues != null) { val = oldValues[pkpos[i]]; } else { val = columndata[pkpos[i]]; } if (val instanceof DbIdentValue && ((DbIdentValue)val).getPkValue() != null) { val = ((DbIdentValue)val).getPkValue(); } retval[i] = val; } return retval; } public boolean isChanged() { if (!existInDB) return true; if (oldValues != null) { for (int i = 0; i < oldValues.length; i++) { if (!Utils.equalObjects(oldValues[i], columndata[i])) return true; } oldValues = null; } return false; } public Object[] getRawColumnData() { return columndata; } public Object[] getRawOldColumnData() { return oldValues; } public RowManager getRowManager() { return parent; } boolean lockedByMyself() { return parent.lockedByMyself(this); } void rollbackFromDB() throws Exception { parent.rollbackFromDB(this, true, ROLLBACK_MODE.OVERWRITE_CHANGES); } void rollbackFromDB(ROLLBACK_MODE mode) throws Exception { parent.rollbackFromDB(this, true, mode); } void setRollbackData(Object[] array, ROLLBACK_MODE mode) { Map<String, Object> changedColumns = new HashMap<String, Object>(); String[] columnNames = getRowManager().getSQLSheet().getColumnNames(); synchronized (this) { if (mode == ROLLBACK_MODE.OVERWRITE_CHANGES || oldValues == null) { if (columnNames != null && array.length == columnNames.length) { for (int i = 0; i < array.length; i++) { if (!Utils.equalObjects(array[i], columndata[i])) { changedColumns.put(columnNames[i], array[i]); } } } columndata = array; oldValues = null; } else { if (mode != ROLLBACK_MODE.KEEP_CHANGES) { for (int i = 0; i < oldValues.length; i++) { if (!Utils.equalObjects(oldValues[i], array[i])) { columndata[i] = array[i]; changedColumns.put(columnNames[i], array[i]); } } } oldValues = array; } } fireChanges(changedColumns); } void rollbackFromOldValues() { Map<String, Object> changedColumns = new HashMap<String, Object>(); String[] columnNames = getRowManager().getSQLSheet().getColumnNames(); synchronized (this) { if (oldValues != null) { if (columnNames != null && oldValues.length == columnNames.length) { for (int i = 0; i < oldValues.length; i++) { if (!Utils.equalObjects(oldValues[i], columndata[i])) { changedColumns.put(columnNames[i], oldValues[i]); } } } columndata = oldValues; oldValues = null; } } // maybe is new record, just clear exception lastException = null; fireChanges(changedColumns); } /** * @param changedColumns */ private void fireChanges(Map<String, Object> changedColumns) { if (changedColumns.size() > 0) { for (String dataProviderID : changedColumns.keySet()) { parent.fireDependingCalcs(getPKHashKey(), dataProviderID, null); } parent.fireNotifyChange(null, this, changedColumns.keySet().toArray(), RowEvent.UPDATE); FireCollector collector = FireCollector.getFireCollector(); try { for (String dataProviderID : changedColumns.keySet()) { fireNotifyChange(dataProviderID, changedColumns.get(dataProviderID), collector); } } finally { collector.done(); } } } void setLastException(Exception lastException) { this.lastException = lastException; } Exception getLastException() { return lastException; } @Override public String toString() { String[] columnNames = this.parent.getSQLSheet().getColumnNames(); StringBuilder sb = new StringBuilder(); sb.append("Row("); //$NON-NLS-1$ sb.append(parent.getFoundsetManager().getDataSource(parent.getSQLSheet().getTable())); sb.append(")[DATA:"); //$NON-NLS-1$ for (int i = 0; i < columndata.length; i++) { sb.append(columnNames[i]); sb.append('='); sb.append(columndata[i]); sb.append(','); } sb.append(" CALCULATIONS: "); //$NON-NLS-1$ sb.append(unstoredCalcCache); sb.append(']'); return sb.toString(); } private final SortedList<String> calcsUptodate = new SortedList<String>(StringComparator.INSTANCE, 3); /** Should never be called directly, always use RowManager. * @see RowManager.flagRowCalcForRecalculation * @param dp */ boolean internalFlagCalcForRecalculation(String dp) { synchronized (calcsUptodate) { return calcsUptodate.remove(dp); } } protected List<String> getCalcsUptodate() { synchronized (calcsUptodate) { return new ArrayList<String>(calcsUptodate); } } /** * @param dataProviderID * @return */ public boolean mustRecalculate(String dataProviderID, boolean justTesting) { synchronized (calcsUptodate) { if (!calcsUptodate.contains(dataProviderID)) { if (!justTesting) calcsUptodate.add(dataProviderID); return true; } } return false; } /** * Synchronization for not calculating the same calculation on multiple threads simultaneously...<br> * After calling this method, YOU MUST call in a finally block {@link #threadCalculationComplete()} method. * @param dataProviderID the calculation. */ public void threadWillExecuteCalculation(String dataProviderID) { Thread currentThread = Thread.currentThread(); Thread previous = calculatingThreads.putIfAbsent(dataProviderID, currentThread); if (previous != null && previous != currentThread) { long time = System.currentTimeMillis(); try { previous = calculatingThreads.putIfAbsent(dataProviderID, currentThread); while (previous != null && previous != currentThread && System.currentTimeMillis() < (time + 5000)) { synchronized (calculatingThreads) { calculatingThreads.wait(1000); } previous = calculatingThreads.putIfAbsent(dataProviderID, currentThread); } } catch (InterruptedException e) { //ignore } if (previous != null && previous != currentThread) { try { StackTraceElement[] stackTrace = previous.getStackTrace(); StringBuilder sb = new StringBuilder(); sb.append("Calc '" + dataProviderID + "' did time out for thread: " + currentThread.getName() + " still waiting for: " + previous.getName() + ", stack:"); for (StackTraceElement stackTraceElement : stackTrace) { sb.append("\n"); sb.append(stackTraceElement.toString()); } Debug.error(sb.toString(), new RuntimeException("calc timeout")); } catch (Exception e) { } } } } /** * Tell the row that current thread has finished calculating this calculation. * @param dataProviderID the calculation. */ public void threadCalculationComplete(String dataProviderID) { calculatingThreads.remove(dataProviderID, Thread.currentThread()); synchronized (calculatingThreads) { calculatingThreads.notifyAll(); } } private boolean isRemoving = false; public void remove() { isRemoving = true; Object[] array; synchronized (listeners) { array = listeners.keySet().toArray(); } for (Object element2 : array) { IRowChangeListener element = (IRowChangeListener)element2; element.rowRemoved(); } } }