/* 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.rmi.RemoteException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import com.servoy.base.persistence.constants.IValueListConstants; import com.servoy.base.query.BaseQueryTable; import com.servoy.j2db.IServiceProvider; import com.servoy.j2db.persistence.Column; import com.servoy.j2db.persistence.IDataProvider; import com.servoy.j2db.persistence.IRepository; import com.servoy.j2db.persistence.Relation; import com.servoy.j2db.persistence.RepositoryException; import com.servoy.j2db.persistence.Table; import com.servoy.j2db.persistence.ValueList; import com.servoy.j2db.query.AbstractBaseQuery; import com.servoy.j2db.query.IQuerySelectValue; import com.servoy.j2db.query.ISQLTableJoin; import com.servoy.j2db.query.QuerySelect; import com.servoy.j2db.query.QueryTable; import com.servoy.j2db.query.TablePlaceholderKey; import com.servoy.j2db.util.Debug; import com.servoy.j2db.util.Pair; import com.servoy.j2db.util.SafeArrayList; import com.servoy.j2db.util.ServoyException; /** * Valuelist based on values from a relation (related foundset) * * @author jblok */ public class RelatedValueList extends DBValueList implements IFoundSetEventListener { private final boolean concatShowValues; private final boolean concatReturnValues; private IRecordInternal parentState; private final List<IFoundSetInternal> related = new ArrayList<IFoundSetInternal>(); public RelatedValueList(IServiceProvider app, ValueList vl) { super(app, vl); int showValues = vl.getShowDataProviders(); int returnValues = vl.getReturnDataProviders(); Relation[] relations = application.getFlattenedSolution().getRelationSequence(valueList.getRelationName()); if (relations != null && relations.length > 0) { try { setContainsCalculationFlag(relations[relations.length - 1].getForeignTable()); } catch (RepositoryException e) { Debug.error(e); } } //more than one value -> concat concatShowValues = (showValues & 7) != 1 && (showValues & 7) != 2 && (showValues & 7) != 4; concatReturnValues = (returnValues & 7) != 1 && (returnValues & 7) != 2 && (returnValues & 7) != 4; fill(null); } @Override public void deregister() { if (registered && valueList.getDatabaseValuesType() == IValueListConstants.RELATED_VALUES) { try { Relation[] relations = application.getFlattenedSolution().getRelationSequence(valueList.getRelationName()); if (relations != null) { for (Relation relation : relations) { if (!relation.isGlobal()) { //if changes are performed on the data refresh this list. ((FoundSetManager)application.getFoundSetManager()).removeTableListener(relation.getForeignTable(), this); } } } } catch (RepositoryException e) { Debug.error(e); } } registered = false; } @Override public String getRelationName() { return valueList.getRelationName(); } @Override public void tableChange(TableEvent e) { refill(); } public void foundSetChanged(FoundSetEvent e) { if (e.getType() == FoundSetEvent.CONTENTS_CHANGED) refill(); } public void refill() { if (parentState != null && valueList != null) { for (IFoundSetInternal fs : related) { fs.removeFoundSetEventListener(this); } related.clear(); //to pass performance check in fill fill(parentState); } } @Override public void fill(IRecordInternal ps)//to create all the rows { this.parentState = ps; Relation[] relations = application.getFlattenedSolution().getRelationSequence(valueList.getRelationName()); if (relations != null) { try { defaultSort = ((FoundSetManager)application.getFoundSetManager()).getSortColumns(relations[relations.length - 1].getForeignDataSource(), valueList.getSortOptions()); // check if all relations go to the same server boolean sameServer = true; String serverName = relations[0].getForeignServerName(); for (Relation relation : relations) { //if changes are performed on the data refresh this list. if (!relation.isGlobal()) { if (!registered) { ((FoundSetManager)application.getFoundSetManager()).addTableListener(relation.getForeignTable(), this); } if (sameServer) { sameServer = serverName.equals(relation.getForeignServerName()); } } } registered = true; int size = super.getSize(); stopBundlingEvents(); // to be on the safe side removeAllElements(); for (IFoundSetInternal fs : related) { fs.removeFoundSetEventListener(this); } related.clear(); realValues = new SafeArrayList<Object>(); fireIntervalRemoved(this, 0, size); if (sameServer && relations.length > 1 && !containsCalculation) { // multiple-level relation on same server, use query fillWithQuery(relations); } else { // 1-level relation or different servers, use foundsets fillFromFoundset(relations); } } catch (Exception e) { Debug.error(e);//due to buggy swing ui on macosx 131 this is needed, throws nullp when filled and not selected } } super.fill(ps); stopBundlingEvents(); // to be on the safe side fireContentsChanged(this, -1, -1); } private String[] getDisplayFormat(Table table) { if (table != null && hasRealValues()) { String[] displayFormats = new String[3]; Column col1 = table.getColumn(valueList.getDataProviderID1()); if (col1 != null && col1.getColumnInfo() != null) displayFormats[0] = col1.getColumnInfo().getDefaultFormat(); Column col2 = table.getColumn(valueList.getDataProviderID2()); if (col2 != null && col2.getColumnInfo() != null) displayFormats[1] = col2.getColumnInfo().getDefaultFormat(); Column col3 = table.getColumn(valueList.getDataProviderID3()); if (col3 != null && col3.getColumnInfo() != null) displayFormats[2] = col3.getColumnInfo().getDefaultFormat(); return displayFormats; } return null; } /** * Fill from foundset in case of one-level relation or multiple servers * * @throws ServoyException */ @SuppressWarnings("nls") public void fillFromFoundset(Relation[] relations) throws ServoyException { try { IFoundSetInternal relatedFoundSet; if (parentState == null) { relatedFoundSet = application.getFoundSetManager().getGlobalRelatedFoundSet(relations[0].getName()); } else if (parentState instanceof FindState) { relatedFoundSet = parentState.getParentFoundSet().getRelatedFoundSet(parentState, relations[0].getName(), application.getFoundSetManager().getDefaultPKSortColumns(relations[0].getForeignDataSource())); } else { relatedFoundSet = parentState.getRelatedFoundSet(relations[0].getName()); } for (IFoundSetInternal fs : related) { fs.removeFoundSetEventListener(this); } List<IFoundSetInternal> oldrelated = new ArrayList<IFoundSetInternal>(related); related.clear(); if (relatedFoundSet != null) { int showValues = valueList.getShowDataProviders(); int returnValues = valueList.getReturnDataProviders(); boolean showAndReturnAreSame = showValues == returnValues; try { List<IRecordInternal> allRecords = new ArrayList<IRecordInternal>(); getRelatedRecords(allRecords, related, relatedFoundSet, relations, 1, maxValuelistRows); startBundlingEvents(); //add empty row if (valueList.getAddEmptyValue() == IValueListConstants.EMPTY_VALUE_ALWAYS) { addElement(""); //$NON-NLS-1$ realValues.add(null); } if (related.equals(oldrelated)) { return; //performance check } IRecordInternal[] records = allRecords.toArray(new IRecordInternal[allRecords.size()]); if (defaultSort != null) Arrays.sort(records, new RecordComparator(defaultSort)); if (records.length >= maxValuelistRows) { application.reportJSError("Valuelist " + getName() + " fully loaded with " + maxValuelistRows + " rows, more rows are discarded!!", null); } String[] displayFormat = getDisplayFormat((Table)relatedFoundSet.getTable()); for (IRecordInternal state : records) { if (state == null || state.getParentFoundSet() == null) continue; Object element = CustomValueList.handleRowData(valueList, displayFormat, concatShowValues, showValues, state, application); if (showAndReturnAreSame && indexOf(element) > -1) continue; addElement(element); realValues.add(CustomValueList.handleRowData(valueList, null, concatReturnValues, returnValues, state, application)); } } finally { stopBundlingEvents(); } isLoaded = true; } } finally { for (IFoundSetInternal fs : related) { fs.addFoundSetEventListener(this); } } } private static void getRelatedRecords(List<IRecordInternal> relatedRecords, List<IFoundSetInternal> visitedFoundsets, IFoundSetInternal foundSet, Relation[] relations, int depth, int maxValuelistRows) { if (foundSet != null) { visitedFoundsets.add(foundSet); IRecordInternal[] records = foundSet.getRecords(0, maxValuelistRows); for (int i = 0; relatedRecords.size() < maxValuelistRows && i < records.length; i++) { if (depth == relations.length) { if (!relatedRecords.contains(records[i])) { relatedRecords.add(records[i]); } } else { // go one level deeper getRelatedRecords(relatedRecords, visitedFoundsets, ((Record)records[i]).getRelatedFoundSet(relations[depth].getName()), relations, depth + 1, maxValuelistRows); } } } } /** * Fill using query in case of multi-level relation * * @param relations * @throws ServoyException * @throws RemoteException */ @SuppressWarnings("nls") protected void fillWithQuery(Relation[] relations) throws RemoteException, ServoyException { FoundSetManager foundSetManager = (FoundSetManager)application.getFoundSetManager(); Pair<QuerySelect, BaseQueryTable> pair = createRelatedValuelistQuery(application, valueList, relations, parentState); if (pair == null) { return; } QuerySelect select = pair.getLeft(); int showValues = valueList.getShowDataProviders(); int returnValues = valueList.getReturnDataProviders(); boolean showAndReturnAreSame = showValues == returnValues; // perform the query String serverName = relations[0].getForeignServerName(); SQLStatement trackingInfo = null; if (foundSetManager.getEditRecordList().hasAccess(relations[relations.length - 1].getForeignTable(), IRepository.TRACKING_VIEWS)) { trackingInfo = new SQLStatement(ISQLActionTypes.SELECT_ACTION, serverName, pair.getRight().getName(), null, null); trackingInfo.setTrackingData(select.getColumnNames(), new Object[][] { }, new Object[][] { }, application.getUserUID(), foundSetManager.getTrackingInfo(), application.getClientID()); } IDataSet dataSet = application.getDataServer().performQuery(application.getClientID(), serverName, foundSetManager.getTransactionID(serverName), select, foundSetManager.getTableFilterParams(serverName, select), !select.isUnique(), 0, maxValuelistRows, IDataServer.VALUELIST_QUERY, trackingInfo); try { startBundlingEvents(); //add empty row if (valueList.getAddEmptyValue() == IValueListConstants.EMPTY_VALUE_ALWAYS) { addElement(""); //$NON-NLS-1$ realValues.add(null); } if (dataSet.getRowCount() >= maxValuelistRows) { application.reportJSError("Valuelist " + getName() + " fully loaded with " + maxValuelistRows + " rows, more rows are discarded!!", null); } String[] displayFormat = getDisplayFormat((Table)application.getFoundSetManager().getTable(pair.getRight().getDataSource())); for (int i = 0; i < dataSet.getRowCount(); i++) { Object[] row = CustomValueList.processRow(dataSet.getRow(i), showValues, returnValues); Object element = null; if (displayFormat != null) element = CustomValueList.handleDisplayData(valueList, displayFormat, concatShowValues, showValues, row, application).toString(); else element = CustomValueList.handleRowData(valueList, concatShowValues, showValues, row, application); if (showAndReturnAreSame && indexOf(element) != -1) continue; addElement(element); realValues.add(CustomValueList.handleRowData(valueList, concatReturnValues, returnValues, row, application)); } } finally { stopBundlingEvents(); } isLoaded = true; } public static Pair<QuerySelect, BaseQueryTable> createRelatedValuelistQuery(IServiceProvider application, ValueList valueList, Relation[] relations, IRecordInternal parentState) throws ServoyException { if (parentState == null) { return null; } FoundSetManager foundSetManager = (FoundSetManager)application.getFoundSetManager(); SQLGenerator sqlGenerator = foundSetManager.getSQLGenerator(); IGlobalValueEntry scopesScopeProvider = foundSetManager.getScopesScopeProvider(); SQLSheet childSheet = sqlGenerator.getCachedTableSQLSheet(relations[0].getPrimaryDataSource()); // this returns quickly if it already has a sheet for that relation, but optimize further? sqlGenerator.makeRelatedSQL(childSheet, relations[0]); QuerySelect select = AbstractBaseQuery.deepClone((QuerySelect)childSheet.getRelatedSQLDescription(relations[0].getName()).getSQLQuery()); Object[] relationWhereArgs = foundSetManager.getRelationWhereArgs(parentState, relations[0], false); if (relationWhereArgs == null) { return null; } TablePlaceholderKey placeHolderKey = SQLGenerator.createRelationKeyPlaceholderKey(select.getTable(), relations[0].getName()); if (!select.setPlaceholderValue(placeHolderKey, relationWhereArgs)) { Debug.error(new RuntimeException("Could not set relation placeholder " + placeHolderKey + " in query " + select)); //$NON-NLS-1$//$NON-NLS-2$ return null; } BaseQueryTable lastTable = select.getTable(); Table foreignTable = relations[0].getForeignTable(); for (int i = 1; i < relations.length; i++) { foreignTable = relations[i].getForeignTable(); ISQLTableJoin join = SQLGenerator.createJoin(application.getFlattenedSolution(), relations[i], lastTable, new QueryTable(foreignTable.getSQLName(), foreignTable.getDataSource(), foreignTable.getCatalog(), foreignTable.getSchema()), scopesScopeProvider); select.addJoin(join); lastTable = join.getForeignTable(); } List<SortColumn> defaultSort = foundSetManager.getSortColumns(relations[relations.length - 1].getForeignDataSource(), valueList.getSortOptions()); foundSetManager.getSQLGenerator().addSorts(select, lastTable, scopesScopeProvider, foreignTable, defaultSort, true); int showValues = valueList.getShowDataProviders(); int returnValues = valueList.getReturnDataProviders(); int total = (showValues | returnValues); ArrayList<IQuerySelectValue> columns = new ArrayList<IQuerySelectValue>(); if ((total & 1) != 0) { columns.add(getQuerySelectValue(foreignTable, lastTable, valueList.getDataProviderID1())); } if ((total & 2) != 0) { columns.add(getQuerySelectValue(foreignTable, lastTable, valueList.getDataProviderID2())); } if ((total & 4) != 0) { columns.add(getQuerySelectValue(foreignTable, lastTable, valueList.getDataProviderID3())); } select.setColumns(columns); select.setDistinct(false); // not allowed in all situations return new Pair<QuerySelect, BaseQueryTable>(select, lastTable); } @Override public IDataProvider[] getDependedDataProviders() { Relation[] relations = application.getFlattenedSolution().getRelationSequence(valueList.getRelationName()); if (relations != null && relations.length > 0) { try { return relations[relations.length - 1].getPrimaryDataProviders(application.getFlattenedSolution()); } catch (RepositoryException e) { Debug.error(e); } } // If it can't get from the relation, just return that it depends on all. return new IDataProvider[0]; } }