/*
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.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import org.mozilla.javascript.Scriptable;
import com.servoy.j2db.ApplicationException;
import com.servoy.j2db.IApplication;
import com.servoy.j2db.ISmartClientApplication;
import com.servoy.j2db.component.ComponentFormat;
import com.servoy.j2db.component.INullableAware;
import com.servoy.j2db.dataprocessing.ValueFactory.DbIdentValue;
import com.servoy.j2db.persistence.IColumnTypes;
import com.servoy.j2db.persistence.Relation;
import com.servoy.j2db.scripting.GlobalScope;
import com.servoy.j2db.scripting.IScriptable;
import com.servoy.j2db.scripting.IScriptableProvider;
import com.servoy.j2db.ui.IFieldComponent;
import com.servoy.j2db.ui.ISupportOnRender;
import com.servoy.j2db.ui.ISupportOnRenderCallback;
import com.servoy.j2db.ui.scripting.AbstractRuntimeRendersupportComponent;
import com.servoy.j2db.ui.scripting.IFormatScriptComponent;
import com.servoy.j2db.util.Debug;
import com.servoy.j2db.util.IDestroyable;
import com.servoy.j2db.util.Pair;
import com.servoy.j2db.util.ScopesUtils;
import com.servoy.j2db.util.ServoyException;
import com.servoy.j2db.util.Utils;
/**
* This adapter is a kind of model between the display(s) and the state.
*
* @author jblok
*/
public class DisplaysAdapter implements IDataAdapter, IEditListener, TableModelListener, IDestroyable, ListSelectionListener
{
private boolean adjusting;
//holds list of all displays
private final List<IDisplayData> displays;
//the state (=model) where to get/set the data
private IRecordInternal record;
private List<IRecordInternal> relatedData;// null when not related, list of currently listening records when related
//representing dataprovider
private final String dataProviderID;
private final IApplication application;
private final DataAdapterList dal;
DisplaysAdapter(IApplication app, DataAdapterList dal, String dataProviderID, IDisplayData display)
{
application = app;
this.dal = dal;
displays = new ArrayList<IDisplayData>(1);//normally one, this is first display
addDisplay(display);
this.dataProviderID = dataProviderID;
if (dataProviderID == null || ScopesUtils.isVariableScope(dataProviderID) || dataProviderID.indexOf('.') < 0)
{
relatedData = null; // not related
}
else
{
relatedData = Collections.emptyList();
}
}
/**
* Push new state in this data adapter
*
* @param state the state to work with.
*/
public void setRecord(IRecordInternal state)
{
this.record = state;
Object obj = null;
if (dataProviderID != null)
{
Pair<String, String> scope = ScopesUtils.getVariableScope(dataProviderID);
if (scope.getLeft() != null)
{
GlobalScope gs = application.getScriptEngine().getScopesScope().getGlobalScope(scope.getLeft());
obj = gs == null ? null : gs.get(scope.getRight());
}
else if (relatedData != null)
{
if (state != null) obj = state.getValue(dataProviderID);
}
else if (dal.getFormScope() != null /* design component */ && dal.getFormScope().has(dataProviderID, dal.getFormScope()))
{
obj = dal.getFormScope().get(dataProviderID);
}
else if (state != null)
{
obj = state.getValue(dataProviderID);
}
// if display value is null but is for count/avg/sum aggregate set it to 0, as
// it means that the foundset has no records, so count/avg/sum is 0;
if (obj == null && dal.isCountOrAvgOrSumAggregateDataProvider(this)) obj = Integer.valueOf(0);
}
if (obj == Scriptable.NOT_FOUND)
{
obj = null;
}
setValueToDisplays(obj);
}
public void deregister()
{
if (relatedData != null)
{
for (IRecordInternal rec : relatedData)
{
rec.removeModificationListener(this);
((ISwingFoundSet)rec.getParentFoundSet()).removeTableModelListener(this);
((ISwingFoundSet)rec.getParentFoundSet()).getSelectionModel().removeListSelectionListener(this);
}
}
}
private void reregister()
{
if (record == null)
{
return;
}
// get the new records were are depending on
IRecordInternal currRecord = record;
String[] parts = dataProviderID.split("\\."); //$NON-NLS-1$
// similar code as the loop below is also in class DataproviderTypeSabloValue - just in case future fixes need to apply to both places
List<IRecordInternal> newRelated = new ArrayList<IRecordInternal>(parts.length - 1);
for (int i = 0; currRecord != null && i < parts.length - 1; i++)
{
Object value = currRecord.getValue(parts[i]);
if (value instanceof ISwingFoundSet)
{
currRecord = ((ISwingFoundSet)value).getRecord(((ISwingFoundSet)value).getSelectedIndex());
if (currRecord == null) currRecord = ((ISwingFoundSet)value).getPrototypeState();
newRelated.add(currRecord);
}
else
{
currRecord = null;
}
}
if (!newRelated.equals(relatedData))
{
deregister();
relatedData = newRelated;
// register
for (IRecordInternal rec : relatedData)
{
rec.addModificationListener(this);
((ISwingFoundSet)rec.getParentFoundSet()).addTableModelListener(this);
((ISwingFoundSet)rec.getParentFoundSet()).getSelectionModel().addListSelectionListener(this);
}
}
}
//inform all displays about a change
private void setValueToDisplays(Object obj)
{
if (relatedData != null)
{
reregister();
}
Object val = obj;
if (val instanceof DbIdentValue)
{
val = ((DbIdentValue)val).getPkValue();
}
for (IDisplayData display : displays)
{
Object value = null;
if (display.needEntireState())
{
display.setTagResolver(dal);
if (display.getDataProviderID() != null)
{
value = dal.getValueObject(record, display.getDataProviderID());
}
}
else
{
value = val;
}
if (!findMode)
{
// use UI converter to convert from record value to UI value
value = ComponentFormat.applyUIConverterToObject(display, value, dataProviderID, application.getFoundSetManager());
}
display.setValueObject(value);
// when the data-provider for this check box is a non-null integer column,
// we must force it to take the value 0 (so it can be saved in the database);
// in some cases we do not have a editProvider (table view - renderer component)
// and we use the explicitly set "record" to commit the changed value;
// similar code exists for web client check boxes
if (!findMode && value == null && display instanceof INullableAware && !((INullableAware)display).getAllowNull() &&
display instanceof IFieldComponent && ((IFieldComponent)display).getScriptObject() instanceof IFormatScriptComponent &&
((IFormatScriptComponent)((IFieldComponent)display).getScriptObject()).getComponentFormat() != null &&
((IFormatScriptComponent)((IFieldComponent)display).getScriptObject()).getComponentFormat().dpType == IColumnTypes.INTEGER &&
display.getDataProviderID() != null && record != null && record.startEditing() &&
!(record instanceof PrototypeState && !ScopesUtils.isVariableScope(display.getDataProviderID()))) // ignore PrototypeState if not global
{
// NOTE: when a UI converter is defined, the converter should handle this
if (((IFormatScriptComponent)((IFieldComponent)display).getScriptObject()).getComponentFormat().parsedFormat.getUIConverterName() == null)
{
record.setValue(display.getDataProviderID(), Integer.valueOf(0));
}
}
}
}
/**
* focus listener implementation, notifies the state that is started editing
*/
public void startEdit(IDisplayData display)
{
if (record != null)
{
startEdit(dal, display, record);
}
}
public static void startEdit(DataAdapterList dal, IDisplay display, IRecordInternal state)
{
final IApplication application = dal.getApplication();
dal.setCurrentDisplay(display);
boolean isGlobal = false;
boolean isColumn = true;
if (display instanceof IDisplayData)
{
String dataProviderID = ((IDisplayData)display).getDataProviderID();
isGlobal = dataProviderID != null && ScopesUtils.isVariableScope(dataProviderID);
if (!isGlobal && dataProviderID != null)
{
String[] parts = dataProviderID.split("\\."); //$NON-NLS-1$
IRecordInternal currState = state;
for (int i = 0; i < parts.length - 1; i++)
{
IFoundSetInternal foundset = currState.getRelatedFoundSet(parts[i]);
if (foundset == null)
{
break;
}
Relation r = application.getFoundSetManager().getApplication().getFlattenedSolution().getRelation(parts[i]);
currState = foundset.getRecord(foundset.getSelectedIndex());
if (currState == null)
{
if (r != null && r.getAllowCreationRelatedRecords())
{
try
{
currState = foundset.getRecord(foundset.newRecord(0, true));
}
catch (ServoyException se)
{
application.reportError(se.getLocalizedMessage(), se);
}
}
else
{
final ApplicationException ae = new ApplicationException(ServoyException.NO_RELATED_CREATE_ACCESS, new Object[] { parts[i] });
// unfocus the current field, otherwise when the dialog is closed focus is set back to this field and the same error recurs ad infinitum.
application.looseFocus();
application.invokeLater(new Runnable()
{
public void run()
{
application.handleException(null, ae); // ApplicationException knows how to translate this null into an i18n message
}
});
}
}
if (currState == null) return;
}
isColumn = currState.getParentFoundSet().getSQLSheet().getColumnIndex(parts[parts.length - 1]) != -1;
}
}
if (isGlobal || !isColumn || state.startEditing()) //globals are always allowed to set in datarenderers
{
//bit ugly should use property event here
if (application instanceof ISmartClientApplication) ((ISmartClientApplication)application).updateInsertModeIcon(display);
}
else
{
//loose focus first
//don't transfer focus to menu bar.. (macosx)
//application.getMainApplicationFrame().getJMenuBar().requestFocus();
application.looseFocus();
application.reportWarningInStatus(application.getI18NMessage("servoy.foundSet.error.noModifyAccess")); //$NON-NLS-1$
}
}
/**
* focus listener implementation, set value to state and possible other displays, and notify other datalisteners about changes
*/
public void commitEdit(IDisplayData display)
{
if (dataProviderID == null) return;
Object obj = Utils.removeJavascripLinkFromDisplay(display, null);
Object prevValue = null;
boolean valueWasConverted = false;
if (!findMode)
{
// use UI converter to convert from UI value to record value
Object converted = ComponentFormat.applyUIConverterFromObject(display, obj, dataProviderID, application.getFoundSetManager());
valueWasConverted = obj != converted;
obj = converted;
}
Pair<String, String> scope = ScopesUtils.getVariableScope(dataProviderID);
if (scope.getLeft() != null)
{
adjusting = true;
try
{
if (record == null)
{
prevValue = application.getScriptEngine().getScopesScope().getGlobalScope(scope.getLeft()).put(scope.getRight(), obj);
}
else
{
//does an additional fire in foundset!
prevValue = record.getParentFoundSet().setDataProviderValue(dataProviderID, obj);
}
}
catch (Exception e)
{
Debug.error(e);
}
finally
{
adjusting = false;
}
}
else if (dal.getFormScope() != null && dal.getFormScope().has(dataProviderID, dal.getFormScope()))
{
prevValue = dal.getFormScope().get(dataProviderID);
dal.getFormScope().put(dataProviderID, obj);
}
else if (record != null && (record.isEditing() || record.getParentFoundSet().getSQLSheet().getColumnIndex(dataProviderID) == -1))
{
// If object == "" and previous == null don't update value
if (obj != null && obj.equals("")) //$NON-NLS-1$
{
if (record.getValue(dataProviderID) == null)
{
return;
}
}
try
{
adjusting = true;
prevValue = record.getValue(dataProviderID);
record.setValue(dataProviderID, obj);
}
catch (IllegalArgumentException e)
{
Debug.trace(e);
application.handleException(null, new ApplicationException(ServoyException.INVALID_INPUT, e));
Object stateValue = record.getValue(dataProviderID);
if (Utils.equalObjects(prevValue, stateValue))
{
// reset display to typed value
setValueToDisplays(obj);
}
else
{
// reset display to changed value in validator method
setValueToDisplays(stateValue);
}
display.setValueValid(false, prevValue);
return;
}
finally
{
adjusting = false;
}
if (record instanceof FindState)
{
if (display instanceof IScriptableProvider && ((IScriptableProvider)display).getScriptObject() instanceof IFormatScriptComponent &&
((IFormatScriptComponent)((IScriptableProvider)display).getScriptObject()).getComponentFormat() != null)
{
((FindState)record).setFormat(dataProviderID,
((IFormatScriptComponent)((IScriptableProvider)display).getScriptObject()).getComponentFormat().parsedFormat);
}
// findstate doesn't inform others...
if (!Utils.equalObjects(prevValue, obj))
{
// do call notifyLastNewValue changed so that the onChangeEvent will be fired and called when attached.
display.notifyLastNewValueWasChange(prevValue, obj);//to trigger onChangeMethod (not all displays have own property change impl)
}
prevValue = obj;
}
}
boolean changed = !Utils.equalObjects(prevValue, obj);
if (!changed)
{
// value was changed back to original value manually after an invalid input
display.setValueValid(true, null);
}
if (changed || valueWasConverted)
{
adjusting = true;
try
{
// fireDataChange to possible listeners
if (changed)
{
fireModificationEvent(obj);
display.notifyLastNewValueWasChange(prevValue, obj);//to trigger onChangeMethod (not all displays have own property change impl)
}
// check if the DataAdapterList is now destroyed (because of recreateUI in the onchange method)
// ignore the rest if it is destroyed.
if (!dal.isDestroyed())
{
// onDataChange(==notifyLastNewValueWasChange) call can have changed the value
if (dal.getFormScope() != null && dal.getFormScope().has(dataProviderID, dal.getFormScope()))
{
obj = dal.getFormScope().get(dataProviderID);
}
else if (record != null)
{
obj = record.getValue(dataProviderID);
}
setValueToDisplays(obj);// we also want to reset the value in the current display if changed by script
}
}
finally
{
adjusting = false;
}
}
}
private boolean findMode = false;
public void setFindMode(boolean b)
{
findMode = b;
for (IDisplayData display : displays)
{
// skip form variables
if (dal.getFormScope() == null || display.getDataProviderID() == null ||
dal.getFormScope().get(display.getDataProviderID()) == Scriptable.NOT_FOUND)
{
display.setValidationEnabled(!b);
}
}
}
/*
* _____________________________________________________________ DataListener
*/
private final List<IDataAdapter> listeners = new ArrayList<IDataAdapter>(3);
public void addDataListener(IDataAdapter l)
{
if (!listeners.contains(l) && l != this) listeners.add(l);
}
public void removeDataListener(IDataAdapter listener)
{
listeners.remove(listener);
}
private void fireModificationEvent(Object value)
{
ModificationEvent e = null;
if (listeners != null && listeners.size() != 0)
{
Iterator<IDataAdapter> it = listeners.iterator();
while (it.hasNext())
{
if (e == null) e = new ModificationEvent(dataProviderID, value, record);
IDataAdapter listener = it.next();
listener.displayValueChanged(e);
}
}
}
/**
* Add a display to this adapter
*
* @param display the display to add
*/
public void addDisplay(IDisplayData display)
{
displays.add(display);
}
public String getDataProviderID()
{
return dataProviderID;
}
public void displayValueChanged(ModificationEvent event)
{
valueChanged(event);
}
/*
* _____________________________________________________________ JavaScriptModificationlistener
*/
public void valueChanged(ModificationEvent e)
{
if (!adjusting)
{
try
{
adjusting = true;
Object obj = null;
boolean formVariable = false;
if (dataProviderID != null)
{
if (dal.getFormScope().has(dataProviderID, dal.getFormScope()))
{
formVariable = true;
obj = dal.getFormScope().get(dataProviderID);
}
else if (record != null)
{
obj = record.getValue(dataProviderID);
if (obj == Scriptable.NOT_FOUND)
{
obj = null;
}
// have to do this because for calcs in calcs. Better was to had a check for previous value.
fireModificationEvent(obj);
}
}
// do not set value for form variable except when really changed
if (!formVariable || dataProviderID == null || dataProviderID.equals(e.getName()))
{
setValueToDisplays(obj);
}
}
finally
{
adjusting = false;
}
}
// do fire on render on all components for record change
for (IDisplayData displayData : displays)
{
if (displayData instanceof ISupportOnRender && displayData instanceof IScriptableProvider)
{
IScriptable so = ((IScriptableProvider)displayData).getScriptObject();
if (so instanceof AbstractRuntimeRendersupportComponent && ((ISupportOnRenderCallback)so).getRenderEventExecutor().hasRenderCallback())
{
String componentDataproviderID = ((AbstractRuntimeRendersupportComponent)so).getDataProviderID();
if (e.getRecord() != null || (e.getName() != null && e.getName().equals(componentDataproviderID)))
{
((ISupportOnRender)displayData).fireOnRender(true);
}
}
}
}
}
@Override
public String toString()
{
return "DisplaysAdapter " + dataProviderID + " with " + displays.size() + " displays, hash " + hashCode(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
public void tableChanged(TableModelEvent e)
{
ISwingFoundSet source = (ISwingFoundSet)e.getSource();
if (record != null && relatedData != null && e.getFirstRow() >= source.getSelectedIndex() && source.getSelectedIndex() <= e.getLastRow())
{
setValueToDisplays(record.getValue(dataProviderID));
}
}
public void destroy()
{
deregister();
}
public void valueChanged(ListSelectionEvent e)
{
setValueToDisplays(record.getValue(dataProviderID));
}
}