/* 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.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.mozilla.javascript.BaseFunction; import org.mozilla.javascript.Context; import org.mozilla.javascript.MemberBox; import org.mozilla.javascript.NativeJavaMethod; import org.mozilla.javascript.ScriptRuntime; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.Undefined; import org.mozilla.javascript.WrappedException; import org.mozilla.javascript.Wrapper; import org.mozilla.javascript.annotations.JSFunction; 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.ApplicationException; import com.servoy.j2db.documentation.ServoyDocumented; import com.servoy.j2db.persistence.Relation; import com.servoy.j2db.scripting.UsedDataProviderTracker; import com.servoy.j2db.scripting.UsedDataProviderTracker.UsedAggregate; import com.servoy.j2db.scripting.UsedDataProviderTracker.UsedDataProvider; import com.servoy.j2db.scripting.UsedDataProviderTracker.UsedRelation; import com.servoy.j2db.scripting.annotations.AnnotationManagerReflection; import com.servoy.j2db.scripting.annotations.JSReadonlyProperty; import com.servoy.j2db.util.Debug; 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 */ @ServoyDocumented(category = ServoyDocumented.RUNTIME, publicName = "JSRecord", scriptingName = "JSRecord") public class Record implements Scriptable, IRecordInternal, IJSRecord { public static final String JS_RECORD = "JSRecord"; //$NON-NLS-1$ /* * _____________________________________________________________ JavaScript stuff */ private static Map<String, NativeJavaMethod> jsFunctions = new HashMap<String, NativeJavaMethod>(); static { try { Method[] methods = Record.class.getMethods(); for (Method m : methods) { String name = null; if (m.getName().startsWith("js_")) //$NON-NLS-1$ { name = m.getName().substring(3); } else if (AnnotationManagerReflection.getInstance().isAnnotationPresent(m, Record.class, JSFunction.class) || AnnotationManagerReflection.getInstance().isAnnotationPresent(m, Record.class, JSReadonlyProperty.class)) { name = m.getName(); } if (name != null) { NativeJavaMethod nativeJavaMethod = jsFunctions.get(name); if (nativeJavaMethod == null) { nativeJavaMethod = new NativeJavaMethod(m, name); } else { nativeJavaMethod = new NativeJavaMethod(Utils.arrayAdd(nativeJavaMethod.getMethods(), new MemberBox(m), true), name); } jsFunctions.put(name, nativeJavaMethod); } } } catch (Exception e) { Debug.error(e); } } public static final ThreadLocal<Boolean> VALIDATE_CALCS = new ThreadLocal<Boolean>(); protected IFoundSetInternal parent; private Row row; //table row data (and calculations which is row related) //temp storage to make possible to stop edits on relatedFields, we do not cache/lookup here because the we can't flush substates globally (important for valuelists) private final Map<String, SoftReference<IFoundSetInternal>> relatedFoundSets; private final List<IModificationListener> modificationListeners; /** * Constructor I */ public Record(IFoundSetInternal parent, Row r) { this(parent); if (r == null) throw new IllegalArgumentException(parent.getFoundSetManager().getApplication().getI18NMessage("servoy.record.error.nullRow")); //$NON-NLS-1$ this.row = r; r.register(this); } /** * Constructor II (used by substate) */ Record(IFoundSetInternal parent) { this.parent = parent; this.relatedFoundSets = new HashMap<String, SoftReference<IFoundSetInternal>>(3); this.modificationListeners = Collections.synchronizedList(new ArrayList<IModificationListener>(3)); } void validateStoredCalculations() { if (VALIDATE_CALCS.get() != null) return; SQLSheet sheet = parent.getSQLSheet(); List<String> storedCalcs = sheet.getStoredCalculationNames(); //recalc all stored calcs (requered due to use of plugin methods in calc) for (String calc : storedCalcs) { getValue(calc); } } public IFoundSetInternal getParentFoundSet() { return parent; } /** * called by data adapter for a new value * * @param dataProviderID the data requested for * @param useCache, false if you want for sure the value recalculated if is calculation */ public final Object getValue(String dataProviderID) { return getValue(dataProviderID, true); } public Object getValue(String dataProviderID, boolean converted) { if (dataProviderID == null || parent == null) return null; if ("currentRecordIndex".equals(dataProviderID)) //$NON-NLS-1$ { return new Integer(parent.getRecordIndex(this) + 1); //deprecated } if ("foundset".equals(dataProviderID)) //$NON-NLS-1$ { return parent; } if ("exception".equals(dataProviderID)) //$NON-NLS-1$ { return row.getLastException(); } try { boolean containsCalc = row.containsCalculation(dataProviderID); boolean mustRecalc = containsCalc && row.mustRecalculate(dataProviderID, true); if (mustRecalc) { row.threadWillExecuteCalculation(dataProviderID); } mustRecalc = containsCalc && row.mustRecalculate(dataProviderID, true); if ((containsCalc || row.containsDataprovider(dataProviderID)) && !mustRecalc) { return converted ? row.getValue(dataProviderID) : row.getRawValue(dataProviderID);//also stored calcs are always calculated ones(required due to use of plugin methods in calc); } if (containsCalc) //check if calculation { UsedDataProviderTracker usedDataProviderTracker = new UsedDataProviderTracker( getParentFoundSet().getFoundSetManager().getApplication().getFlattenedSolution()); Object value = parent.getCalculationValue(this, dataProviderID, null, usedDataProviderTracker);//do real calc if (!(value instanceof Undefined)) { value = Utils.mapToNullIfUnmanageble(value); row.setValue(this, dataProviderID, value); } // re get it so that we do have the right type if the calc didn't return the type it specifies. // and that a converter is also applied. value = converted ? row.getValue(dataProviderID) : row.getRawValue(dataProviderID); manageCalcDependency(dataProviderID, usedDataProviderTracker); return value; } } finally { row.threadCalculationComplete(dataProviderID); } if (parent.containsDataProvider(dataProviderID)) //as shared (global or aggregate) { return parent.getDataProviderValue(dataProviderID); } if (ScopesUtils.isVariableScope(dataProviderID)) { return Utils.mapToNullIfUnmanageble(parent.getDataProviderValue(dataProviderID)); } int index = dataProviderID.lastIndexOf('.'); if (index > 0) //check if is related value request { String partName = dataProviderID.substring(0, index); String restName = dataProviderID.substring(index + 1); if ("lazyMaxRecordIndex".equals(restName)) //$NON-NLS-1$ { if (!isRelatedFoundSetLoaded(partName, restName)) { return "?"; //$NON-NLS-1$ } restName = "maxRecordIndex"; //$NON-NLS-1$ } IFoundSetInternal foundSet = getRelatedFoundSet(partName);// partName may be multiple levels deep; check substate, will return null if not found if (foundSet != null) { //related data int selected = foundSet.getSelectedIndex(); //in printing selected row will be set to -1, but if data is retrieved we need to use the first record again for use after printing... if (selected == -1 && foundSet.getSize() > 0) selected = 0; IRecordInternal state = foundSet.getRecord(selected); if (state != null) { return state.getValue(restName); } if (foundSet.containsDataProvider(restName)) { return foundSet.getDataProviderValue(restName); } } return null; } if (parent.isValidRelation(dataProviderID)) { return getRelatedFoundSet(dataProviderID); } return Scriptable.NOT_FOUND; } public Object setValue(String dataProviderID, Object value) { return setValue(dataProviderID, value, true); } /** * called by dataadapter * * @return oldvalue */ public Object setValue(String dataProviderID, Object value, boolean checkIsEditing) { Object managebleValue = Utils.mapToNullIfUnmanageble(value); if (row.containsDataprovider(dataProviderID)) { // when the value of a database column is set, the record must be in editing mode (not for unstored calcs) if (checkIsEditing && parent.getSQLSheet().getColumnIndex(dataProviderID) != -1 && !isEditing()) throw new IllegalStateException( "Record is not in edit, call startEditing() first"); //$NON-NLS-1$ boolean mustRecalculate = row.mustRecalculate(dataProviderID, true); Object prevValue = row.setValue(this, dataProviderID, managebleValue); if (mustRecalculate && row.containsCalculation(dataProviderID)) { // if a calculation is set, then just flag this row for recalculation so that it will be recalculated when it is asked for. // but only if it was in a mustRecalculate mode before (so it was it was never calculated or some depedency was changed) row.getRowManager().flagRowCalcForRecalculation(getPKHashKey(), dataProviderID); } return prevValue; } else if (parent.containsDataProvider(dataProviderID)) //as shared (global or aggregate) { return parent.setDataProviderValue(dataProviderID, managebleValue); } if (ScopesUtils.isVariableScope(dataProviderID)) { return parent.setDataProviderValue(dataProviderID, managebleValue); } //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) { return foundSet.setDataProviderValue(restName, managebleValue); } } return null; } //called by DisplaysAdapter or CellAdapter public boolean startEditing() { return startEditing(true); } public boolean startEditing(boolean mustFireEditRecordChange) { return getParentFoundSet().getFoundSetManager().getEditRecordList().startEditing(this, mustFireEditRecordChange); } //called by DataAdapterList, return changed public int stopEditing() { return getParentFoundSet().getFoundSetManager().getEditRecordList().stopEditing(false, this); } public boolean existInDataSource() { return row.existInDB(); } @Deprecated public boolean existInDB() { return existInDataSource(); } public boolean isLocked() { return false; } /* * _____________________________________________________________ JavaScriptModificationListner */ public void addModificationListener(IModificationListener listener) { if (listener != null) modificationListeners.add(listener); } public void removeModificationListener(IModificationListener listener) { if (listener != null) modificationListeners.remove(listener); } @Deprecated public void addModificationListner(IModificationListener l) { addModificationListener(l); } @Deprecated public void removeModificationListner(IModificationListener l) { removeModificationListener(l); } private void fireJSModificationEvent(String name, Object value) { if (modificationListeners.size() > 0) { fireJSModificationEvent(new ModificationEvent(name, value, this)); } } private void fireJSModificationEvent(ModificationEvent me) { // Test if this record is in edit state for stopping it below if necessary boolean isEditting = parent != null ? parent.getFoundSetManager().getEditRecordList().isEditing() : false; me.setRecord(this); Object[] array = modificationListeners.toArray(); for (Object element : array) { ((IModificationListener)element).valueChanged(me); } // If it wasn't editing 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 (!isEditting && isEditing()) { try { this.stopEditing(); } catch (Exception e) { Debug.error(e); } } else if (parent != null) // make sure pk is updated { if (Arrays.asList(parent.getSQLSheet().getPKColumnDataProvidersAsArray()).indexOf(me.getName()) != -1) { ((FoundSet)parent).updatePk(this); } } } /* * _____________________________________________________________ Scriptable impementation */ public void delete(int index) { // ignore } public void delete(String name) { // ignore } public Object get(int index, Scriptable start) { return Scriptable.NOT_FOUND; } public Object get(String name, Scriptable start) { if (FoundSet.isToplevelKeyword(name)) return Scriptable.NOT_FOUND; Object mobj = jsFunctions.get(name); if (mobj != null) { ScriptRuntime.setFunctionProtoAndParent((BaseFunction)mobj, start); return mobj; } Object o = getValue(name); if (o != null && o != Scriptable.NOT_FOUND && !(o instanceof Scriptable)) { Context context = Context.getCurrentContext(); if (context != null) o = context.getWrapFactory().wrap(context, start, o, o.getClass()); } return o; } public String getClassName() { return "Record"; //$NON-NLS-1$ } public Object getDefaultValue(Class hint) { return toString(); } public Object[] getIds() { List<String> al = new ArrayList<String>(); if (parent != null) { String[] columns = parent.getSQLSheet().getColumnNames(); for (String element : columns) { al.add(element); } // columns = parent.getSQLSheet().getCalculationNames(); // for (int i = 0; i < columns.length; i++) // { // if(!al.contains(columns[i])) al.add(columns[i]); // } // columns = parent.getSQLSheet().getAggregateNames(); // for (String element : columns) // { // al.add(element); // } } al.addAll(jsFunctions.keySet()); return al.toArray(); } private Scriptable parentScope; public Scriptable getParentScope() { if (parentScope == null) { return parent.getFoundSetManager().getApplication().getScriptEngine().getSolutionScope().getParentScope(); } return parentScope; } private Scriptable prototypeScope; public Scriptable getPrototype() { return prototypeScope; } /** * @see com.servoy.j2db.dataprocessing.IRecord#has(java.lang.String) */ public boolean has(String dataprovider) { return has(dataprovider, this); } public boolean has(int index, Scriptable start) { return false; } public boolean has(String name, Scriptable start) { if (name == null) return false; if ("foundset".equals(name) || "exception".equals(name) || jsFunctions.containsKey(name)) return true; //$NON-NLS-1$ //$NON-NLS-2$ if (FoundSet.isToplevelKeyword(name)) return false; // TODO test for aggregates?? int columnIndex = parent.getSQLSheet().getColumnIndex(name); if (columnIndex >= 0) { return true; } boolean b = (row == null ? false : row.containsCalculation(name)); int index = 0; if (!b && (index = name.indexOf('.')) != -1) { String partName = name.substring(0, index); String restName = name.substring(index + 1); IFoundSetInternal foundSet = getRelatedFoundSet(partName);//check substate, will return null if not found if (foundSet != null) { //related data int selected = foundSet.getSelectedIndex(); // in printing selected row will be -1, but aggregates should still go through record 0 if (selected == -1 && foundSet.getSize() > 0) { selected = 0; } IRecordInternal state = foundSet.getRecord(selected); if (state != null) { return ((Scriptable)state).has(restName, start); } } } return b; } public boolean hasInstance(Scriptable instance) { return false; } public void put(int index, Scriptable start, Object value) { // ignore } public void put(String name, Scriptable start, final Object value) { try { if ("foundset".equals(name) || "exception".equals(name)) return; //$NON-NLS-1$ //$NON-NLS-2$ if (jsFunctions.containsKey(name)) return;//dont allow to set Object realValue = value; if (realValue instanceof IDelegate< ? >) { realValue = ((IDelegate< ? >)realValue).getDelegate(); if (realValue instanceof IDataSet) { IDataSet set = (IDataSet)realValue; StringBuilder sb = new StringBuilder(); sb.append('\n'); for (int i = 0; i < set.getRowCount(); i++) { sb.append(set.getRow(i)[0]); sb.append('\n'); } realValue = sb.toString(); } } else if (realValue instanceof FoundSet) { StringBuilder sb = new StringBuilder(); sb.append('\n'); FoundSet fs = (FoundSet)realValue; for (int i = 0; i < fs.getSize(); i++) { IRecordInternal record = fs.getRecord(i); sb.append(record.getPKHashKey()); sb.append('\n'); } realValue = sb.toString(); } else { Object tmp = realValue; while (tmp instanceof Wrapper) { tmp = ((Wrapper)tmp).unwrap(); if (tmp == realValue) { break; } } realValue = tmp; } boolean dbColumn = parent.getSQLSheet().getColumnIndex(name) != -1; if (!dbColumn || startEditing()) //make sure any js change is noted { if (realValue instanceof Date) { //make copy so then when it is further used in js it won't change this one. realValue = new Date(((Date)realValue).getTime()); //make copy so changes are seen (date is mutable and whould bypass equals) } Object oldVal = setValue(name, realValue); if (oldVal != realValue)//did change? { fireJSModificationEvent(name, realValue); } } else { ((FoundSetManager)parent.getFoundSetManager()).getApplication().handleException(null, new ApplicationException(ServoyException.RECORD_LOCKED)); } } catch (RuntimeException e) { throw new WrappedException(e); } } public void setParentScope(Scriptable parent) { this.parentScope = parent; } public void setPrototype(Scriptable prototype) { this.prototypeScope = prototype; } /* * _____________________________________________________________ Related states impementation */ /** * Get related foundset, relationName may be multiple-levels deep. * When defaultSortColumns is null, the sort from the relation will be used */ public IFoundSetInternal getRelatedFoundSet(String relationName, List<SortColumn> defaultSortColumns) { if (relationName == null || parent == null) return null; try { if (relationName.indexOf('.') < 0) { // one level deep relation name Relation relation = parent.getFoundSetManager().getApplication().getFlattenedSolution().getRelation(relationName); if (relation != null && relation.isGlobal())//only do handle global relations { return parent.getFoundSetManager().getGlobalRelatedFoundSet(relationName); } } // when relationName is multiple levels deep or a fk(null)->pk relation then the relation doesn't have to be there yet. IFoundSetInternal sub = parent.getRelatedFoundSet(this, relationName, defaultSortColumns); synchronized (relatedFoundSets) { if (sub != null) { relatedFoundSets.put(relationName, new SoftReference<IFoundSetInternal>(sub)); } else { // relation was asked for but didn't return anything, don't ask for it again. (see isRelatedFoundsetLoaded()) relatedFoundSets.put(relationName, null); } return sub; } } catch (ServoyException ex) { parent.getFoundSetManager().getApplication().reportError(parent.getFoundSetManager().getApplication().getI18NMessage("servoy.relation.error"), ex); //$NON-NLS-1$ return null; } } /** * Get related foundset, relationName may be multiple-levels deep */ public IFoundSetInternal getRelatedFoundSet(String relationName) { // foundsetManager will add sort from relation if configured, otherwise from the related sheet return getRelatedFoundSet(relationName, null); } public boolean isRelatedFoundSetLoaded(String relationName, String restName) { if (relatedFoundSets.size() > 0) { SoftReference<IFoundSetInternal> sr = null; synchronized (relatedFoundSets) { if (relatedFoundSets.containsKey(relationName)) { sr = relatedFoundSets.get(relationName); if (sr == null) { // special case, relation was ask for but didn't return anything, // see getRelatedFoundSet() return true; } } } if (sr != null) { IFoundSetInternal fs = sr.get(); if (fs instanceof RelatedFoundSet) { RelatedFoundSet rfs = (RelatedFoundSet)fs; return !rfs.mustQueryForUpdates() && !(rfs.mustAggregatesBeLoaded() && fs.getSQLSheet().containsAggregate(restName)); } } } return ((FoundSetManager)parent.getFoundSetManager()).isRelatedFoundSetLoaded(this, relationName); } public Row getRawData() { return row; } public String getAsTabSeparated() { StringBuilder retval = new StringBuilder(); SQLSheet.SQLDescription desc = parent.getSQLSheet().getSQLDescription(SQLSheet.SELECT); Iterator<String> it = desc.getDataProviderIDsDilivery().iterator(); while (it.hasNext()) { String pd = it.next(); Object obj = getValue(pd); if (obj != null) { retval.append(obj.toString()); } if (it.hasNext()) retval.append('\t'); } return retval.toString(); } /** * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (obj instanceof Record) { Record rec = (Record)obj; return row == rec.row && parent == rec.parent; } return false; } @Override public String toString() { if (parent == null) return super.toString(); String id = parent.getRecordToStringDataProviderID(); if (id != null) { Object s = getValue(id); if (s == null) s = ""; //$NON-NLS-1$ return s.toString(); } StringBuilder sb = new StringBuilder(); sb.append("Record[DATA:"); //$NON-NLS-1$ sb.append(row); sb.append(']'); sb.append(" COLUMS: "); //$NON-NLS-1$ Object[] objects = getIds(); for (Object element : objects) { sb.append(element); sb.append(','); } return sb.toString(); } public String getPKHashKey() { return row.getPKHashKey(); } public Object[] getPK() { if (row != null) { // the pks might be generated by calculations with the same name; in this case, we must // perform the calculations and store them in the pks (with getValue()) before returning them String[] pks = parent.getSQLSheet().getPKColumnDataProvidersAsArray(); for (String element : pks) { if (row.containsCalculation(element)) { getValue(element); // calculate the pk value and store it in the row - let's hope the user knows what he is doing // because if he always returns other values for pks it will be chaos (+ caches out of synch) // parent.updatePk(Record state) could be made public and used here for this, but it is not normal // for pks to change the value all the time - so no use to add it and lower performance } } return row.getPK(); } return null; } /** * @see com.servoy.j2db.dataprocessing.IRowChangeListener#notifyChange(com.servoy.j2db.scripting.ModificationEvent) */ public void notifyChange(ModificationEvent e, FireCollector collector)//this method is only called if I'm not the source of the event { fireJSModificationEvent(e); collector.put(this.getParentFoundSet(), this); } protected void manageCalcDependency(String calc, UsedDataProviderTracker usedDataProviderTracker) { if (usedDataProviderTracker == null) { return; } Set<String> usedGlobals = usedDataProviderTracker.getGlobals(); if (usedGlobals != null) { for (String usedGlobal : usedGlobals) { row.getRowManager().addCalculationGlobalDependency(usedGlobal, calc); } } Set<UsedDataProvider> usedColumns = usedDataProviderTracker.getColumns(); if (usedColumns != null) { for (UsedDataProvider usedColumn : usedColumns) { try { RowManager rowManager = ((FoundSetManager)parent.getFoundSetManager()).getRowManager(usedColumn.dataSource); if (rowManager != null) { rowManager.addCalculationDependency(usedColumn.pkHashKey, usedColumn.dataProviderId, parent.getDataSource(), getPKHashKey(), calc); } } catch (ServoyException e) { Debug.error(e); } } } Set<UsedRelation> uedRelations = usedDataProviderTracker.getRelations(); if (uedRelations != null) { for (UsedRelation usedRelation : uedRelations) { row.getRowManager().addCalculationRelationDependency(usedRelation.whereArgsHash, usedRelation.name, parent.getDataSource(), getPKHashKey(), calc); } } Set<UsedAggregate> usedAggregates = usedDataProviderTracker.getAggregates(); if (usedAggregates != null) { for (UsedAggregate usedAggregate : usedAggregates) { row.getRowManager().addCalculationAggregateDependency(usedAggregate.foundSet, usedAggregate.name, calc); } } } /** * Returns true or false if the record is being edited or not. * * @sample * var isEditing = foundset.getSelectedRecord().isEditing() // also foundset.getRecord can be used * * @return a boolean when in edit. */ @JSFunction public boolean isEditing() { return (parent != null ? parent.getFoundSetManager().getEditRecordList().isEditing(this) : false); } /** * Returns true if the current record is a new record or false otherwise. * * @sample * var isNew = foundset.getSelectedRecord().isNew(); * * @return true if the current record is a new record, false otherwise; */ @JSFunction public boolean isNew() { return getRawData() != null && !existInDataSource(); } /** * Returns an array with the primary key values of the record. * * @sample * var pks = foundset.getSelectedRecord().getPKs() // also foundset.getRecord can be used * * @return an Array with the pk values. */ @JSFunction public Object[] getPKs() { Object[] pk = getPK(); if (pk != null) { int[] pkpos = parent.getSQLSheet().getPKIndexes(); for (int i = 0; i < pk.length; i++) { pk[i] = parent.getSQLSheet().convertValueToObject(pk[i], pkpos[i], parent.getFoundSetManager().getColumnConverterManager()); } } return pk; } /** * Delete this record from the Foundset and the underlying datasource. * * @deprecated Use foundset.deleteRecord(record) * * @sample * var record= %%prefix%%foundset.getRecord(index); * record.deleteRecord(); */ @Deprecated public void js_deleteRecord() { try { getParentFoundSet().deleteRecord(this); } catch (ServoyException e) { throw new RuntimeException(e); } } /** * If this record exists in underlying datasource it will do a re-query to fetch the latest data from the datasource. * NOTE: If you use transactions then it will be the data of your last update of this record in the transaction, * not the latest committed data of that record in the datasource. * * @deprecated As of release 6.1, replaced by {@link #revertChanges()}. Note that revertChanges does in memory revert of outstanding changes, does not query the database. * * @sample * var record= %%prefix%%foundset.getSelectedRecord(); * record.rollbackChanges(); */ @Deprecated public void js_rollbackChanges() { try { getRawData().rollbackFromDB(); getRawData().setLastException(null); } catch (Exception e) { throw new RuntimeException(e); } } /** * Reverts the in memory outstanding (not saved) changes of the record. * * * @sample * var record= %%prefix%%foundset.getSelectedRecord(); * record.revertChanges(); */ @JSFunction public void revertChanges() { try { List<IRecordInternal> records = new ArrayList<IRecordInternal>(); records.add(this); getParentFoundSet().getFoundSetManager().getEditRecordList().rollbackRecords(records); } catch (Exception e) { throw new RuntimeException(e); } } /** * Saves this record to the datasource if it had changes. * * @deprecated Use databasemanager.saveData(record) * * @sample * var record= %%prefix%%foundset.getSelectedRecord(); * record.save(); * * @return true if the save was done without an error. */ @Deprecated public boolean js_save() { try { return getParentFoundSet().getFoundSetManager().getEditRecordList().stopEditing(true, this) == ISaveConstants.STOPPED; } catch (Exception e) { throw new RuntimeException(e); } } /** * Returns a JSDataSet with outstanding (not saved) changed data of this record. * column1 is the column name, colum2 is the old data and column3 is the new data. * * NOTE: To return an array of records with outstanding changed data, see the function databaseManager.getEditedRecords(). * * @sample * /** @type {JSDataSet} */ * var dataset = record.getChangedData() * for( var i = 1 ; i <= dataset.getMaxRowIndex() ; i++ ) * { * application.output(dataset.getValue(i,1) +' '+ dataset.getValue(i,2) +' '+ dataset.getValue(i,3)); * } * * @return a JSDataSet with the changed data of this record. */ @JSFunction public IJSDataSet getChangedData() { if (getParentFoundSet() != null && getRawData() != null) { String[] cnames = getParentFoundSet().getSQLSheet().getColumnNames(); Object[] oldd = getRawData().getRawOldColumnData(); List<Object[]> rows = new ArrayList<Object[]>(); if (oldd != null || !getRawData().existInDB()) { Object[] newd = getRawData().getRawColumnData(); for (int i = 0; i < cnames.length; i++) { Object oldv = (oldd == null ? null : oldd[i]); if (!Utils.equalObjects(oldv, newd[i])) rows.add(new Object[] { cnames[i], oldv, newd[i] }); } } return new JSDataSet(getParentFoundSet().getFoundSetManager().getApplication(), new BufferedDataSet( new String[] { "col_name", "old_value", "new_value" }, rows)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } return null; } /** * Returns true if the current record has outstanding/changed data. * * @sample * var hasChanged = record.hasChangedData(); * * @return true if the current record has outstanding/changed data. */ @JSFunction public boolean hasChangedData() { return getRawData() != null && getRawData().isChanged(); } /** * Returns last occurred exception on this record (or null). * * @sample * var exception = record.exception; * * @return The occurred exception. */ @JSReadonlyProperty public Exception getException() { return row.getLastException(); } /** * Returns the records datasource string. * * @sample * var ds = record.getDataSource(); * * @return The datasource string of this record. */ @JSFunction public String getDataSource() { return parent.getDataSource(); } /** * Returns parent foundset of the record. * * @sample * var parent = record.foundset; * * @return The parent foundset of the record. */ @JSReadonlyProperty public IJSFoundSet getFoundset() { return (IJSFoundSet)parent; } public void rowRemoved() { if (getParentFoundSet().getRecordIndex(this) != -1) { try { getParentFoundSet().deleteRecord(this); } catch (ServoyException e) { throw new RuntimeException(e); } } } }