/* 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.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.Wrapper; import com.servoy.base.query.BaseQueryTable; import com.servoy.base.scripting.api.IJSDataSet; import com.servoy.base.scripting.api.IJSFoundSet; import com.servoy.base.scripting.api.IJSRecord; import com.servoy.j2db.Messages; import com.servoy.j2db.persistence.IRelation; import com.servoy.j2db.persistence.Relation; import com.servoy.j2db.persistence.RepositoryException; import com.servoy.j2db.persistence.Table; import com.servoy.j2db.query.ISQLTableJoin; import com.servoy.j2db.query.QuerySelect; import com.servoy.j2db.query.QueryTable; import com.servoy.j2db.util.Debug; import com.servoy.j2db.util.FormatParser.ParsedFormat; import com.servoy.j2db.util.IDelegate; import com.servoy.j2db.util.ScopesUtils; import com.servoy.j2db.util.ServoyException; import com.servoy.j2db.util.Utils; /** * This class is passed as value by the JEditListModel(==FormModel) and represents 1 row * * @author jblok */ public class FindState implements Scriptable, IRecordInternal, Serializable, IJSRecord { private final Map<String, Object> columndata;//actual find columndata private final IFoundSetInternal parent; private final Map<String, IFoundSetInternal> relatedStates; /** * Constructor */ FindState(IFoundSetInternal parent) { this.parent = parent; columndata = new HashMap<String, Object>(); relatedStates = new HashMap<String, IFoundSetInternal>(); } List<Relation> getValidSearchRelations() { List<Relation> retval = new ArrayList<Relation>(); try { Iterator<Relation> it = parent.getFoundSetManager().getApplication().getFlattenedSolution().getRelations(parent.getTable(), true, false); while (it.hasNext()) { Relation element = it.next(); if (element.isUsableInSearch()) { retval.add(element); } } } catch (RepositoryException e) { Debug.error(e); } return retval; } /** * Duplicate this findState. */ public FindState duplicate() { FindState dup = new FindState(parent); dup.columndata.putAll(columndata); dup.relatedStates.putAll(relatedStates); return dup; } public IFoundSetInternal getParentFoundSet() { return parent; } /** * Store columns, aggregates and calculations in find state. Default find will ignore calculations and aggregates but onSearch method may use them in * customized searches. * * @param dataProviderID * @return */ private boolean storeDataProvider(String dataProviderID) { SQLSheet parentSheet = parent.getSQLSheet(); return parentSheet.getColumnIndex(dataProviderID) != -1 || parentSheet.containsCalculation(dataProviderID) || parentSheet.containsAggregate(dataProviderID); } /** * called by data adapter for a new value (calcs MUST recalc) * * @param dataProviderID the data requested for */ public Object getValue(String dataProviderID) { return getValue(dataProviderID, true); } public Object getValue(String dataProviderID, boolean converted) { if (storeDataProvider(dataProviderID)) { return columndata.get(dataProviderID); } if (ScopesUtils.isVariableScope(dataProviderID)) { // Do return the global values, needed for global relations. return parent.getDataProviderValue(dataProviderID); } int index = dataProviderID.indexOf('.'); if (index > 0) { String partName = dataProviderID.substring(0, index); String restName = dataProviderID.substring(index + 1); IFoundSetInternal foundSet = getRelatedFoundSet(partName);//check substate, will return null if not found if (foundSet != null) { IRecordInternal state = foundSet.getRecord(0); if (state != null) { return state.getValue(restName, converted); } } return null; } IFoundSetInternal fs = getRelatedFoundSet(dataProviderID); if (fs != null) { return fs; } return Scriptable.NOT_FOUND; } public Object setValue(String dataProviderID, Object value) { return setValue(dataProviderID, value, true); } public Object setValue(String dataProviderID, Object value, boolean checkIsEditing) { Object oldValue = setValueImpl(dataProviderID, value); if (oldValue != value)//did change? { fireJSModificationEvent(dataProviderID, value); } return oldValue; } private Object setValueImpl(String dataProviderID, Object value) { if (storeDataProvider(dataProviderID)) { return columndata.put(dataProviderID, Utils.mapToNullIfUnmanageble(value)); } if (ScopesUtils.isVariableScope(dataProviderID)) { // do set the global in the global scope so that globals just work in find mode for global relations return parent.setDataProviderValue(dataProviderID, value); } //check if is related value request int index = dataProviderID.indexOf('.'); if (index > 0) { String partName = dataProviderID.substring(0, index); String restName = dataProviderID.substring(index + 1); IFoundSetInternal foundSet = getRelatedFoundSet(partName);//check substate, will return null if not found if (foundSet != null) { // set this related foundset in editing mode, so that the value are stored in db. Object oldVal = foundSet.setDataProviderValue(restName, value); if (oldVal != value) { IRecordInternal state = foundSet.getRecord(0); if (state != null) state.startEditing(); } return oldVal; } } Debug.log("Ignoring unknown data provider '" + dataProviderID + "' in find mode"); //$NON-NLS-1$ //$NON-NLS-2$ return null; } public IFoundSetInternal getRelatedFoundSet(String name) { return getRelatedFoundSet(name, null);//only used for related fields, sort is irrelevant } private boolean isEditing = false; public boolean startEditing(boolean b) { isEditing = true; return isEditing; } public boolean startEditing() { return startEditing(true); } public int stopEditing() { isEditing = false; return ISaveConstants.STOPPED; } public boolean isEditing() { return isEditing; } public boolean isLocked() { return false; } public boolean isChanged() { return (columndata.size() != 0); } /* * _____________________________________________________________ JavaScriptModificationListner */ private final List<IModificationListener> modificationListner = Collections.synchronizedList(new ArrayList<IModificationListener>(3));//only one possible on State public void addModificationListener(IModificationListener listner) { if (listner != null) modificationListner.add(listner); } public void removeModificationListener(IModificationListener listner) { if (listner != null) modificationListner.remove(listner); } @Deprecated public void addModificationListner(IModificationListener l) { addModificationListener(l); } @Deprecated public void removeModificationListner(IModificationListener l) { removeModificationListener(l); } private void fireJSModificationEvent(String name, Object value) { if (modificationListner.size() > 0) { ModificationEvent me = new ModificationEvent(name, value, this); fireJSModificationEvent(me); } } private void fireJSModificationEvent(ModificationEvent me) { // Test if this record is in edit state for stopping it below if nessesary boolean editState = this.isEditing(); me.setRecord(this); Object[] array = modificationListner.toArray(); for (Object element : array) { ((IModificationListener)element).valueChanged(me); } // If it wasn't editting and now it is (see RelookupdAdapter modification) then stop it now so that every change // is recorded in one go and stored in one update if (!editState && isEditing()) { try { this.stopEditing(); } catch (Exception e) { Debug.error(e); } } } /* * _____________________________________________________________ Scriptable implementation */ public void delete(int index) { Debug.trace("ignore State:delete " + index); //$NON-NLS-1$ } public void delete(String name) { Debug.trace("ignore State:delete " + name); //$NON-NLS-1$ } public Object get(int index, Scriptable start) { Debug.trace("ignore State:get " + index + " " + start); //$NON-NLS-1$ //$NON-NLS-2$ return null; } public Object get(String name, Scriptable start) { Object o = getValue(name); if (o == null && !storeDataProvider(name)) { o = Scriptable.NOT_FOUND; } return o; } public String getClassName() { return "FindRecord"; //$NON-NLS-1$ } public Object getDefaultValue(Class hint) { return toString(); } public Object[] getIds() { SQLSheet parentSheet = parent.getSQLSheet(); return parentSheet.getColumnNames(); } public Scriptable getParentScope() { Debug.trace("ignore State:getParentScope"); //$NON-NLS-1$ return null; } public Scriptable getPrototype() { // Debug.trace("ignore State:getPrototype"); return null; } /** * @see com.servoy.j2db.dataprocessing.IRecord#has(java.lang.String) */ public boolean has(String dataprovider) { return true; } public boolean has(int index, Scriptable start) { Debug.trace("ignore State:has " + index + " " + start); //$NON-NLS-1$ //$NON-NLS-2$ return false; } public boolean has(String name, Scriptable start) { return true;//TODO: is this oke?? } public boolean hasInstance(Scriptable instance) { Debug.trace("ignore State:hasInstance " + instance); //$NON-NLS-1$ return false; } public void put(int index, Scriptable start, Object value) { Debug.trace("ignore State:put " + index + " " + start + " " + value); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } public void put(String name, Scriptable start, Object val) { Object value = val; if (value instanceof IDelegate) { value = ((IDelegate)value).getDelegate(); if (value instanceof IDataSet) { IDataSet set = (IDataSet)value; StringBuffer sb = new StringBuffer(); sb.append("\n"); //$NON-NLS-1$ for (int i = 0; i < set.getRowCount(); i++) { sb.append(set.getRow(i)[0]); sb.append("\n"); //$NON-NLS-1$ } value = sb.toString(); } } else if (value instanceof FoundSet) { StringBuffer sb = new StringBuffer(); sb.append("\n"); //$NON-NLS-1$ FoundSet fs = (FoundSet)value; for (int i = 0; i < fs.getSize(); i++) { IRecordInternal record = fs.getRecord(i); sb.append(record.getPKHashKey()); sb.append("\n"); //$NON-NLS-1$ } value = sb.toString(); } else { Object tmp = value; while (tmp instanceof Wrapper) { tmp = ((Wrapper)tmp).unwrap(); if (tmp == value) { break; } } value = tmp; } setValue(name, value); } public void setParentScope(Scriptable parent) { Debug.trace("ignore State:setParentScope " + parent); //$NON-NLS-1$ } public void setPrototype(Scriptable prototype) { Debug.trace("ignore State:setPrototype " + prototype); //$NON-NLS-1$ } /* * _____________________________________________________________ Related states implementation */ /** * Get related foundset, relationName may be multiple-levels deep */ public IFoundSetInternal getRelatedFoundSet(String relationName, List<SortColumn> defaultSortColumns) { if (relationName == null || parent == null) return null; String partName = relationName; String restName = null; int index = relationName.indexOf('.'); if (index > 0) { partName = relationName.substring(0, index); restName = relationName.substring(index + 1); } IFoundSetInternal rfs = relatedStates.get(partName); if (rfs == null) { Relation r = parent.getFoundSetManager().getApplication().getFlattenedSolution().getRelation(partName); if (r == null) return null; //safety try { if (r.isGlobal() || r.isParentRef()) { rfs = parent.getRelatedFoundSet(this, partName, defaultSortColumns); // do not store in relatedStates because it is not a relatedfindfoundset } else { if (!getValidSearchRelations().contains(r)) { String reason = ""; if (r.isGlobal()) reason = "global relation"; else if (r.isMultiServer()) reason = "multi server"; else if (!r.isValid()) reason = "server/table not valid/loaded"; else { reason = "relation primary datasource: " + r.getPrimaryDataSource() + " != findstate primary datasource: " + parent.getDataSource(); } Debug.warn("Find: skip related search for '" + partName + "', relation cannot be used in search, because: " + reason); parent.getFoundSetManager().getApplication().reportJSError( Messages.getString("servoy.relation.find.unusable", new Object[] { partName }) + " (" + reason + ')', null); //$NON-NLS-2$ return null; } SQLSheet sheet = parent.getSQLSheet().getRelatedSheet( ((FoundSetManager)parent.getFoundSetManager()).getApplication().getFlattenedSolution().getRelation(partName), ((FoundSetManager)parent.getFoundSetManager()).getSQLGenerator()); rfs = ((FoundSetManager)parent.getFoundSetManager()).createRelatedFindFoundSet(this, partName, sheet); ((FoundSet)rfs).addParent(this); ((FoundSet)rfs).setFindMode(); relatedStates.put(partName, rfs); } } catch (ServoyException ex) { Debug.error("Error making related findstate", ex); //$NON-NLS-1$ return null; } } if (restName != null) { IRecordInternal record = rfs.getRecord(rfs.getSelectedIndex()); if (record == null) return null; return record.getRelatedFoundSet(restName); } return rfs; } public boolean existInDataSource() { return true;//pretend to be stored, we never want to store this } @Deprecated public boolean existInDB() { return existInDataSource(); } public String getAsTabSeparated() { return null; } private final HashMap<String, ParsedFormat> formats = new HashMap<String, ParsedFormat>(); public void setFormat(String dataProviderID, ParsedFormat format) { if (format == null || format.getDisplayFormat() == null || ScopesUtils.isVariableScope(dataProviderID)) return; int index = dataProviderID.lastIndexOf('.'); if (index > 0) { String partName = dataProviderID.substring(0, index); String restName = dataProviderID.substring(index + 1); IFoundSetInternal foundSet = getRelatedFoundSet(partName);//check substate, will return null if not found if (foundSet != null) { FindState state = (FindState)foundSet.getRecord(0); if (state != null) { state.setFormat(restName, format); } } return; } if (!formats.containsKey(dataProviderID)) { formats.put(dataProviderID, format); } } public ParsedFormat getFormat(String dataProviderID) { return formats.get(dataProviderID); } public String getPKHashKey() { return ""; //$NON-NLS-1$ } public Object[] getPK() { return null; } /** * @see com.servoy.j2db.dataprocessing.IState#flagExistInDB() */ public void flagExistInDB() { } /** * @return nothing */ public Row getRawData() { return null; } public Map<String, Object> getColumnData() { return columndata; } /** * @see com.servoy.j2db.dataprocessing.IRowChangeListener#notifyChange(com.servoy.j2db.scripting.ModificationEvent) */ public void notifyChange(ModificationEvent e, FireCollector col) { //not needed here } /** * Find all processable related find states and create joins. A find state is processable when it has changed or when a related find state has changed. * @param sqlSelect * @param relations path to this state * @param selectTable * @param provider * @return * @throws RepositoryException */ public List<RelatedFindState> createFindStateJoins(QuerySelect sqlSelect, List<IRelation> relations, BaseQueryTable selectTable, IGlobalValueEntry provider) throws RepositoryException { List<RelatedFindState> relatedFindStates = null; List<Relation> searchRelations = getValidSearchRelations(); // find processable find states of related find states for (int i = 0; i < searchRelations.size(); i++) { Relation relation = searchRelations.get(i); if (relation != null) { IFoundSetInternal set = relatedStates.get(relation.getName()); if (set != null && set.getSize() > 0) { ISQLTableJoin existingJoin = (ISQLTableJoin)sqlSelect.getJoin(selectTable, relation.getName()); BaseQueryTable foreignQTable; if (existingJoin == null) { Table foreignTable = relation.getForeignTable(); foreignQTable = new QueryTable(foreignTable.getSQLName(), foreignTable.getDataSource(), foreignTable.getCatalog(), foreignTable.getSchema()); } else { foreignQTable = existingJoin.getForeignTable(); } FindState fs = (FindState)set.getRecord(0); List<IRelation> nextRelations = new ArrayList<IRelation>(relations); nextRelations.add(relation); List<RelatedFindState> rfs = fs.createFindStateJoins(sqlSelect, nextRelations, foreignQTable, provider); if (rfs != null && rfs.size() > 0) { // changed related findstate, add self with join if (relatedFindStates == null) { relatedFindStates = rfs; } else { relatedFindStates.addAll(rfs); } if (existingJoin == null) { sqlSelect.addJoin(SQLGenerator.createJoin(parent.getFoundSetManager().getApplication().getFlattenedSolution(), relation, selectTable, foreignQTable, provider)); } } } } } // add yourself if you have changed or one or more related states has changed if (isChanged() || (relatedFindStates != null && relatedFindStates.size() > 0)) { if (relatedFindStates == null) { relatedFindStates = new ArrayList<RelatedFindState>(); } relatedFindStates.add(new RelatedFindState(this, relations, selectTable)); } return relatedFindStates; } /* * (non-Javadoc) * * @see java.awt.Component#toString() */ @Override public String toString() { StringBuffer sb = new StringBuffer(); sb.append("FindRecord[COLUMS: {"); //$NON-NLS-1$ Object[] objects = getIds(); if (objects != null) { for (Object element : objects) { sb.append(element); sb.append(","); //$NON-NLS-1$ } } sb.append("} DATA:"); //$NON-NLS-1$ sb.append(columndata); sb.append(", RELATED: "); //$NON-NLS-1$ sb.append(relatedStates); sb.append("]"); //$NON-NLS-1$ return sb.toString(); } public boolean isRelatedFoundSetLoaded(String relationName, String restName) { return true;//return true to prevent async loading. } public IJSDataSet getChangedData() { return null; } public String getDataSource() { return parent.getDataSource(); } public Exception getException() { return null; } public IJSFoundSet getFoundset() { return (IJSFoundSet)parent; } public Object[] getPKs() { return null; } public boolean hasChangedData() { return false; } public boolean isNew() { return false; } public void revertChanges() { } public void rowRemoved() { } /** * @author rgansevles * */ public static class RelatedFindState { private final FindState findState; private final BaseQueryTable primaryTable; private final List<IRelation> relations; /** * @param findState * @param relation */ public RelatedFindState(FindState findState, List<IRelation> relations, BaseQueryTable primaryTable) { this.findState = findState; this.relations = relations; this.primaryTable = primaryTable; } public FindState getFindState() { return findState; } public List<IRelation> getRelations() { return relations; } public BaseQueryTable getPrimaryTable() { return primaryTable; } } }