/*
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.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import org.mozilla.javascript.NativeJavaMethod;
import org.mozilla.javascript.Scriptable;
import com.servoy.base.query.IBaseSQLCondition;
import com.servoy.base.scripting.annotations.ServoyClientSupport;
import com.servoy.j2db.dataprocessing.ValueFactory.DbIdentValue;
import com.servoy.j2db.persistence.Column;
import com.servoy.j2db.persistence.IRepository;
import com.servoy.j2db.persistence.Relation;
import com.servoy.j2db.persistence.RepositoryException;
import com.servoy.j2db.query.AbstractBaseQuery;
import com.servoy.j2db.query.AndOrCondition;
import com.servoy.j2db.query.IQuerySelectValue;
import com.servoy.j2db.query.ISQLSelect;
import com.servoy.j2db.query.Placeholder;
import com.servoy.j2db.query.QueryColumn;
import com.servoy.j2db.query.QuerySelect;
import com.servoy.j2db.query.TablePlaceholderKey;
import com.servoy.j2db.util.Debug;
import com.servoy.j2db.util.SafeArrayList;
import com.servoy.j2db.util.ServoyException;
import com.servoy.j2db.util.Utils;
import com.servoy.j2db.util.visitor.PackVisitor;
/**
* This class is normally found as related state from another state and therefore holds related data
*
* @author jblok
*/
public abstract class RelatedFoundSet extends FoundSet
{
private static NativeJavaMethod maxRecord;
static
{
try
{
maxRecord = new NativeJavaMethod(FoundSet.class.getMethod("js_getMaxRecordIndex", (Class[])null), "getMaxRecordIndex"); //$NON-NLS-1$ //$NON-NLS-2$
}
catch (Exception e)
{
Debug.error(e);
}
}
protected RelatedFoundSet(IDataSet data, QuerySelect select, IFoundSetManagerInternal app, IRecordInternal parent, String relationName, SQLSheet sheet,
List<SortColumn> defaultSortColumns, QuerySelect aggregateSelect, IDataSet aggregateData) throws ServoyException
{
super(app, parent, relationName, sheet, null, defaultSortColumns);
if (data == null)
{
getPksAndRecords().setPksAndQuery(new BufferedDataSet(), 0, select);
}
else
{
IDataSet pks = createPKDataSet(sheet, data);
SafeArrayList<IRecordInternal> cachedRecords = getPksAndRecords().setPksAndQuery(pks, pks.getRowCount(), select);
for (int row = 0; row < pks.getRowCount(); row++)
{
Object[] rowArray = data.getRow(row);
// fire delayed so that this constructor will end before fires will touch this relatedfoundset again.
List<Runnable> fireRunnables = new ArrayList<Runnable>(1);
Row rowData = rowManager.getRowBasedonPKFromEntireColumnArray(rowArray, fireRunnables);
fsm.registerFireRunnables(fireRunnables);
Record state = new Record(this, rowData);
cachedRecords.set(row, state);
}
if (pks != null && pks.getRowCount() > 0)
{
setSelectedIndex(0);
}
}
//fix the select from related_SQL to relatedPK_SQL
ArrayList<IQuerySelectValue> pkColumns = new ArrayList<IQuerySelectValue>();
Iterator<Column> pkIt = sheet.getTable().getRowIdentColumns().iterator();
while (pkIt.hasNext())
{
Column column = pkIt.next();
pkColumns.add(new QueryColumn(select.getTable(), column.getID(), column.getSQLName(), column.getType(), column.getLength(), column.getScale(),
column.getFlags()));
}
select.setColumns(pkColumns);
creationSqlSelect = AbstractBaseQuery.deepClone(select);
if (aggregateData != null)
{
fillAggregates(aggregateSelect, aggregateData);
}
initialized = true;
}
//can only used by findState
protected RelatedFoundSet(IFoundSetManagerInternal app, IRecordInternal parent, String relationName, SQLSheet sheet) throws ServoyException
{
super(app, parent, relationName, sheet, null, null);
getPksAndRecords().setPksAndQuery(new BufferedDataSet(), 0, null);
initialized = true;
}
/**
* Create multiple related foundsets in one call to the data server.
*
* @param factory
* @param app
* @param parents same length as whereArsgLists
* @param relation
* @param sheet
* @param whereArsgLists
* @param defaultSortColumns
* @return
* @throws ServoyException
*/
public static IFoundSetInternal[] createRelatedFoundSets(IFoundSetFactory factory, IFoundSetManagerInternal app, IRecordInternal[] parents,
Relation relation, SQLSheet sheet, Object[][] whereArsgLists, List<SortColumn> defaultSortColumns) throws ServoyException
{
if (sheet == null)
{
throw new IllegalArgumentException(app.getApplication().getI18NMessage("servoy.foundSet.error.sqlsheet")); //$NON-NLS-1$
}
FoundSetManager fsm = (FoundSetManager)app;
List<SortColumn> sortColumns;
if (defaultSortColumns == null || defaultSortColumns.size() == 0)
{
sortColumns = sheet.getDefaultPKSort();
}
else
{
sortColumns = defaultSortColumns;
}
QuerySelect cleanSelect = fsm.getSQLGenerator().getPKSelectSqlSelect(fsm.getScopesScopeProvider(), sheet.getTable(), null, null, true, null,
sortColumns, false);
QuerySelect relationSelect = (QuerySelect)sheet.getRelatedSQLDescription(relation.getName()).getSQLQuery();
//don't select all columns in pk select
cleanSelect.setColumns(AbstractBaseQuery.relinkTable(relationSelect.getTable(), cleanSelect.getTable(), relationSelect.getColumnsClone()));
//copy the where (is foreign where)
cleanSelect.setCondition(SQLGenerator.CONDITION_RELATION,
AbstractBaseQuery.relinkTable(relationSelect.getTable(), cleanSelect.getTable(), relationSelect.getConditionClone(SQLGenerator.CONDITION_RELATION)));
TablePlaceholderKey placeHolderKey = SQLGenerator.createRelationKeyPlaceholderKey(cleanSelect.getTable(), relation.getName());
QuerySelect[] sqlSelects = new QuerySelect[whereArsgLists.length]; // all queries
QuerySelect[] aggregateSelects = new QuerySelect[whereArsgLists.length]; // all aggregates
List<Integer> queryIndex = new ArrayList<Integer>(whereArsgLists.length);
Map<Integer, Row> cachedRows = new HashMap<Integer, Row>();
List<QueryData> queryDatas = new ArrayList<QueryData>(whereArsgLists.length);
String transactionID = fsm.getTransactionID(sheet);
String clientID = fsm.getApplication().getClientID();
ArrayList<TableFilter> sqlFilters = fsm.getTableFilterParams(sheet.getServerName(), cleanSelect);
for (int i = 0; i < whereArsgLists.length; i++)
{
Object[] whereArgs = whereArsgLists[i];
if (whereArgs == null || whereArgs.length == 0)
{
throw new IllegalArgumentException(app.getApplication().getI18NMessage("servoy.relatedfoundset.error.noFK") + relation.getName()); //$NON-NLS-1$
}
QuerySelect sqlSelect;
if (i == whereArsgLists.length - 1)
{
sqlSelect = cleanSelect; // the last one, use the template, no clone needed
}
else
{
sqlSelect = AbstractBaseQuery.deepClone(cleanSelect);
}
if (!sqlSelect.setPlaceholderValue(placeHolderKey, whereArgs))
{
Debug.error(new RuntimeException("Could not set placeholder " + placeHolderKey + " in query " + sqlSelect + "-- continuing")); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
}
sqlSelects[i] = sqlSelect;
// Check for non-empty where-arguments, joins on null-conditions are not allowed (similar to FK constraints in databases)
if (!whereArgsIsEmpty(whereArgs))
{
Row cachedRow = null;
if (relation.isFKPKRef())
{
// optimize for FK->PK relation, if the data is already cached, do not query
RowManager rowManager = fsm.getRowManager(relation.getForeignDataSource());
if (rowManager != null)
{
cachedRow = rowManager.getCachedRow(whereArgs).getLeft();
}
}
if (cachedRow != null)
{
if (Debug.tracing())
{
Debug.trace(Thread.currentThread().getName() + ": Found cached FK record"); //$NON-NLS-1$
}
cachedRows.put(Integer.valueOf(i), cachedRow);
}
else
{
ISQLSelect selectStatement = AbstractBaseQuery.deepClone((ISQLSelect)sqlSelect);
// Note: put a clone of sqlSelect in the queryDatas list, we will compress later over multiple queries using pack().
// Clone is needed because packed queries may not be save to manipulate.
SQLStatement trackingInfo = null;
if (fsm.getEditRecordList().hasAccess(sheet.getTable(), IRepository.TRACKING_VIEWS))
{
trackingInfo = new SQLStatement(ISQLActionTypes.SELECT_ACTION, sheet.getServerName(), sheet.getTable().getName(), null, null);
trackingInfo.setTrackingData(sheet.getColumnNames(), new Object[][] { }, new Object[][] { }, fsm.getApplication().getUserUID(),
fsm.getTrackingInfo(), fsm.getApplication().getClientID());
}
queryDatas.add(new QueryData(selectStatement, sqlFilters, !sqlSelect.isUnique(), 0, fsm.initialRelatedChunkSize,
IDataServer.RELATION_QUERY, trackingInfo));
queryIndex.add(Integer.valueOf(i));
QuerySelect aggregateSelect = FoundSet.getAggregateSelect(sheet, sqlSelect);
if (aggregateSelect != null)
{
// Note: see note about clone above.
queryDatas.add(new QueryData(AbstractBaseQuery.deepClone((ISQLSelect)aggregateSelect), fsm.getTableFilterParams(sheet.getServerName(),
aggregateSelect), false, 0, 1, IDataServer.AGGREGATE_QUERY, null));
queryIndex.add(Integer.valueOf(i)); // same index for aggregates
aggregateSelects[i] = aggregateSelect;
}
}
}
}
IDataSet[] dataSets = null;
if (queryDatas.size() > 0)
{
try
{
// pack is safe here because queryDatas contains only cloned ISQLSelect objects
QueryData[] qDatas = queryDatas.toArray(new QueryData[queryDatas.size()]);
AbstractBaseQuery.acceptVisitor(qDatas, new PackVisitor());
int size = 0;
if (Debug.tracing()) // trace the message size
{
try
{
ByteArrayOutputStream bs = new ByteArrayOutputStream();
ObjectOutputStream os = new ObjectOutputStream(bs);
os.writeObject(qDatas);
os.close();
size = bs.size();
}
catch (Exception e)
{
Debug.trace(e);
}
}
long time = System.currentTimeMillis();
dataSets = fsm.getDataServer().performQuery(clientID, sheet.getServerName(), transactionID, qDatas);
if (Debug.tracing())
{
Debug.trace(Thread.currentThread().getName() + ": Relation query: " + relation.getName() + " with: " + qDatas.length + //$NON-NLS-1$ //$NON-NLS-2$
" queries,query size: " + size + ",time: " + (System.currentTimeMillis() - time) + "ms"); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$
}
}
catch (RepositoryException re)
{
testException(clientID, re);
throw re;
}
catch (RemoteException e)
{
testException(clientID, e.getCause());
throw new RepositoryException(e);
}
}
IFoundSetInternal[] foundsets = new RelatedFoundSet[whereArsgLists.length];
int d = 0;
for (int i = 0; i < whereArsgLists.length; i++)
{
IDataSet data;
IDataSet aggregateData = null;
int index = (d >= queryIndex.size()) ? -1 : queryIndex.get(d).intValue();
if (index == i)
{
// regular query
data = dataSets[d++];
// optionally followed by aggregate
index = (d >= queryIndex.size()) ? -1 : queryIndex.get(d).intValue();
if (index == i)
{
// aggregate
aggregateData = dataSets[d++];
}
}
else
{
data = new BufferedDataSet();
Row row = cachedRows.get(Integer.valueOf(i));
if (row != null)
{
// cached
data.addRow(row.getRawColumnData());
}
// else whereArgsIsEmpty
}
foundsets[i] = factory.createRelatedFoundSet(data, sqlSelects[i], app, parents[i], relation.getName(), sheet, sortColumns, aggregateSelects[i],
aggregateData);
if (aggregateData != null && foundsets[i] instanceof FoundSet)
{
((FoundSet)foundsets[i]).fillAggregates(aggregateSelects[i], aggregateData);
}
}
if (d != queryIndex.size())
{
// should never happen!
throw new RepositoryException("Related query parameters out of sync " + d + '/' + queryIndex.size()); //$NON-NLS-1$
}
return foundsets;
}
private static void testException(String clientID, Throwable t)
{
if (Debug.tracing() && t instanceof RepositoryException)
{
RepositoryException re = (RepositoryException)t;
if (re.getErrorCode() == ServoyException.InternalCodes.CLIENT_NOT_REGISTERED)
{
Debug.trace("Client not registered on server, expecting a reconnect: " + clientID, re); //$NON-NLS-1$
}
else
{
Debug.trace("Error getting related foundsets for clientID: " + clientID, re); //$NON-NLS-1$
}
}
}
//related foundsets based on null cannot exist by SQL definition
private static boolean whereArgsIsEmpty(Object[] whereArgs)
{
boolean empty = true;
for (Object element : whereArgs)
{
if (element instanceof DbIdentValue && ((DbIdentValue)element).getPkValue() == null) return true;
if (element != null) empty = false;
}
return empty;//only return true when all null (in case of multi key)
}
@Override
protected int newRecord(Row rowData, int indexToAdd, boolean changeSelection, boolean javascriptRecord) throws ServoyException
{
checkQueryForUpdates();
return super.newRecord(rowData, indexToAdd, changeSelection, javascriptRecord);
}
@Override
protected int getRecordIndex(Object[] pk)
{
checkQueryForUpdates(); // make foundset is loaded, the query may insert records before record parameter.
return super.getRecordIndex(pk);
}
@Override
public void loadAllRecords() throws ServoyException
{
// Also clear omit in browse all/refresh from db
// don't do it in refreshFromDb because then
// the omits can be cleared if there is a refresh
// from db coming from outside or a search that has no results.
clearOmit(null);
refreshFromDBInternal(AbstractBaseQuery.deepClone(creationSqlSelect), true, false, fsm.pkChunkSize, false, false);
}
@Override
public int getSize()
{
checkQueryForUpdates();
return super.getSize();
}
/**
* Get the arguments hash this related foundSet was created with. Hash is based on values converted using column converters.
* @return
*/
public String getWhereArgsHash()
{
Placeholder ph = creationSqlSelect.getPlaceholder(SQLGenerator.createRelationKeyPlaceholderKey(creationSqlSelect.getTable(), getRelationName()));
if (ph == null || !ph.isSet())
{
if (!findMode)
{
Debug.error(
"RelatedFoundset, creation args not found\nplaceholder=" + ph + "\nrelation=" + getRelationName() + "\ncreationSqlSelect=" + creationSqlSelect, new RuntimeException("RelatedFoundset, creation args not found!!")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
return null; // how can this happen (other then in find mode) ??
}
Relation relation = fsm.getApplication().getFlattenedSolution().getRelation(relationName);
if (relation == null)
{
throw new IllegalStateException("Relation not found for related foundset: " + relationName); //$NON-NLS-1$
}
Object[][] foreignData = (Object[][])ph.getValue();
Column[] columns;
try
{
columns = relation.getForeignColumns();
}
catch (RepositoryException e)
{
Debug.error(e);
throw new IllegalStateException("Relation columns not found for related foundset: " + relationName); //$NON-NLS-1$
}
if (columns.length != foreignData.length)
{
// safety check, should not happen
throw new IllegalStateException("Relation where-args inconsistent with columns for relation" + relationName); //$NON-NLS-1$
}
Object[] whereArgs = new Object[foreignData.length];
for (int i = 0; i < foreignData.length; i++)
{
// Use converted value for hash
int colindex = getSQLSheet().getColumnIndex(columns[i].getDataProviderID());
whereArgs[i] = getSQLSheet().convertValueToObject(foreignData[i][0], colindex, fsm.getColumnConverterManager());
}
return RowManager.createPKHashKey(whereArgs);
}
@Override
public boolean addFilterParam(String filterName, String dataprovider, String operator, Object value)
{
// don't do anything, can't add parameters to related foundset
fsm.getApplication().reportJSError("Cannot addFoundSetFilterParam to related foundset", null); //$NON-NLS-1$
return false;//would also cause problem due to sharing related foundsets for different records
}
@Override
@ServoyClientSupport(mc = false, wc = false, sc = false)
public void js_clear()
{
// don't do anything, can't clear related data
fsm.getApplication().reportJSError("Clearing a relatedfoundset is not possible", null); //$NON-NLS-1$
}
private static IDataSet createPKDataSet(SQLSheet sheet, IDataSet data)//extract primary columns from all the related data
{
if (data.getRowCount() == 0)//performance enhancement
{
return new BufferedDataSet();
}
int[] columns = sheet.getPKIndexes();// new int[old.size()];
return new BufferedDataSet(data, columns);
}
@Override
public Object get(java.lang.String name, Scriptable start)
{
if ("recordIndex".equals(name)) //$NON-NLS-1$
{
return Integer.valueOf(getSelectedIndex() + 1);
}
else if ("getMaxRecordIndex".equals(name)) //adding a method here must also be added in Ident.java //$NON-NLS-1$
{
return maxRecord;
}
return super.get(name, start);
}
@Override
public boolean has(String name, Scriptable start)
{
boolean b = super.has(name, start);
if (!b)
{
b = "getMaxRecordIndex".equals(name); //$NON-NLS-1$
}
return b;
}
@Override
public void put(java.lang.String name, Scriptable start, java.lang.Object value)
{
if ("recordIndex".equals(name)) //$NON-NLS-1$
{
int i = Utils.getAsInteger(value) - 1;
if (i >= 0 && i < getSize())
{
setSelectedIndex(i);
}
return;
}
else if ("getMaxRecordIndex".equals(name)) //$NON-NLS-1$
{
// don't set
return;
}
super.put(name, start, value);
}
/*
* _____________________________________________________________ Methods from AbstractEditListModel for use if portal is using a JEditList
*/
@Override
public IRecordInternal getRecord(int row)
{
checkQueryForUpdates();
return super.getRecord(row);//for new/dup records (ontop records are handled above)
}
/**
* @see com.servoy.j2db.dataprocessing.FoundSet#recordsUpdated(java.util.List, java.util.List, java.util.HashMap)
*/
@Override
protected void recordsUpdated(List<Record> records, List<String> aggregatesToRemove)
{
super.recordsUpdated(records, aggregatesToRemove);
for (int i = 0; i < records.size(); i++)
{
IRecordInternal record = records.get(i);
notifyChange_checkForUpdate(record.getRawData(), false);
}
}
private boolean isInNotify = false;
@Override
public void notifyChange(RowEvent e) //this method is only called if I'm not the source of the event
{
if (!isInNotify)//prevent circle calling
{
try
{
isInNotify = true;
// ROW CAN BE NULL ON UPDATE
Row r = e.getRow();
if (e.getType() == RowEvent.INSERT)
{
notifyChange_checkForNewRow(r);
}
else
{
if ((e.getType() == RowEvent.UPDATE //
|| (e.getType() == RowEvent.PK_UPDATED && e.getOldPkHash() == null)) // pk was updated by another client (oldpkhash is filled when updated by self)
&&
getPksAndRecords().getPks() != null)
{
if (r == null || (e.getType() == RowEvent.PK_UPDATED && e.getOldPkHash() == null))
{
// cached row was not found, check if a column was updated that the relation depends on
if (e.getChangedColumnNames() != null)
{
if (hasForeignColumn(e.getChangedColumnNames()))
{
invalidateFoundset();
getFoundSetManager().getEditRecordList().fireEvents();
}
return;//make sure processing stops here
}
}
else
{
boolean mustLookForMore = notifyChange_checkForUpdate(r, true);
if (mustLookForMore)//this code is entered when a foreign column is changed and row falls into this foundset
{
notifyChange_checkForNewRow(r);
}
return;//make sure processing stops here
}
}
super.notifyChange(e);
}
}
finally
{
isInNotify = false;
}
}
}
private boolean hasForeignColumn(Object[] changedColumnNames)
{
if (changedColumnNames != null)
{
Relation relation = fsm.getApplication().getFlattenedSolution().getRelation(relationName);
if (relation == null)
{
throw new IllegalStateException("Relation not found for related foundset: " + relationName); //$NON-NLS-1$
}
try
{
Column[] foreignColumns = relation.getForeignColumns();
for (Column column : foreignColumns)
{
for (Object columnName : changedColumnNames)
{
if (column.getName().equals(columnName))
{
return true;
}
}
}
}
catch (RepositoryException e)
{
Debug.error(e);
}
}
return false;
}
private boolean notifyChange_checkForUpdate(Row r, boolean updateTest)
{
// if already in state for new query then don't test anything.
if (mustQueryForUpdates)
{
return false;
}
boolean retval = true;
String pkHash = r.getPKHashKey();
Relation relation = fsm.getApplication().getFlattenedSolution().getRelation(relationName);
if (relation == null)
{
throw new IllegalStateException("Relation not found for related foundset: " + relationName); //$NON-NLS-1$
}
Placeholder whereArgsPlaceholder = creationSqlSelect.getPlaceholder(SQLGenerator.createRelationKeyPlaceholderKey(creationSqlSelect.getTable(),
relation.getName()));
if (whereArgsPlaceholder == null || !whereArgsPlaceholder.isSet())
{
Debug.error("creationSqlSelect = " + creationSqlSelect); //$NON-NLS-1$
Debug.error("PlaceholderKey = " + SQLGenerator.createRelationKeyPlaceholderKey(creationSqlSelect.getTable(), relation.getName())); //$NON-NLS-1$
Debug.error("RelatedFoundSet = " + this); //$NON-NLS-1$
Debug.error("Row = " + r); //$NON-NLS-1$
Debug.error("whereArgsPlaceholder = " + whereArgsPlaceholder); //$NON-NLS-1$
// log on server as well
try
{
fsm.getApplication().getDataServer().logMessage("creationSqlSelect = " + creationSqlSelect + '\n' + "PlaceholderKey = " + //$NON-NLS-1$ //$NON-NLS-2$
SQLGenerator.createRelationKeyPlaceholderKey(creationSqlSelect.getTable(), relation.getName()) + '\n' + "RelatedFoundSet = " + this + '\n' + //$NON-NLS-1$
"Row = " + r + '\n' + "whereArgsPlaceholder = " + whereArgsPlaceholder); //$NON-NLS-1$//$NON-NLS-2$
}
catch (Exception e)
{
Debug.error("Failed to log on server", e); //$NON-NLS-1$
}
Debug.error("Relation values not set for related foundset " + relation.getName()); //$NON-NLS-1$
return false;
}
// createWhereArgs is a matrix as wide as the relation keys and 1 deep
Object[][] createWhereArgs = (Object[][])whereArgsPlaceholder.getValue(); // Really get only the where params not all of them (like table filter)
SQLSheet.SQLDescription sqlDesc = sheet.getRelatedSQLDescription(relationName);
List<String> req = sqlDesc.getRequiredDataProviderIDs();//foreign key names
if (createWhereArgs.length != req.size())
{
Debug.error("RelatedFoundset check for updated row, creation args and relation args are not the same!!"); //$NON-NLS-1$
return false;//how can this happen??
}
IDataSet pks = getPksAndRecords().getPks();
if (pks != null)
{
int[] operators = relation.getOperators();
for (int i = 0; i < req.size(); i++)
{
String foreignKeyName = req.get(i);
// createWhereArgs is a matrix as wide as the relation keys and 1 deep, compare unconverted values
boolean remove = !checkForeignKeyValue(r.getRawValue(foreignKeyName), createWhereArgs[i][0], operators[i]);
if (remove)
{
retval = false;
//row is not longer part of this related foundset, so remove in myself
for (int ii = pks.getRowCount() - 1; ii >= 0; ii--)
{
Object[] pk = pks.getRow(ii);
if (RowManager.createPKHashKey(pk).equals(pkHash))
{
removeRecordInternal(ii);//does fireIntervalRemoved(this,ii,ii);
break;
}
}
break;
}
}
if (retval && updateTest)
{
for (int ii = pks.getRowCount() - 1; ii >= 0; ii--)
{
Object[] pk = pks.getRow(ii);
if (RowManager.createPKHashKey(pk).equals(pkHash))
{
fireAggregateChangeWithEvents(getRecord(ii));
retval = false;
break;
}
}
}
}
return retval;
}
private boolean checkForeignKeyValue(Object obj, Object whereArg, int operator)
{
int maskedOperator = operator & IBaseSQLCondition.OPERATOR_MASK;
if (Utils.equalObjects(whereArg, obj, true))
{
return (maskedOperator == IBaseSQLCondition.EQUALS_OPERATOR || maskedOperator == IBaseSQLCondition.GTE_OPERATOR ||
maskedOperator == IBaseSQLCondition.LTE_OPERATOR || maskedOperator == IBaseSQLCondition.LIKE_OPERATOR);
}
if ((operator & IBaseSQLCondition.ORNULL_MODIFIER) != 0 && Utils.equalObjects(obj, null))
{
return true;
}
if (maskedOperator == IBaseSQLCondition.NOT_OPERATOR)
{
return true;
}
boolean equal = false;
if (maskedOperator == IBaseSQLCondition.LIKE_OPERATOR)
{
if (obj == null) return false;
// For LIKE we make case-insensitive comparison.
String arg = whereArg.toString().toUpperCase();
String objString = obj.toString().toUpperCase();
StringTokenizer st = new StringTokenizer(arg, "%", true); //$NON-NLS-1$
List<String> al = new ArrayList<String>();
while (st.hasMoreTokens())
{
al.add(st.nextToken());
}
boolean startsWidth = true;
int prevIndex = 0;
for (int i = 0; i < al.size(); i++)
{
String tokenOrString = al.get(i);
if ("%".equals(tokenOrString)) //$NON-NLS-1$
{
startsWidth = false;
// if only % (left) then everything is equal
equal = true;
}
else
{
if (startsWidth)
{
equal = objString.startsWith(tokenOrString);
}
else
{
if (al.size() > i + 1)
{
prevIndex = objString.indexOf(tokenOrString, prevIndex);
equal = prevIndex != -1;
}
else
{
equal = objString.endsWith(tokenOrString);
}
}
if (!equal)
{
break;
}
}
}
}
else if (maskedOperator != IBaseSQLCondition.EQUALS_OPERATOR)
{
// Now test the GT(E) and LT(E) if possible (Comparable)
if (obj instanceof Comparable && whereArg instanceof Comparable)
{
int compare = 0;
if (whereArg instanceof String)
{
compare = ((String)whereArg).compareToIgnoreCase((String)obj);
}
else if (obj instanceof Number && whereArg instanceof Number && obj.getClass() != whereArg.getClass())
{
compare = (int)(((Number)whereArg).doubleValue() - ((Number)obj).doubleValue());
}
else
{
compare = ((Comparable)whereArg).compareTo(obj);
}
if (maskedOperator == IBaseSQLCondition.GT_OPERATOR || maskedOperator == IBaseSQLCondition.GTE_OPERATOR)
{
equal = compare >= 0;
}
else if (maskedOperator == IBaseSQLCondition.LT_OPERATOR || maskedOperator == IBaseSQLCondition.LTE_OPERATOR)
{
equal = compare <= 0;
}
}
}
return equal;
}
private void notifyChange_checkForNewRow(Row row)
{
// if already in state for new query then don't test anything.
if (mustQueryForUpdates)
{
return;
}
// check if sql where is still the same, if there is search in it do nothing
AndOrCondition createCondition = creationSqlSelect.getCondition(SQLGenerator.CONDITION_RELATION);
AndOrCondition condition = getPksAndRecords().getQuerySelectForReading().getCondition(SQLGenerator.CONDITION_RELATION);
if ((createCondition == null && condition == null) || createCondition != null && createCondition.equals(condition)) // does not include placeholder values in comparison
{
try
{
boolean doCheck = true;
Relation relation = fsm.getApplication().getFlattenedSolution().getRelation(relationName);
if (relation == null)
{
// this may happen when the relation was removed using solution model
return;
}
//check the foreign key if they match, if so it will fall in this foundset
Placeholder ph = creationSqlSelect.getPlaceholder(SQLGenerator.createRelationKeyPlaceholderKey(creationSqlSelect.getTable(), relation.getName()));
if (ph == null || !ph.isSet())
{
Column[] cols = relation.getForeignColumns();
StringBuilder columns = new StringBuilder();
columns.append("(");
if (cols != null && cols.length > 0)
{
for (Column col : cols)
{
columns.append(col.getName());
columns.append(",");
}
columns.setLength(columns.length() - 1);
}
columns.append(")");
Debug.error("RelatedFoundset check for relation:" + relationName + " for a new row, creation args " + columns + "not found!!"); //$NON-NLS-1$
return;//how can this happen??
}
// foreignData is a matrix as wide as the relation keys and 1 deep
Object[][] foreignData = (Object[][])ph.getValue(); // Really get only the where params not all of them (like table filter)
Column[] cols = relation.getForeignColumns();
if (foreignData.length != cols.length)
{
StringBuilder columns = new StringBuilder();
columns.append("(");
if (cols.length > 0)
{
for (Column col : cols)
{
columns.append(col.getName());
columns.append(",");
}
columns.setLength(columns.length() - 1);
}
columns.append(")");
StringBuilder data = new StringBuilder();
data.append("(");
if (foreignData.length > 0)
{
for (Object[] d : foreignData)
{
data.append("[");
if (d.length > 0)
{
for (Object object : d)
{
data.append(object);
data.append(",");
}
data.setLength(data.length() - 1);
}
data.append("]");
}
data.setLength(data.length() - 1);
}
data.append(")");
Debug.error("RelatedFoundset check for relation:" + relationName + " for new row, creation args " + columns + " and relation args " + data + " are not the same!!"); //$NON-NLS-1$
return;//how can this happen??
}
int[] operators = relation.getOperators();
for (int i = 0; i < cols.length; i++)
{
Object obj = row.getRawValue(cols[i].getDataProviderID()); // compare unconverted values
if (!checkForeignKeyValue(obj, foreignData[i][0], operators[i]))
{
doCheck = false;
break;
}
}
if (doCheck)
{
invalidateFoundset();
getFoundSetManager().getEditRecordList().fireEvents();
}
}
catch (Exception ex)
{
Debug.error(ex);
}
}
}
protected void invalidateFoundset()
{
if (!mustQueryForUpdates)
{
int size = getCorrectedSizeForFires();
IRecordInternal record = getRecord(getSelectedIndex());
mustQueryForUpdates = true;
Map<FoundSet, int[]> parentToIndexen = getFoundSetManager().getEditRecordList().getFoundsetEventMap();
// if (size >= 0)
// {
// parentToIndexen.put(this, new int[] { 0, size });
// }
// else
{
// when foundset was empty (size = -1) this will be fired as an foundset-invalidated event, see EditRecordList.fireEvents()
parentToIndexen.put(this, new int[] { -1, -1 });
}
fireAggregateChange(record);
}
}
private void checkQueryForUpdates()
{
// Only do a new query if the foundset is in a mustQuery state
// AND if it doesn't have edited records. These records should first be saved before
// doing a query for pks. Else those records could be removed from this related foundset
// and never return again (or the next mustQuery comes around)
if (mustQueryForUpdates && !fsm.getEditRecordList().hasEditedRecords(this, false))
{
try
{
long time = System.currentTimeMillis();
int oldSize = getRawSize();
String transaction_id = fsm.getTransactionID(sheet);
QuerySelect sqlSelect = getPksAndRecords().getQuerySelectForReading();
IDataSet pks = performQuery(transaction_id, sqlSelect, !sqlSelect.isUnique(), 0, Math.max(getSelectedIndex(), fsm.chunkSize) + fsm.chunkSize,
IDataServer.FOUNDSET_LOAD_QUERY);
if (Debug.tracing())
{
Debug.trace(Thread.currentThread().getName() +
": Related CheckForUpdate DB time: " + (System.currentTimeMillis() - time) + " pks: " + pks.getRowCount() + ", SQL: " + sqlSelect.toString()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
// optimistic locking, if the query has been changed in the mean time forget about the refresh
synchronized (getPksAndRecords())
{
if (sqlSelect != getPksAndRecords().getQuerySelectForReading())
{
Debug.trace("checkQueryForUpdates: query was changed during refresh, not resetting old query"); //$NON-NLS-1$
mustQueryForUpdates = false;
return;
}
getPksAndRecords().setPksAndQuery(pks, pks.getRowCount(), sqlSelect);
mustQueryForUpdates = false;
}
//do kind of browseAll/refresh from db.
// differences: selected record isn't tried to keep in sync with pk, new records are handled different.
// boolean must be false before clear (that fires a aggregate change so again a getRecord())
clearInternalState(true);
fireDifference(oldSize, getRawSize());
}
catch (ServoyException ex)
{
fsm.getApplication().reportError("Error quering for new pks in relatedfoundset", ex); //$NON-NLS-1$
throw new RuntimeException("Error quering for new pks in relatedfoundset", ex); //$NON-NLS-1$
}
catch (Exception ex)
{
Debug.error(ex);
throw new RuntimeException("Error quering for new pks in relatedfoundset", ex); //$NON-NLS-1$
}
}
else if (mustQueryForUpdates)
{
Debug.trace("checkQueryForUpdates: skipping because there were edited records"); //$NON-NLS-1$
}
}
@Override
@ServoyClientSupport(mc = false, wc = false, sc = false)
public boolean js_addFoundSetFilterParam(String dataprovider, String operator, Object value) throws ServoyException
{
return super.js_addFoundSetFilterParam(dataprovider, operator, value);
}
@Override
@ServoyClientSupport(mc = false, wc = false, sc = false)
public boolean js_addFoundSetFilterParam(String dataprovider, String operator, Object value, String name) throws ServoyException
{
return super.js_addFoundSetFilterParam(dataprovider, operator, value, name);
}
/*
* (non-Javadoc)
*
* @see com.servoy.j2db.dataprocessing.FoundSet#js_removeFoundSetFilterParam(java.lang.String)
*/
@Override
@ServoyClientSupport(mc = false, wc = false, sc = false)
public boolean js_removeFoundSetFilterParam(String name)
{
return super.js_removeFoundSetFilterParam(name);
}
@Override
public String toString()
{
String str = super.toString();
return "Related" + str.substring(0, str.length() - 1) + ", Args: " + getWhereArgsHash() + ']'; //$NON-NLS-1$ //$NON-NLS-2$
}
}