/* 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.lang.ref.SoftReference; import java.lang.ref.WeakReference; import java.rmi.RemoteException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.StringTokenizer; import java.util.WeakHashMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.Wrapper; import com.servoy.base.query.IBaseSQLCondition; import com.servoy.base.util.DataSourceUtilsBase; import com.servoy.j2db.ClientState; import com.servoy.j2db.IApplication; import com.servoy.j2db.Messages; import com.servoy.j2db.dataprocessing.SQLSheet.ConverterInfo; import com.servoy.j2db.persistence.AbstractBase; import com.servoy.j2db.persistence.Column; import com.servoy.j2db.persistence.EnumDataProvider; import com.servoy.j2db.persistence.IColumn; import com.servoy.j2db.persistence.IDataProvider; import com.servoy.j2db.persistence.IScriptProvider; import com.servoy.j2db.persistence.IServer; import com.servoy.j2db.persistence.ITable; import com.servoy.j2db.persistence.LiteralDataprovider; import com.servoy.j2db.persistence.Relation; import com.servoy.j2db.persistence.RelationItem; import com.servoy.j2db.persistence.RepositoryException; import com.servoy.j2db.persistence.ScriptMethod; import com.servoy.j2db.persistence.ScriptVariable; import com.servoy.j2db.persistence.Solution; import com.servoy.j2db.persistence.Table; import com.servoy.j2db.query.IQueryElement; import com.servoy.j2db.query.ISQLSelect; import com.servoy.j2db.query.QueryAggregate; import com.servoy.j2db.query.QueryColumnValue; import com.servoy.j2db.query.QuerySelect; import com.servoy.j2db.query.QueryTable; import com.servoy.j2db.querybuilder.IQueryBuilder; import com.servoy.j2db.querybuilder.IQueryBuilderFactory; import com.servoy.j2db.querybuilder.impl.QBFactory; import com.servoy.j2db.querybuilder.impl.QBSelect; import com.servoy.j2db.scripting.IExecutingEnviroment; import com.servoy.j2db.util.DataSourceUtils; import com.servoy.j2db.util.Debug; import com.servoy.j2db.util.ServoyException; import com.servoy.j2db.util.SortedList; import com.servoy.j2db.util.StringComparator; import com.servoy.j2db.util.Utils; import com.servoy.j2db.util.WeakHashSet; import com.servoy.j2db.util.visitor.IVisitor; /** * Manager for foundsets * @author jblok */ public class FoundSetManager implements IFoundSetManagerInternal { @SuppressWarnings("unchecked") private static final Entry<String, SoftReference<RelatedFoundSet>>[] EMPTY_ENTRY_ARRAY = new Map.Entry[0]; private final IApplication application; private Map<IFoundSetListener, FoundSet> separateFoundSets; //FoundSetListener -> FoundSet ... 1 foundset per listener private Map<String, FoundSet> sharedDataSourceFoundSet; //dataSource -> FoundSet ... 1 foundset per data source private Set<FoundSet> foundSets; private WeakReference<IFoundSetInternal> noTableFoundSet; private Map<String, RowManager> rowManagers; //dataSource -> RowManager... 1 per table private Map<ITable, CopyOnWriteArrayList<ITableChangeListener>> tableListeners; //table -> ArrayList(tableListeners) protected SQLGenerator sqlGenerator; private GlobalTransaction globalTransaction; private IInfoListener infoListener;//we allow only one private final IFoundSetFactory foundsetfactory; private boolean createEmptyFoundsets = false; private Map<String, List<TableFilter>> tableFilterParams;//server -> ArrayList(TableFilter) private Map<String, ITable> inMemDataSources; // dataSourceUri -> temp table protected Map<String, ConcurrentMap<String, SoftReference<RelatedFoundSet>>> cachedSubStates; protected List<String> locks = new SortedList<String>(StringComparator.INSTANCE); private final GlobalFoundSetEventListener globalFoundSetEventListener = new GlobalFoundSetEventListener(); private final EditRecordList editRecordList; public final int pkChunkSize; public final int chunkSize; public final int initialRelatedChunkSize; private final List<Runnable> fireRunabbles = new ArrayList<Runnable>(); // tracking info used for logging private final HashMap<String, Object> trackingInfoMap = new HashMap<String, Object>(); public FoundSetManager(IApplication app, IFoundSetFactory factory) { application = app; initMembers(); editRecordList = new EditRecordList(this); foundsetfactory = factory; pkChunkSize = Utils.getAsInteger(app.getSettings().getProperty("servoy.foundset.pkChunkSize", Integer.toString(200)));//primarykeys to be get in one roundtrip //$NON-NLS-1$ chunkSize = Utils.getAsInteger(app.getSettings().getProperty("servoy.foundset.chunkSize", Integer.toString(30)));//records to be get in one roundtrip //$NON-NLS-1$ initialRelatedChunkSize = Utils.getAsInteger(app.getSettings().getProperty("servoy.foundset.initialRelatedChunkSize", Integer.toString(chunkSize * 2))); //initial related records to get in one roundtrip//$NON-NLS-1$ } private Runnable createFlushAction(final String dataSource) { return new Runnable() { public void run() { if (dataSource == null) { Iterator<RowManager> it = rowManagers.values().iterator(); while (it.hasNext()) { RowManager element = it.next(); element.flushAllCachedRows(); fireTableEvent(element.getSQLSheet().getTable()); } refreshFoundSetsFromDB(); } else { RowManager element = rowManagers.get(dataSource); if (element != null) { element.flushAllCachedRows(); refreshFoundSetsFromDB(dataSource, null, false); fireTableEvent(element.getSQLSheet().getTable()); } } } }; } //triggered by server call public void flushCachedDatabaseDataFromRemote(String dataSource) { runOnEditOrTransactionStoppedActions.add(createFlushAction(dataSource)); performActionIfRequired(); } public void flushCachedDatabaseData(String dataSource) { Runnable action = createFlushAction(dataSource); action.run(); } /* * Flush locally in the current client, for client plugins * * @see com.servoy.j2db.dataprocessing.IDatabaseManager#notifyDataChange(java.lang.String, com.servoy.j2db.dataprocessing.IDataSet, int) */ public boolean notifyDataChange(String dataSource, IDataSet pks, int action) { if (pks == null) { flushCachedDatabaseData(dataSource); } else { notifyDataChange(dataSource, pks, action, null); } return true; } private final List<Runnable> runOnEditOrTransactionStoppedActions = Collections.synchronizedList(new ArrayList<Runnable>(3)); private final AtomicBoolean isBusy = new AtomicBoolean(false); void performActionIfRequired() { if (isBusy.getAndSet(true)) return; try { while (runOnEditOrTransactionStoppedActions.size() > 0 && !hasTransaction() && !editRecordList.isEditing()) { try { runOnEditOrTransactionStoppedActions.remove(0).run(); } catch (Exception e) { Debug.error("Exception calling remote flush action", e); //$NON-NLS-1$ } } } finally { isBusy.set(false); } } public void refreshFoundSetsFromDB() { refreshFoundSetsFromDB(null, null, false); } /** * * @param table * @param dataSource * @param columnName when not null, only return true if the table has the column */ private boolean mustRefresh(ITable table, String dataSource, String columnName) { if (columnName != null && table instanceof Table && ((Table)table).getColumn(columnName) == null) { // does not have specified column not return false; } ITable dsTable = null; try { dsTable = getTable(dataSource); } catch (RepositoryException e) { Debug.error(e); } if (table != null && dsTable != null) { return (table.getServerName().equals(dsTable.getServerName()) && table.getName().equals(dsTable.getName())); } return true; } /** * Used by rollback and flush table/all * @param dataSource the datasource the foundsets must be build on to refresh (null then all) * @param columnName when not null, only refresh foundsets on tables that have this column * @param skipStopEdit If true then stop edit will not be called * @return affected tables */ private Collection<ITable> refreshFoundSetsFromDB(String dataSource, String columnName, boolean skipStopEdit) { Set<ITable> affectedTables = new HashSet<ITable>(); List<FoundSet> fslist = new ArrayList<FoundSet>(sharedDataSourceFoundSet.size() + separateFoundSets.size() + foundSets.size()); fslist.addAll(sharedDataSourceFoundSet.values()); fslist.addAll(separateFoundSets.values()); fslist.addAll(foundSets); for (FoundSet fs : fslist) { try { if (mustRefresh(fs.getTable(), dataSource, columnName)) { if (fs.isInitialized()) { fs.refreshFromDB(false, skipStopEdit); } affectedTables.add(fs.getTable()); } } catch (Exception e) { Debug.error(e); } } // Can't just clear substates!! if used in portal then everything is out of sync // if(server_name == null && table_name == null) // { // cachedSubStates.clear(); // } // else for (ConcurrentMap<String, SoftReference<RelatedFoundSet>> map : cachedSubStates.values()) { Map.Entry<String, SoftReference<RelatedFoundSet>>[] array = map.entrySet().toArray(EMPTY_ENTRY_ARRAY); for (Map.Entry<String, SoftReference<RelatedFoundSet>> entry : array) { SoftReference<RelatedFoundSet> sr = entry.getValue(); RelatedFoundSet element = sr.get(); if (element != null) { try { if (mustRefresh(element.getTable(), dataSource, columnName)) { //element.refreshFromDB(false); // this call is somewhat different then a complete refresh from db. // The selection isn't tried to keep on the same pk // new records are really being flushed.. element.invalidateFoundset(); affectedTables.add(element.getTable()); } } catch (Exception e) { Debug.error(e); } } else { map.remove(entry.getKey(), sr); } } } getEditRecordList().fireEvents(); return affectedTables; } private Collection<ITable> getFilterUpdateAffectedTables(String dataSource, String columnName) { Collection<ITable> affectedtableList = refreshFoundSetsFromDB(dataSource, columnName, false); // also add tables that have listeners but no foundsets Iterator<ITable> tableListeneresIte = tableListeners.keySet().iterator(); ITable tableKey; while (tableListeneresIte.hasNext()) { tableKey = tableListeneresIte.next(); if (!affectedtableList.contains(tableKey) && mustRefresh(tableKey, dataSource, columnName)) { affectedtableList.add(tableKey); } } return affectedtableList; } /* * (non-Javadoc) * * @see com.servoy.j2db.dataprocessing.IFoundSetManagerInternal#reloadFoundsetMethod(java.lang.String, com.servoy.j2db.persistence.IScriptProvider) */ public void reloadFoundsetMethod(String dataSource, IScriptProvider scriptMethod) { if (dataSource == null) { return; } for (Object element : sharedDataSourceFoundSet.values().toArray()) { FoundSet fs = (FoundSet)element; try { if (dataSource.equals(fs.getDataSource())) { fs.reloadFoundsetMethod(scriptMethod); } } catch (Exception e) { Debug.error(e); } } for (Object element : separateFoundSets.values().toArray()) { FoundSet fs = (FoundSet)element; try { if (dataSource.equals(fs.getDataSource())) { fs.reloadFoundsetMethod(scriptMethod); } } catch (Exception e) { Debug.error(e); } } for (Object element : foundSets.toArray()) { FoundSet fs = (FoundSet)element; try { if (dataSource.equals(fs.getDataSource())) { fs.reloadFoundsetMethod(scriptMethod); } } catch (Exception e) { Debug.error(e); } } for (ConcurrentMap<String, SoftReference<RelatedFoundSet>> map : cachedSubStates.values()) { Map.Entry<String, SoftReference<RelatedFoundSet>>[] array = map.entrySet().toArray(EMPTY_ENTRY_ARRAY); for (Map.Entry<String, SoftReference<RelatedFoundSet>> entry : array) { SoftReference<RelatedFoundSet> sr = entry.getValue(); RelatedFoundSet element = sr.get(); if (element != null) { try { if (dataSource.equals(element.getDataSource())) { element.reloadFoundsetMethod(scriptMethod); } } catch (Exception e) { Debug.error(e); } } else { map.remove(entry.getKey(), sr); } } } } public void init() { editRecordList.init(); } /** * flushes/refreshes only related foundsets with a changed sql. * * @param caller * @param relationName * @param parentToIndexen */ public void flushRelatedFoundSet(IFoundSetInternal caller, String relationName) { ConcurrentMap<String, SoftReference<RelatedFoundSet>> hm = cachedSubStates.get(relationName); if (hm != null) { Map.Entry<String, SoftReference<RelatedFoundSet>>[] array = hm.entrySet().toArray(EMPTY_ENTRY_ARRAY); for (Map.Entry<String, SoftReference<RelatedFoundSet>> entry : array) { SoftReference<RelatedFoundSet> sr = entry.getValue(); RelatedFoundSet element = sr.get(); if (element != null) { //prevent callbacks by called test if (element != caller) { try { if (!element.creationSqlSelect.equals(element.getSqlSelect())) { element.invalidateFoundset(); } } catch (Exception e) { Debug.error(e); } } } else { hm.remove(entry.getKey(), sr); } } } } public IFoundSetInternal getGlobalRelatedFoundSet(String name) throws RepositoryException, ServoyException { Relation relation = application.getFlattenedSolution().getRelation(name); if (relation != null && relation.isGlobal()) { SQLSheet childSheet = getSQLGenerator().getCachedTableSQLSheet(relation.getForeignDataSource()); // this returns quickly if it already has a sheet for that relation, but optimize further? getSQLGenerator().makeRelatedSQL(childSheet, relation); return getRelatedFoundSet(new PrototypeState(getSharedFoundSet(relation.getForeignDataSource())), childSheet, name, getDefaultPKSortColumns(relation.getForeignDataSource())); } return null; } /** * Check if related foundset is loaded, relationName may be multiple-levels deep. */ protected boolean isRelatedFoundSetLoaded(IRecordInternal state, String relationName) { try { IRecordInternal rec = state; Relation[] relationSequence = application.getFlattenedSolution().getRelationSequence(relationName); for (int i = 0; relationSequence != null && i < relationSequence.length; i++) { IFoundSetInternal rfs = getRelatedFoundSetWhenLoaded(rec, relationSequence[i]); if (rfs == null) { return false; } if (i == relationSequence.length - 1 || rfs.getSize() == 0) { // no more relations to be loaded because rfs is empty, or at end of relation sequence return true; } rec = rfs.getRecord(rfs.getSelectedIndex()); } } catch (Exception e) { Debug.error("is related foundset check failed", e); //$NON-NLS-1$ } return false; } private RelatedFoundSet getRelatedFoundSetWhenLoaded(IRecordInternal state, Relation relation) throws RepositoryException { if (state == null || relation == null || !relation.isValid()) { return null; } RelatedHashedArguments relatedArguments = calculateFKHash(state, relation, false); if (relatedArguments == null) { return null; } Map<String, SoftReference<RelatedFoundSet>> rfsMap = cachedSubStates.get(relation.getName()); if (rfsMap != null) { SoftReference<RelatedFoundSet> sr = rfsMap.get(relatedArguments.hash); if (sr != null) { RelatedFoundSet rfs = sr.get(); if (rfs != null && !rfs.mustQueryForUpdates() && !rfs.mustAggregatesBeLoaded()) { return rfs; } } } return null; } //query for a substate protected IFoundSetInternal getRelatedFoundSet(IRecordInternal state, SQLSheet childSheet, String relationName, List<SortColumn> defaultSortColumns) throws ServoyException { IFoundSetInternal retval = null; Relation relation = application.getFlattenedSolution().getRelation(relationName); if (relation == null || !relation.isValid()) { return null; } if (relation.isParentRef()) { return state.getParentFoundSet(); } RelatedHashedArguments relatedArguments = calculateFKHash(state, relation, false); if (relatedArguments == null) { return null; } ConcurrentMap<String, SoftReference<RelatedFoundSet>> rfs = cachedSubStates.get(relationName); if (rfs != null) { SoftReference<RelatedFoundSet> sr = rfs.get(relatedArguments.hash); if (sr != null) { retval = sr.get(); if (retval == null && Debug.tracing()) Debug.trace("-----------CacheMiss for related founset " + relationName + " for keys " + relatedArguments.hash); //$NON-NLS-1$ //$NON-NLS-2$ //else Debug.trace("-----------CacheHit!! for related foundset " + relID + " for keys " + calcPKHashKey); } } List<RelatedHashedArguments> toFetch = null; if (retval == null) { String lockString = relationName + relatedArguments.hash; synchronized (locks) { rfs = cachedSubStates.get(relationName); if (rfs == null) { rfs = new ConcurrentHashMap<String, SoftReference<RelatedFoundSet>>(); cachedSubStates.put(relationName, rfs); } while (locks.contains(lockString)) { try { locks.wait(); } catch (InterruptedException e) { Debug.error(e); } } SoftReference<RelatedFoundSet> sr = rfs.get(relatedArguments.hash); if (sr != null) { retval = sr.get(); } if (retval == null) { locks.add(lockString); // pre-fetch a number of sibling related found sets toFetch = new ArrayList<RelatedHashedArguments>(); toFetch.add(relatedArguments); // first to fetch is the one currently requested IFoundSetInternal parent = state.getParentFoundSet(); int currIndex = parent.getRecordIndex(state); if (currIndex >= 0 && parent instanceof FoundSet) { int relatedChunkSize = chunkSize / 3; Object[] siblingRecords = ((FoundSet)parent).getPksAndRecords().getCachedRecords().toArray(); // take a snapshot of cachedRecords for (int s = currIndex + 1; s < siblingRecords.length && toFetch.size() < relatedChunkSize; s++) { IRecordInternal sibling = (IRecordInternal)siblingRecords[s]; if (sibling != null) { RelatedHashedArguments extra = calculateFKHash(sibling, relation, true); if (extra != null) { SoftReference<RelatedFoundSet> srSibling = rfs.get(extra.hash); if (srSibling != null && srSibling.get() != null) { // already cached continue; } String extraLockString = relationName + extra.hash; if (!locks.contains(extraLockString)) { locks.add(extraLockString); toFetch.add(extra); } } } } } } } if (retval == null) { RelatedFoundSet[] retvals = null; try { IRecordInternal[] states = new IRecordInternal[toFetch.size()]; Object[][] whereArgsList = new Object[toFetch.size()][]; for (int f = 0; f < toFetch.size(); f++) { RelatedHashedArguments relargs = toFetch.get(f); states[f] = relargs.state; whereArgsList[f] = relargs.whereArgs; } if (relation.getInitialSort() != null) { defaultSortColumns = getSortColumns(relation.getForeignDataSource(), relation.getInitialSort()); } retvals = (RelatedFoundSet[])RelatedFoundSet.createRelatedFoundSets(foundsetfactory, this, states, relation, childSheet, whereArgsList, defaultSortColumns); retval = retvals[0];// the first query is the one requested now, the rest is pre-fetch } finally { synchronized (locks) { for (int f = 0; f < toFetch.size(); f++) { RelatedHashedArguments relargs = toFetch.get(f); if (retvals != null) { rfs.put(relargs.hash, new SoftReference<RelatedFoundSet>(retvals[f])); } locks.remove(relationName + relargs.hash); } locks.notifyAll(); } } // inform global foundset event listeners that a new foundset has been created globalFoundSetEventListener.foundSetsCreated(retvals); // run runnables for firing events after foundsets have been created if (fireRunabbles.size() > 0) { Runnable[] runnables; synchronized (fireRunabbles) { runnables = new ArrayList<Runnable>(fireRunabbles).toArray(new Runnable[fireRunabbles.size()]); fireRunabbles.clear(); } for (Runnable runnable : runnables) { runnable.run(); } } } } return retval; } private RelatedHashedArguments calculateFKHash(IRecordInternal state, Relation r, boolean testForCalcs) throws RepositoryException { Object[] whereArgs = getRelationWhereArgs(state, r, testForCalcs); if (whereArgs == null) { return null; } return new RelatedHashedArguments(state, whereArgs, RowManager.createPKHashKey(whereArgs)); } /** * Get relation where-args, not using column converters * @param state * @param relation * @param testForCalcs * @return * @throws RepositoryException */ public Object[] getRelationWhereArgs(IRecordInternal state, Relation relation, boolean testForCalcs) throws RepositoryException { boolean isNull = true; IDataProvider[] args = relation.getPrimaryDataProviders(application.getFlattenedSolution()); Column[] columns = relation.getForeignColumns(); Object[] array = new Object[args.length]; for (int i = 0; i < args.length; i++) { Object value = null; if (args[i] instanceof LiteralDataprovider) { value = ((LiteralDataprovider)args[i]).getValue(); } else if (args[i] instanceof EnumDataProvider) { value = getScopesScopeProvider().getDataProviderValue(args[i].getDataProviderID()); } else { String dataProviderID = args[i].getDataProviderID(); if (testForCalcs && state.getRawData().containsCalculation(dataProviderID) && state.getRawData().mustRecalculate(dataProviderID, true)) { // just return null if a calc is found that also have to be recalculated. // else this can just cascade through.. return null; } value = state.getValue(dataProviderID, false); // unconverted (todb value) } if (value != Scriptable.NOT_FOUND) { array[i] = columns[i].getAsRightType(value); } if (array[i] != null) { isNull = false; } else { // If it is a column that is null then always set isNull on true and break. // Because null columns can't have a relation. if (args[i] instanceof IColumn) { return null; } if (isNull) { isNull = !(args[i] instanceof ScriptVariable); } } } if (isNull) return null; //optimize for null keys (multiple all null!) but not empty pk (db ident) return array; } public void flushCachedItems() { trackingInfoMap.clear(); //just to make sure rollbackTransaction(true, true, true); releaseAllLocks(null); createEmptyFoundsets = false; for (RowManager rm : rowManagers.values()) { rm.dispose(); } initMembers(); sqlGenerator = null; scopesScopeProvider = null; editRecordList.init(); } private void initMembers() { sharedDataSourceFoundSet = new ConcurrentHashMap<String, FoundSet>(64); separateFoundSets = Collections.synchronizedMap(new WeakHashMap<IFoundSetListener, FoundSet>(32)); foundSets = Collections.synchronizedSet(new WeakHashSet<FoundSet>(64)); noTableFoundSet = null; rowManagers = new ConcurrentHashMap<String, RowManager>(64); tableListeners = new ConcurrentHashMap<ITable, CopyOnWriteArrayList<ITableChangeListener>>(16); tableFilterParams = new ConcurrentHashMap<String, List<TableFilter>>(); cachedSubStates = new ConcurrentHashMap<String, ConcurrentMap<String, SoftReference<RelatedFoundSet>>>(128); inMemDataSources = new ConcurrentHashMap<String, ITable>(); } /** * This calls flush on the sql sheet of that table only used when developing the solution. */ public void flushSQLSheet(String dataSource) { try { SQLSheet cachedTableSQLSheet = getSQLGenerator().getCachedTableSQLSheet(dataSource); if (cachedTableSQLSheet != null) cachedTableSQLSheet.flush(application, null); } catch (ServoyException e) { Debug.error(e); } } /** * This calls flush on the sql sheet of that table only used when developing the solution. */ public void flushSQLSheet(Relation relation) { try { SQLSheet cachedTableSQLSheet = getSQLGenerator().getCachedTableSQLSheet(relation.getPrimaryDataSource()); if (cachedTableSQLSheet != null) cachedTableSQLSheet.flush(application, relation); } catch (Exception e) { Debug.error(e); } } private ScopesScopeProvider scopesScopeProvider; public IGlobalValueEntry getScopesScopeProvider() { if (scopesScopeProvider == null) { scopesScopeProvider = new ScopesScopeProvider(application.getScriptEngine().getScopesScope()); } return scopesScopeProvider; } public SQLGenerator getSQLGenerator() { if (sqlGenerator == null) { sqlGenerator = new SQLGenerator(application); } return sqlGenerator; } public IDataServer getDataServer() { return application.getDataServer(); } public IApplication getApplication() { return application; } public IExecutingEnviroment getScriptEngine() { return application.getScriptEngine(); } public synchronized RowManager getRowManager(String dataSource) throws ServoyException { if (getDataServer() == null) { // no data access yet return null; } ITable t = getTable(dataSource); if (t != null) { RowManager rm = rowManagers.get(dataSource); if (rm == null) { try { // first time this client uses this table getDataServer().addClientAsTableUser(application.getClientID(), t.getServerName(), t.getName()); } catch (RemoteException e) { throw new RepositoryException(e); } rm = new RowManager(this, getSQLGenerator().getCachedTableSQLSheet(dataSource)); rowManagers.put(dataSource, rm); } return rm; } return null; } ITable getTable(IFoundSetListener l) { String dataSource = l.getDataSource(); if (dataSource == null) { return null; } ITable table = inMemDataSources.get(dataSource); if (table == null) { return l.getTable(); } return table; } @SuppressWarnings("nls") public ITable getTable(String dataSource) throws RepositoryException { if (dataSource == null) { return null; } if (application.getSolution() == null) { if (Debug.tracing()) { Debug.trace("Trying to get a table for a datasource: " + dataSource + " on an already closed solution", new RuntimeException()); } return null; } ITable table = inMemDataSources.get(dataSource); if (table == null) { // when it is a db:/server/table data source String[] servernameTablename = DataSourceUtilsBase.getDBServernameTablename(dataSource); if (servernameTablename != null && servernameTablename[0] != null) { try { IServer server = application.getSolution().getServer(servernameTablename[0]); if (server == null) { throw new RepositoryException(Messages.getString("servoy.exception.serverNotFound", new Object[] { servernameTablename[0] })); //$NON-NLS-1$ } table = server.getTable(servernameTablename[1]); } catch (RemoteException e) { throw new RepositoryException(e); } } } return table; } public Collection<String> getInMemDataSourceNames() { List<String> inMemDataSourceNames = new ArrayList<>(inMemDataSources.size()); for (String dataSource : inMemDataSources.keySet()) { inMemDataSourceNames.add(DataSourceUtils.getInmemDataSourceName(dataSource)); } return inMemDataSourceNames; } public String getDataSource(ITable table) { if (table == null) { return null; } return table.getDataSource(); } public Relation getRelation(String relationName) { return getApplication().getFlattenedSolution().getRelation(relationName); } /** * Find the data source of the table with given sql name in same server as serverDataSource */ public String resolveDataSource(String serverDataSource, String tableSQLName) { ITable serverTable = null; try { serverTable = getTable(serverDataSource); } catch (RepositoryException e) { Debug.error(e); } if (serverTable == null) { return null; } ITable table = null; try { IServer server = application.getSolution().getServer(serverTable.getServerName()); if (server != null) { table = server.getTableBySqlname(tableSQLName); } } catch (RepositoryException e) { Debug.error(e); } catch (RemoteException e) { Debug.error(e); } if (table == null) { return null; } return table.getDataSource(); } public void addFoundSetListener(IFoundSetListener l) throws ServoyException { giveMeFoundSet(l); } public TableFilter createTableFilter(String name, String serverName, ITable table, String dataprovider, String operator, Object value) throws ServoyException { if (dataprovider == null || operator == null) return null; int op = RelationItem.getValidOperator(operator.trim(), IBaseSQLCondition.ALL_DEFINED_OPERATORS, IBaseSQLCondition.ALL_MODIFIERS); if (op == -1) { return null; } return createTableFilter(name, serverName, table, dataprovider.trim(), op, value); } public TableFilter createTableFilter(String name, String serverName, ITable table, String dataprovider, int operator, Object val) throws ServoyException { if (table != null && ((Table)table).getColumn(dataprovider) == null) { return null; } Object value = val; if (value instanceof Wrapper) { value = ((Wrapper)value).unwrap(); } if (table != null) { Column column = ((Table)table).getColumn(dataprovider); if (column == null) { return null; } value = convertFilterValue(table, column, value); } return new TableFilter(name, serverName, table == null ? null : table.getName(), table == null ? null : table.getSQLName(), dataprovider, operator, value); } public Object convertFilterValue(ITable table, Column column, Object value) throws ServoyException { ConverterInfo columnConverterInfo = getSQLGenerator().getCachedTableSQLSheet(table.getDataSource()).getColumnConverterInfo(column.getDataProviderID()); if (columnConverterInfo != null) { IColumnConverter columnConverter = application.getFoundSetManager().getColumnConverterManager().getConverter(columnConverterInfo.converterName); if (columnConverter != null) { Object[] array = null; if (value instanceof List< ? >) { array = ((List< ? >)value).toArray(); } else if (value != null && value.getClass().isArray()) { array = ((Object[])value).clone(); } if (array != null) { for (int i = 0; i < array.length; i++) { array[i] = SQLGenerator.convertFromObject(application, columnConverter, columnConverterInfo, column.getDataProviderID(), column.getDataProviderType(), array[i], false); } return array; } if (value == null || !SQLGenerator.isSelectQuery(value.toString())) { return SQLGenerator.convertFromObject(application, columnConverter, columnConverterInfo, column.getDataProviderID(), column.getDataProviderType(), value, false); } // else add as subquery } } return value; } public boolean addTableFilterParam(String filterName, String serverName, ITable table, String dataprovider, String operator, Object value) throws ServoyException { TableFilter filter = createTableFilter(filterName, serverName, table, dataprovider, operator, value); if (filter == null) { application.reportJSError("Table filter not created, column not found in table or operator invalid, filterName = '" + filterName + "', serverName = '" + serverName + "', table = '" + table + "', dataprovider = '" + dataprovider + "', operator = '" + operator + "'", null); return false; } List<TableFilter> params = tableFilterParams.get(serverName); if (params == null) { tableFilterParams.put(serverName, params = new ArrayList<TableFilter>()); } if (!filter.isContainedIn(params)) // do not add the same filter, will add same AND-condition anyway { params.add(filter); for (ITable affectedtable : getFilterUpdateAffectedTables(getDataSource(table), filter.getDataprovider())) { fireTableEvent(affectedtable); } } if (Messages.isI18NTable(serverName, table != null ? table.getName() : null, application)) { ((ClientState)application).refreshI18NMessages(); } return true; } public boolean removeTableFilterParam(String serverName, String filterName) { List<TableFilter> params = tableFilterParams.get(serverName); List<TableFilter> removedFilters = new ArrayList<TableFilter>(); if (params != null) { Iterator<TableFilter> iterator = params.iterator(); while (iterator.hasNext()) { TableFilter f = iterator.next(); if (filterName.equals(f.getName())) { iterator.remove(); removedFilters.add(f); } } Set<ITable> firedTables = new HashSet<ITable>(); for (TableFilter filter : removedFilters) { String dataSource; if (filter.getTableName() == null) { dataSource = null; } else { dataSource = DataSourceUtils.createDBTableDataSource(filter.getServerName(), filter.getTableName()); } for (ITable affectedtable : getFilterUpdateAffectedTables(dataSource, filter.getDataprovider())) { if (firedTables.add(affectedtable)) { fireTableEvent(affectedtable); } } } } return removedFilters.size() > 0; } public Object[][] getTableFilterParams(String serverName, String filterName) { List<TableFilter> params = tableFilterParams.get(serverName); List<Object[]> result = new ArrayList<Object[]>(); if (params != null) { Iterator<TableFilter> iterator = params.iterator(); while (iterator.hasNext()) { TableFilter f = iterator.next(); if (filterName == null || filterName.equals(f.getName())) { result.add(new Object[] { f.getTableName(), f.getDataprovider(), RelationItem.getOperatorAsString(f.getOperator()), f.getValue(), f.getName() }); } } } return result.toArray(new Object[result.size()][]); } /** * Get the table filters that are applicable on the sql for the server. Returns an array of table filters, the resulting array may be modified by the * caller. * * @param serverName * @param sql * @return */ public ArrayList<TableFilter> getTableFilterParams(String serverName, IQueryElement sql) { final List<TableFilter> serverFilters = tableFilterParams.get(serverName); if (serverFilters == null) { return null; } // get the sql table names in the query final Set<String> tableSqlNames = new HashSet<String>(); // find the filters for the tables found in the query final ArrayList<TableFilter>[] filters = new ArrayList[] { null }; sql.acceptVisitor(new IVisitor() { public Object visit(Object o) { if (o instanceof QueryTable && tableSqlNames.add(((QueryTable)o).getName())) { QueryTable qTable = (QueryTable)o; for (TableFilter filter : serverFilters) { TableFilter useFilter = null; if (filter.getTableName() == null) { // filter is on all tables with specified dataProvider as column try { if (qTable.getDataSource() != null) { Table table = (Table)getTable(qTable.getDataSource()); if (table == null) { // should never happen throw new RuntimeException("Could not find table '" + qTable.getDataSource() + "' for table filters"); } Column column = table.getColumn(filter.getDataprovider()); if (column != null) { // Use filter with table name filled in. // When table was null value was not yet converted, convert now. Object value = convertFilterValue(table, column, filter.getValue()); useFilter = new TableFilter(filter.getName(), filter.getServerName(), table.getName(), table.getSQLName(), filter.getDataprovider(), filter.getOperator(), value); } } } catch (Exception e) { // big trouble, this is security filtering, so bail out on error throw new RuntimeException(e); } } else if (filter.getTableSQLName().equals(qTable.getName())) { useFilter = filter; } if (useFilter != null) { if (filters[0] == null) { filters[0] = new ArrayList<TableFilter>(); } filters[0].add(useFilter); } } } return o; } }); return filters[0]; } /** * Checks if the specified table has filter defined * * @param serverName * @param tableName * @return true if there is a filter defined for the table, otherwise false */ public boolean hasTableFilter(String serverName, String tableName) { if (serverName == null || tableName == null) return false; List<TableFilter> serverFilters = tableFilterParams.get(serverName); if (serverFilters == null) return false; Iterator<TableFilter> serverFiltersIte = serverFilters.iterator(); TableFilter tableFilter; while (serverFiltersIte.hasNext()) { tableFilter = serverFiltersIte.next(); if (tableName.equals(tableFilter.getTableName())) return true; } return false; } public IFoundSetInternal getSeparateFoundSet(IFoundSetListener l, List<SortColumn> defaultSortColumns) throws ServoyException { if (l.getDataSource() == null) { return getNoTableFoundSet(); } FoundSet foundset = separateFoundSets.get(l); if (foundset == null) { SQLSheet sheet = getSQLGenerator().getCachedTableSQLSheet(l.getDataSource()); foundset = (FoundSet)foundsetfactory.createFoundSet(this, sheet, null, defaultSortColumns); if (createEmptyFoundsets) foundset.clear(); separateFoundSets.put(l, foundset); // inform global foundset event listeners that a new foundset has been created globalFoundSetEventListener.foundSetCreated(foundset); } return foundset; } public IFoundSetInternal getSharedFoundSet(String dataSource, List<SortColumn> defaultSortColumns) throws ServoyException { if (dataSource == null || !application.getFlattenedSolution().isMainSolutionLoaded()) { return getNoTableFoundSet(); } FoundSet foundset = sharedDataSourceFoundSet.get(dataSource); if (foundset == null) { SQLSheet sheet = getSQLGenerator().getCachedTableSQLSheet(dataSource); foundset = (FoundSet)foundsetfactory.createFoundSet(this, sheet, null, defaultSortColumns); if (createEmptyFoundsets) foundset.clear(); sharedDataSourceFoundSet.put(dataSource, foundset); // inform global foundset event listeners that a new foundset has been created globalFoundSetEventListener.foundSetCreated(foundset); } return foundset; } private IFoundSetInternal getNoTableFoundSet() throws ServoyException { IFoundSetInternal foundSet = noTableFoundSet == null ? null : noTableFoundSet.get(); if (foundSet == null) { SQLSheet sheet = getSQLGenerator().getCachedTableSQLSheet(null); foundSet = foundsetfactory.createFoundSet(this, sheet, null, null); noTableFoundSet = new WeakReference<IFoundSetInternal>(foundSet); } return foundSet; } public IFoundSetInternal getSharedFoundSet(String dataSource) throws ServoyException { return getSharedFoundSet(dataSource, getDefaultPKSortColumns(dataSource)); } public IFoundSet getNewFoundSet(String dataSource) throws ServoyException { return getNewFoundSet(dataSource, null, getDefaultPKSortColumns(dataSource)); } public IFoundSetInternal getNewFoundSet(ITable table, QuerySelect pkSelect, List<SortColumn> defaultSortColumns) throws ServoyException { return getNewFoundSet(getDataSource(table), pkSelect, defaultSortColumns); } /** * @param defaultSortColumns: when null: use sorting defined in query */ public IFoundSetInternal getNewFoundSet(String dataSource, QuerySelect pkSelect, List<SortColumn> defaultSortColumns) throws ServoyException { if (dataSource == null) { return getNoTableFoundSet(); } SQLSheet sheet = getSQLGenerator().getCachedTableSQLSheet(dataSource); FoundSet foundset = (FoundSet)foundsetfactory.createFoundSet(this, sheet, pkSelect, defaultSortColumns); if (createEmptyFoundsets) foundset.clear(); foundSets.add(foundset); // inform global foundset event listeners that a new foundset has been created globalFoundSetEventListener.foundSetCreated(foundset); return foundset; } public void giveMeFoundSet(IFoundSetListener l) throws ServoyException { IFoundSetInternal set = null; if (l.getDataSource() == null || l.wantSharedFoundSet()) { String wantedGlobalRelationName = l.getGlobalRelationNamedFoundset(); // form is set on using a global relation through namedFoundset property if (wantedGlobalRelationName != null) { set = getGlobalRelatedFoundSet(wantedGlobalRelationName); if (set == null || !Solution.areDataSourcesCompatible(application.getRepository(), set.getDataSource(), l.getDataSource())) { throw new RepositoryException("Cannot create global relation namedFoundset '" + wantedGlobalRelationName + //$NON-NLS-1$ "' - please check relation"); //$NON-NLS-1$ } } else { set = getSharedFoundSet(l.getDataSource(), l.getDefaultSortColumns()); } } else { set = getSeparateFoundSet(l, l.getDefaultSortColumns()); } l.newValue(new FoundSetEvent(set, FoundSetEvent.NEW_FOUNDSET, FoundSetEvent.CHANGE_UPDATE)); } public void removeFoundSetListener(IFoundSetListener l) { } public boolean isShared(IFoundSet set) { if (set == null) return false; if (set instanceof RelatedFoundSet) return false; SQLSheet sheet = ((IFoundSetInternal)set).getSQLSheet(); if (sheet != null) { Table table = sheet.getTable(); if (table != null) { return set.equals(sharedDataSourceFoundSet.get(getDataSource(table))); } } return false; } public boolean isNew(IFoundSet set) { if (set == null) return false; if (set instanceof RelatedFoundSet) return false; return foundSets.contains(set); } /* * _____________________________________________________________ Methods for informing valuelists about table contents changes */ public void addTableListener(ITable table, ITableChangeListener l) { CopyOnWriteArrayList<ITableChangeListener> list = tableListeners.get(table); if (list == null) { list = new CopyOnWriteArrayList<ITableChangeListener>(); tableListeners.put(table, list); } list.add(l); } public void removeTableListener(ITable table, ITableChangeListener l) { CopyOnWriteArrayList<ITableChangeListener> list = tableListeners.get(table); if (list != null) { list.remove(l); } } void notifyChange(Table table) { fireTableEvent(table); } private void fireTableEvent(ITable table) { final CopyOnWriteArrayList<ITableChangeListener> list = tableListeners.get(table); if (list != null && list.size() > 0) { Runnable runnable = new Runnable() { public void run() { TableEvent e = new TableEvent(this); for (ITableChangeListener l : list) { l.tableChange(e); } } }; if (application.isEventDispatchThread()) { runnable.run(); } else { application.invokeLater(runnable); } } } public static String getSortColumnsAsString(List<SortColumn> list) { StringBuilder sb = new StringBuilder(); if (list != null) { for (int i = 0; i < list.size(); i++) { SortColumn sc = list.get(i); sb.append(sc.toString()); if (i < list.size() - 1) sb.append(", "); //$NON-NLS-1$ } } return sb.toString(); } public List<SortColumn> getSortColumns(ITable t, String options) { List<SortColumn> list = new ArrayList<SortColumn>(3); if (t == null) return list; if (options != null) { try { StringTokenizer tk = new StringTokenizer(options, ","); //$NON-NLS-1$ while (tk.hasMoreTokens()) { String columnName = null; String order = null; String def = tk.nextToken().trim(); int index = def.indexOf(" "); //$NON-NLS-1$ if (index != -1) { columnName = def.substring(0, index); order = def.substring(index + 1); } else { columnName = def; } if (columnName != null) { SortColumn sc = getSortColumn(t, Utils.toEnglishLocaleLowerCase(columnName), true); if (sc != null) { if (order != null && order.trim().toLowerCase().startsWith("desc")) //$NON-NLS-1$ { sc.setSortOrder(SortColumn.DESCENDING); } list.add(sc); } } } } catch (Exception ex) { Debug.error(ex); } } if (list.size() == 0) { // default pk sort try { return getDefaultPKSortColumns(t.getDataSource()); } catch (ServoyException e) { Debug.error(e); } } return list; } public List<SortColumn> getSortColumns(String dataSource, String options) throws RepositoryException { return getSortColumns(getTable(dataSource), options); } public SortColumn getSortColumn(ITable table, String dataProviderID, boolean logIfCannotBeResolved) throws RepositoryException { if (table == null || dataProviderID == null) return null; Table lastTable = (Table)table; List<Relation> relations = new ArrayList<Relation>(); String[] split = dataProviderID.split("\\."); //$NON-NLS-1$ for (int i = 0; i < split.length - 1; i++) { Relation r = application.getFlattenedSolution().getRelation(split[i]); String reason = null; if (r == null) { reason = "relation '" + split[i] + "' not found"; } else if (!r.isUsableInSort()) { if (!r.isValid()) reason = "relation '" + split[i] + "' not valid"; else if (r.isMultiServer()) reason = "relation '" + split[i] + "' is cross server, sorting is not supported"; else if (r.isGlobal()) reason = "relation '" + split[i] + "' is global, sorting is not supported"; else reason = "relation '" + split[i] + "' is outer join with or null modifier, sorting is not supported"; } else if (!lastTable.equals(getTable(r.getPrimaryDataSource()))) { reason = "table '" + lastTable.getName() + "' does not match with relation '" + split[i] + "'primary table"; } if (reason != null) { if (logIfCannotBeResolved) Debug.log("Cannot sort on dataprovider " + dataProviderID + ", " + reason, new Exception(split[i])); return null; } relations.add(r); lastTable = (Table)getTable(r.getForeignDataSource()); } String colName = split[split.length - 1]; IColumn c = lastTable.getColumn(colName); if (c == null) { // check for aggregate c = AbstractBase.selectByName(application.getFlattenedSolution().getAggregateVariables(lastTable, false), colName); } if (c != null) { return new SortColumn(c, relations.size() == 0 ? null : relations.toArray(new Relation[relations.size()])); } return null; } /* * _____________________________________________________________ locking methods */ //index == -1 is (current) selected record,< -1 is all records public boolean acquireLock(IFoundSet fs, int index, String lockName) { if (fs instanceof IFoundSetInternal) { IFoundSetInternal foundSet = (IFoundSetInternal)fs; if (foundSet.getSQLSheet().getTable() == null) { return false; } Map<Object, Object[]> pkhashkeys = new HashMap<Object, Object[]>(); if (index == -1) { int idx = foundSet.getSelectedIndex(); if (idx >= 0 && idx < foundSet.getSize()) { IRecordInternal rec = foundSet.getRecord(idx); if (rec == null || rec.getRawData() == null) return false;//just for safety if (!rec.getRawData().lockedByMyself()) pkhashkeys.put(rec.getPKHashKey(), rec.getPK()); } else { return false;//wrong index } } else if (index < -1) { for (int i = 0; i < foundSet.getSize(); i++) { IRecordInternal rec = foundSet.getRecord(i); if (rec == null || rec.getRawData() == null) return false;//just for safety if (!rec.getRawData().lockedByMyself()) pkhashkeys.put(rec.getPKHashKey(), rec.getPK()); } } else if (index >= 0) { if (index < foundSet.getSize()) { IRecordInternal rec = foundSet.getRecord(index); if (rec == null || rec.getRawData() == null) return false;//just for safety if (!rec.getRawData().lockedByMyself()) pkhashkeys.put(rec.getPKHashKey(), rec.getPK()); } else { return false;//wrong index } } else { return false;//unknown index } if (pkhashkeys.size() == 0) //optimize { return true; } Table table = (Table)foundSet.getTable(); if (table != null) { String server_name = foundSet.getSQLSheet().getServerName(); String table_name = foundSet.getSQLSheet().getTable().getName(); RowManager rm = rowManagers.get(DataSourceUtils.createDBTableDataSource(server_name, table_name)); //process Set<Object> keySet = pkhashkeys.keySet(); Set<Object> ids = new HashSet<Object>(keySet);//make copy because it is not serialized in developer and set is emptied QuerySelect lockSelect = SQLGenerator.createUpdateLockSelect(table, pkhashkeys.values().toArray(new Object[pkhashkeys.size()][]), hasTransaction() && Boolean.parseBoolean(application.getSettings().getProperty("servoy.record.lock.lockInDB", "false"))); //$NON-NLS-1$ //$NON-NLS-2$ if (rm != null) { if (rm.acquireLock(application.getClientID(), lockSelect, lockName, ids)) { if (infoListener != null) infoListener.showLocksStatus(true); // success return true; } } } } return false; } public boolean releaseAllLocks(String lockName) { boolean allReleased = true; boolean hasLocks = false; for (RowManager rm : rowManagers.values()) { Set<Object> pkhashkeys = rm.getOwnLocks(lockName); if (pkhashkeys.size() != 0) { try { if (((ILockServer)getDataServer()).releaseLocks(application.getClientID(), rm.getSQLSheet().getServerName(), rm.getSQLSheet().getTable().getName(), pkhashkeys)) { rm.removeLocks(pkhashkeys); } else { allReleased = false; hasLocks = true; } } catch (RepositoryException e) { // Will not happen Debug.error(e); return false; } catch (RemoteException e) { Debug.error(e);//TODO:put error code in app allReleased = false; hasLocks = true; } } if (infoListener != null && !hasLocks && lockName != null) { // check if some other locks are remaining hasLocks = rm.hasOwnLocks(null); } } if (infoListener != null) infoListener.showLocksStatus(hasLocks); return allReleased; } public boolean hasLocks(String lockName) { for (RowManager rm : rowManagers.values()) { if (rm.hasOwnLocks(lockName)) { return true; } } return false; } /* * _____________________________________________________________ transaction methods */ public void startTransaction() { if (globalTransaction == null) { globalTransaction = new GlobalTransaction(getDataServer(), application.getClientID()); if (infoListener != null) infoListener.showTransactionStatus(true); } } public boolean hasTransaction() { return (globalTransaction != null); } public String[] getTableNames(String serverName) { try { IServer server = application.getSolution().getServer(serverName); if (server != null) { List<String> list = server.getTableNames(false); return list.toArray(new String[list.size()]); } } catch (Exception e) { Debug.error(e); } return null; } public String[] getViewNames(String serverName) { try { IServer server = application.getSolution().getServer(serverName); if (server != null) { List<String> list = server.getViewNames(false); return list.toArray(new String[list.size()]); } } catch (Exception e) { Debug.error(e); } return null; } public int getFoundSetCount(IFoundSetInternal fs) { if (fs instanceof FoundSet && fs.getTable() != null) { FoundSet foundset = (FoundSet)fs; try { //optimize if (foundset.isInitialized() && !foundset.hadMoreRows()) { return foundset.getSize(); } IDataServer ds = application.getDataServer(); Table t = (Table)foundset.getTable(); String transaction_id = getTransactionID(t.getServerName()); QuerySelect sqlString = foundset.getSqlSelect(); QuerySelect selectCountSQLString = sqlString.getSelectCount("n", true); //$NON-NLS-1$ IDataSet set = ds.performQuery(application.getClientID(), t.getServerName(), transaction_id, selectCountSQLString, getTableFilterParams(t.getServerName(), selectCountSQLString), false, 0, 10, IDataServer.FOUNDSET_LOAD_QUERY); if (set.getRowCount() > 0) { Object[] row = set.getRow(0); if (row.length > 0) { return Utils.getAsInteger(row[0]); } } } catch (Exception e) { Debug.error(e); } } return -1; } public int getTableCount(ITable table) { if (table != null) { try { IDataServer ds = application.getDataServer(); String transaction_id = getTransactionID(table.getServerName()); QuerySelect countSelect = new QuerySelect(new QueryTable(table.getSQLName(), table.getDataSource(), table.getCatalog(), table.getSchema())); countSelect.addColumn(new QueryAggregate(QueryAggregate.COUNT, new QueryColumnValue(Integer.valueOf(1), "n", true), null)); //$NON-NLS-1$ IDataSet set = ds.performQuery(application.getClientID(), table.getServerName(), transaction_id, countSelect, getTableFilterParams(table.getServerName(), countSelect), false, 0, 10, IDataServer.FOUNDSET_LOAD_QUERY); if (set.getRowCount() > 0) { Object[] row = set.getRow(0); if (row.length > 0) { return Utils.getAsInteger(row[0]); } } } catch (Exception e) { Debug.error(e); } } return -1; } public boolean commitTransaction() { return commitTransaction(true, true); } public boolean commitTransaction(boolean saveFirst, boolean revertSavedRecords) { // first stop all edits, 'force' stop the edit by saying that it is a javascript stop if (globalTransaction != null && (!saveFirst || getEditRecordList().stopEditing(true) == ISaveConstants.STOPPED)) { GlobalTransaction gt = globalTransaction; globalTransaction = null; Collection<String> dataSourcesToRefresh = gt.commit(revertSavedRecords); if (infoListener != null) infoListener.showTransactionStatus(false); performActionIfRequired(); if (dataSourcesToRefresh != null) { refreshFoundsetsWithoutEditedRecords(dataSourcesToRefresh); return false; } return true; } return false; } private void refreshFoundsetsWithoutEditedRecords(Collection<String> dataSourcesToRefresh) { try { getEditRecordList().ignoreSave(true); for (String dataSource : dataSourcesToRefresh) { refreshFoundSetsFromDB(dataSource, null, true); } } finally { getEditRecordList().ignoreSave(false); } } public void rollbackTransaction() { rollbackTransaction(true, true, true); } public void rollbackTransaction(boolean rollbackEdited, boolean queryForNewData, boolean revertSavedRecords) { if (globalTransaction != null) { // first delete all edits, don't bother saving them they will be rolled back anyway. // Note that rows that have never been saved in the db will not be seen in GlobalTransaction.rollback() // because they never went through GlobalTransaction.addRow(), EditRecordList.rollbackRecords() will rollback in-memory. if (rollbackEdited) { getEditRecordList().rollbackRecords(); } GlobalTransaction gt = globalTransaction; globalTransaction = null; Collection<String> dataSourcesToRefresh = gt.rollback(queryForNewData, revertSavedRecords); if (infoListener != null) infoListener.showTransactionStatus(false); performActionIfRequired(); // refresh foundsets only if rollbackEdited is true, else the foundsets will even save/stopedit the record they where editing.. if (dataSourcesToRefresh != null) { refreshFoundsetsWithoutEditedRecords(dataSourcesToRefresh); } } } /** * Returns the globalTransaction. * * @return GlobalTransaction */ public GlobalTransaction getGlobalTransaction() { return globalTransaction; } public String getTransactionID(SQLSheet sheet) throws ServoyException { return getTransactionID(sheet.getServerName()); } public String getTransactionID(String serverName) throws ServoyException { if (globalTransaction != null) { return globalTransaction.getTransactionID(serverName); } return null; } public String getOriginalServerName(String serverName) { return getOriginalServerNames(serverName).iterator().next(); } @Override public Collection<String> getOriginalServerNames(String serverName) { IDataServer dataServer = application.getDataServer(); if (dataServer instanceof DataServerProxy) { return ((DataServerProxy)dataServer).getReverseMappedServerNames(serverName); } return Collections.singleton(serverName); } public String getSwitchedToServerName(String serverName) { IDataServer dataServer = application.getDataServer(); if (dataServer instanceof DataServerProxy) { return ((DataServerProxy)dataServer).getMappedServerName(serverName); } return serverName; } public IDataSet getDataSetByQuery(String serverName, ISQLSelect sqlSelect, boolean includeFilters, int maxNumberOfRowsToRetrieve) throws ServoyException { IDataServer ds = application.getDataServer(); String transaction_id = getTransactionID(serverName); IDataSet set = null; try { long time = System.currentTimeMillis(); set = ds.performCustomQuery(application.getClientID(), serverName, "<user_query>", transaction_id, sqlSelect, includeFilters ? getTableFilterParams(serverName, sqlSelect) : null, 0, maxNumberOfRowsToRetrieve); if (Debug.tracing()) { Debug.trace("Custom query, time: " + (System.currentTimeMillis() - time) + " thread: " + Thread.currentThread().getName() + " SQL: " + sqlSelect); //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$ } } catch (RemoteException e) { throw new RepositoryException(e); } return set; } public String createDataSourceFromQuery(String name, String serverName, ISQLSelect sqlSelect, boolean useTableFilters, int maxNumberOfRowsToRetrieve, int[] types, String[] pkNames) throws ServoyException { if (name == null) { return null; } try { String queryTid = getTransactionID(serverName); String dataSource = DataSourceUtils.createInmemDataSource(name); ITable table = inMemDataSources.get(dataSource); if (table != null) { // temp table was used before, delete all data in it FoundSet foundSet = (FoundSet)getSharedFoundSet(dataSource); foundSet.removeLastFound(); try { foundSet.deleteAllRecords(); } catch (Exception e) { Debug.log(e); table = null; } } GlobalTransaction gt = getGlobalTransaction(); String targetTid = null; if (gt != null) { targetTid = gt.getTransactionID(table == null ? IServer.INMEM_SERVER : table.getServerName()); } table = application.getDataServer().insertQueryResult(application.getClientID(), serverName, queryTid, sqlSelect, useTableFilters ? getTableFilterParams(serverName, sqlSelect) : null, false, 0, maxNumberOfRowsToRetrieve, IDataServer.CUSTOM_QUERY, dataSource, table == null ? IServer.INMEM_SERVER : table.getServerName(), table == null ? null : table.getName() /* create temp table when null */, targetTid, types, pkNames); if (table != null) { inMemDataSources.put(dataSource, table); fireTableEvent(table); refreshFoundSetsFromDB(dataSource, null, false); return dataSource; } } catch (RemoteException e) { throw new RepositoryException(e); } return null; } @Override public String toString() { return "FoundSetManager"; //$NON-NLS-1$ } /* * _____________________________________________________________ dataNotification */ public void notifyDataChange(final String ds, IDataSet pks, final int action, Object[] insertColumnData) { RowManager rm = rowManagers.get(ds); if (rm != null) { List<Row> insertedRows = null; if (action == ISQLActionTypes.INSERT_ACTION && insertColumnData == null) { // in this case the insert notification is probably triggered by rawSQL; so we need to read the new rows from DB to get correct newly inserted content try { insertedRows = rm.getRows(pks, 0, pks.getRowCount(), false); if (insertedRows.size() != pks.getRowCount()) { insertedRows = rm.getRows(pks, 0, pks.getRowCount(), true); } } catch (ServoyException e) { Debug.error("Cannot get newly inserted rows.", e); } } boolean didHaveRowAndIsUpdated = false; IDataSet newPks = pks; try { // Convert the pk dataset to the column type of the pk columns newPks = BufferedDataSetInternal.convertPksToRightType(pks, (Table)getTable(ds)); } catch (RepositoryException e) { Debug.error(e); } final IDataSet fnewPks = newPks; for (int i = 0; i < fnewPks.getRowCount(); i++) { boolean b = rm.changeByOther(RowManager.createPKHashKey(fnewPks.getRow(i)), action, insertColumnData, insertedRows == null ? null : insertedRows.get(i)); didHaveRowAndIsUpdated = (didHaveRowAndIsUpdated || b); } final boolean didHaveDataCached = didHaveRowAndIsUpdated; Runnable r = new Runnable() { public void run() { Solution solution = application.getSolution(); if (solution != null) { ScriptMethod sm = application.getFlattenedSolution().getScriptMethod(solution.getOnDataBroadcastMethodID()); if (sm != null) { try { application.getScriptEngine().getScopesScope().executeGlobalFunction( sm.getScopeName(), sm.getName(), Utils.arrayMerge( new Object[] { ds, new Integer(action), new JSDataSet(application, fnewPks), Boolean.valueOf(didHaveDataCached) }, Utils.parseJSExpressions(solution.getInstanceMethodArguments("onDataBroadcastMethodID"))), false, false); //$NON-NLS-1$ } catch (Exception e1) { application.reportError( Messages.getString("servoy.foundsetManager.error.ExecutingDataBroadcastMethod", new Object[] { sm.getName() }), e1); //$NON-NLS-1$ } } } } }; application.invokeLater(r); if (didHaveRowAndIsUpdated) { if (infoListener != null) infoListener.showDataChange(); } else // TODO if(action == INSERT) This is called to often now. { fireTableEvent(rm.getSQLSheet().getTable()); } } } public FoundSet getEmptyFoundSet(IFoundSetListener panel) throws ServoyException { FoundSet set = (FoundSet)getNewFoundSet(getTable(panel), null, panel.getDefaultSortColumns()); set.clear(); return set; } public IFoundSetInternal createRelatedFindFoundSet(IRecordInternal parentRecord, String relationName, SQLSheet childSheet) throws ServoyException { return foundsetfactory.createRelatedFindFoundSet(this, parentRecord, relationName, childSheet); } public void flushSecuritySettings() { editRecordList.clearSecuritySettings(); } public IInfoListener getInfoListener() { return infoListener; } public void setInfoListener(IInfoListener listener) { infoListener = listener; } public EditRecordList getEditRecordList() { return editRecordList; } public void createEmptyFormFoundsets() { createEmptyFoundsets = true; } /** * Clear the delete sets of all row managers. */ public void clearAllDeleteSets() { Iterator<RowManager> it = rowManagers.values().iterator(); while (it.hasNext()) { it.next().clearDeleteSet(); } } public void addGlobalFoundsetEventListener(IFoundSetEventListener foundSetEventListener) { globalFoundSetEventListener.addFoundSetEventListener(foundSetEventListener); } public void removeGlobalFoundsetEventListener(IFoundSetEventListener foundSetEventListener) { globalFoundSetEventListener.removeFoundSetEventListener(foundSetEventListener); } /** * Manager for listeners interested in foundst events on any foundset. * They will also be informed of newly created foundsets. * @author rgansevles * */ public static class GlobalFoundSetEventListener implements IFoundSetEventListener { private final List<IFoundSetEventListener> foundSetEventListeners = new ArrayList<IFoundSetEventListener>(); public void foundSetsCreated(IFoundSet[] foundSets) { if (foundSets != null) { for (IFoundSet fs : foundSets) { foundSetCreated(fs); } } } public void foundSetCreated(IFoundSet foundSet) { if (foundSet != null) { foundSet.addFoundSetEventListener(this); if (foundSetEventListeners.size() > 0) { foundSetChanged(new FoundSetEvent(foundSet, FoundSetEvent.NEW_FOUNDSET, FoundSetEvent.CHANGE_UPDATE)); } } } public void foundSetChanged(FoundSetEvent e) { IFoundSetEventListener[] array; synchronized (this) { array = foundSetEventListeners.toArray(new IFoundSetEventListener[foundSetEventListeners.size()]); } for (IFoundSetEventListener element : array) { element.foundSetChanged(e); } } public synchronized void addFoundSetEventListener(IFoundSetEventListener l) { if (!foundSetEventListeners.contains(l)) { foundSetEventListeners.add(l); } } public synchronized void removeFoundSetEventListener(IFoundSetEventListener l) { foundSetEventListeners.remove(l); } } /** * Container class for related foundset arguments and the hash * * @author rgansevles * */ private static class RelatedHashedArguments { final IRecordInternal state; final Object[] whereArgs; final String hash; public RelatedHashedArguments(IRecordInternal state, Object[] whereArgs, String hash) { this.state = state; this.whereArgs = whereArgs; this.hash = hash; } @Override public String toString() { return "RelatedHashedArguments " + hash + " " + Arrays.toString(whereArgs); } } public void setColumnManangers(IColumnValidatorManager columnValidatorManager, IConverterManager<IColumnConverter> columnConverterManager, IConverterManager<IUIConverter> uiConverterManager) { this.columnValidatorManager = columnValidatorManager; this.columnConverterManager = columnConverterManager; this.uiConverterManager = uiConverterManager; } private IColumnValidatorManager columnValidatorManager; private IConverterManager<IColumnConverter> columnConverterManager; private IConverterManager<IUIConverter> uiConverterManager; public IColumnValidatorManager getColumnValidatorManager() { return columnValidatorManager; } public IConverterManager<IColumnConverter> getColumnConverterManager() { return columnConverterManager; } public IConverterManager<IUIConverter> getUIConverterManager() { return uiConverterManager; } private boolean nullColumnValidatorEnabled = true; public boolean isNullColumnValidatorEnabled() { return nullColumnValidatorEnabled; } public void setNullColumnValidatorEnabled(boolean enable) { nullColumnValidatorEnabled = enable; } public String createDataSourceFromDataSet(String name, IDataSet dataSet, int[] intTypes, String[] pkNames) throws ServoyException { if (name == null) { return null; } // check if column names width matches rows if (dataSet.getRowCount() > 0 && dataSet.getRow(0).length != dataSet.getColumnCount()) { throw new RepositoryException("Data set rows do not match column count"); //$NON-NLS-1$ } try { String dataSource = DataSourceUtils.createInmemDataSource(name); ITable table = inMemDataSources.get(dataSource); if (table != null) { // temp table was used before, delete all data in it FoundSet foundSet = (FoundSet)getSharedFoundSet(dataSource); foundSet.removeLastFound(); try { foundSet.deleteAllRecords(); } catch (Exception e) { Debug.log(e); table = null; } } GlobalTransaction gt = getGlobalTransaction(); String tid = null; if (gt != null) { tid = gt.getTransactionID(table == null ? IServer.INMEM_SERVER : table.getServerName()); } table = application.getDataServer().insertDataSet(application.getClientID(), dataSet, dataSource, table == null ? IServer.INMEM_SERVER : table.getServerName(), table == null ? null : table.getName() /* create temp table when null */, tid, intTypes /* inferred from dataset when null */, pkNames); if (table != null) { inMemDataSources.put(dataSource, table); fireTableEvent(table); refreshFoundSetsFromDB(dataSource, null, false); return dataSource; } } catch (RemoteException e) { throw new RepositoryException(e); } return null; } public boolean removeDataSource(String uri) throws RepositoryException { if (uri == null) { return false; } try { ITable table = inMemDataSources.remove(uri); if (table != null) { sharedDataSourceFoundSet.remove(uri); application.getDataServer().dropTemporaryTable(application.getClientID(), table.getServerName(), table.getName()); return true; } } catch (RemoteException e) { throw new RepositoryException(e); } return false; } /** * Test validity of data sources that are for this client. * * @return */ public boolean hasClientDataSources() { // in-memory data sources return inMemDataSources.size() > 0; } /** * Register this client as table user for all used tables. * * @param serverName when non-null limit to server name. * * @throws ServoyException */ public void registerClientTables(String serverName) throws ServoyException { for (String datasource : rowManagers.keySet()) { if (serverName == null || serverName.equals(DataSourceUtils.getDataSourceServerName(datasource))) { ITable t = getTable(datasource); try { if (Debug.tracing()) { Debug.trace("Registering table '" + t.getServerName() + ". " + t.getName() + "' for client '" + application.getClientID() + "'"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$//$NON-NLS-4$ } getDataServer().addClientAsTableUser(application.getClientID(), t.getServerName(), t.getName()); } catch (RemoteException e) { throw new RepositoryException(e); } } } } public int saveData() { return editRecordList.stopEditing(false); } public int saveData(List<IRecord> recordsToSave) { return editRecordList.stopEditing(true, recordsToSave); } @Override public IRecord[] getFailedRecords() { return editRecordList.getFailedRecords(); } /** register runnables that contain fire calls, should be done after foundsets are created. * @param runnable */ public void registerFireRunnables(List<Runnable> runnables) { synchronized (fireRunabbles) { fireRunabbles.addAll(runnables); } } /** * @see com.servoy.j2db.IServiceProvider#setTrackingInfo(com.servoy.j2db.util.Pair) */ public void addTrackingInfo(String columnName, Object value) { if (columnName != null) { if (value == null) trackingInfoMap.remove(columnName); else trackingInfoMap.put(columnName, value); } } /** * @see com.servoy.j2db.IServiceProvider#getTrackingInfo() */ public HashMap<String, Object> getTrackingInfo() { return trackingInfoMap; } public IQueryBuilderFactory getQueryFactory() { return new QBFactory(this, getScopesScopeProvider(), getApplication().getFlattenedSolution(), getApplication().getScriptEngine().getSolutionScope()); } public IFoundSetInternal getFoundSet(String dataSource) throws ServoyException { IFoundSetInternal fs = getNewFoundSet(dataSource, null, getDefaultPKSortColumns(dataSource)); fs.clear();//have to deliver a initialized foundset, user might call new record as next call on this one return fs; } public List<SortColumn> getDefaultPKSortColumns(String dataSource) throws ServoyException { return getSQLGenerator().getCachedTableSQLSheet(dataSource).getDefaultPKSort(); } public IFoundSet getFoundSet(IQueryBuilder query) throws ServoyException { QBSelect select = (QBSelect)query; IFoundSet fs = getNewFoundSet(select.getDataSource(), select.getQuery(), null /* use sorting defined in query */); fs.loadAllRecords(); return fs; } public IDataSet getDataSetByQuery(IQueryBuilder query, int max_returned_rows) throws ServoyException { return getDataSetByQuery(query, true, max_returned_rows); } public IDataSet getDataSetByQuery(IQueryBuilder query, boolean useTableFilters, int max_returned_rows) throws ServoyException { if (!application.haveRepositoryAccess()) { // no access to repository yet, have to log in first return null; } QBSelect select = (QBSelect)query; String serverName = DataSourceUtils.getDataSourceServerName(select.getDataSource()); if (serverName == null) throw new RuntimeException(new ServoyException(ServoyException.InternalCodes.SERVER_NOT_FOUND, new Object[] { select.getDataSource() })); return getDataSetByQuery(serverName, select.build(), useTableFilters, max_returned_rows); } }