/*
This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2014 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.server.ngclient.property.types;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.json.JSONException;
import org.json.JSONWriter;
import org.mozilla.javascript.Scriptable;
import org.sablo.BaseWebObject;
import org.sablo.IChangeListener;
import org.sablo.specification.PropertyDescription;
import org.sablo.specification.property.IBrowserConverterContext;
import org.sablo.specification.property.IPropertyConverterForBrowser;
import org.sablo.specification.property.IPropertyType;
import org.sablo.specification.property.types.TypesRegistry;
import org.sablo.util.ValueReference;
import org.sablo.websocket.utils.DataConversion;
import org.sablo.websocket.utils.JSONUtils;
import org.sablo.websocket.utils.JSONUtils.EmbeddableJSONWriter;
import org.sablo.websocket.utils.JSONUtils.FullValueToJSONConverter;
import org.sablo.websocket.utils.JSONUtils.IJSONStringWithConversions;
import org.sablo.websocket.utils.JSONUtils.JSONStringWithConversions;
import com.servoy.base.util.ITagResolver;
import com.servoy.j2db.FormAndTableDataProviderLookup;
import com.servoy.j2db.component.ComponentFormat;
import com.servoy.j2db.dataprocessing.FoundSetEvent;
import com.servoy.j2db.dataprocessing.IFoundSetEventListener;
import com.servoy.j2db.dataprocessing.IFoundSetInternal;
import com.servoy.j2db.dataprocessing.IModificationListener;
import com.servoy.j2db.dataprocessing.IRecordInternal;
import com.servoy.j2db.dataprocessing.ISwingFoundSet;
import com.servoy.j2db.dataprocessing.ModificationEvent;
import com.servoy.j2db.persistence.IDataProvider;
import com.servoy.j2db.persistence.IDataProviderLookup;
import com.servoy.j2db.persistence.Relation;
import com.servoy.j2db.persistence.RepositoryException;
import com.servoy.j2db.persistence.ScriptVariable;
import com.servoy.j2db.server.ngclient.DataAdapterList;
import com.servoy.j2db.server.ngclient.FormElement;
import com.servoy.j2db.server.ngclient.INGApplication;
import com.servoy.j2db.server.ngclient.IServoyDataConverterContext;
import com.servoy.j2db.server.ngclient.WebFormComponent;
import com.servoy.j2db.server.ngclient.property.DataproviderConfig;
import com.servoy.j2db.server.ngclient.property.IDataLinkedPropertyValue;
import com.servoy.j2db.server.ngclient.property.IFindModeAwarePropertyValue;
import com.servoy.j2db.server.ngclient.property.types.IDataLinkedType.TargetDataLinks;
import com.servoy.j2db.server.ngclient.utils.NGUtils;
import com.servoy.j2db.util.Debug;
import com.servoy.j2db.util.ScopesUtils;
import com.servoy.j2db.util.Text;
import com.servoy.j2db.util.UUID;
/**
* Runtime value stored in WebFormComponents for properties of type {@link DataproviderPropertyType}.
* Handles any needed listeners and deals with to and from browser communications, find mode, ....
*
* @author acostescu
*/
public class DataproviderTypeSabloValue implements IDataLinkedPropertyValue, IFindModeAwarePropertyValue
{
protected final String dataProviderID;
protected final DataAdapterList dataAdapterList;
protected final IServoyDataConverterContext servoyDataConverterContext;
private final FormElement formElement;
protected Object value;
protected Object jsonValue;
protected IChangeListener changeMonitor;
protected PropertyDescription typeOfDP;
protected ComponentFormat fieldFormat;
protected boolean findMode = false;
protected final PropertyDescription dpPD;
private TargetDataLinks dataLinks;
private Set<String> tagsDataProviders;
private boolean displaysTags;
private IFoundSetEventListener globalRelatedFoundsetListener;
private IFoundSetInternal globalRelatedFoundset;
private String globalRelationName;
private ListSelectionListener relatedFoundsetSelectionListener;
private ArrayList<IFoundSetInternal> relatedFoundsets = new ArrayList<IFoundSetInternal>();
private IModificationListener relatedRecordModificationListener;
private ArrayList<IRecordInternal> relatedRecords = new ArrayList<IRecordInternal>();
private String relationName;
public DataproviderTypeSabloValue(String dataProviderID, DataAdapterList dataAdapterList, WebFormComponent component, PropertyDescription dpPD)
{
if (dataProviderID.startsWith(ScriptVariable.GLOBALS_DOT_PREFIX))
{
this.dataProviderID = ScriptVariable.SCOPES_DOT_PREFIX + dataProviderID;
}
else
{
this.dataProviderID = dataProviderID;
}
if (dataProviderID.indexOf('.') != -1 && !ScopesUtils.isVariableScope(dataProviderID))
{
Relation relation = dataAdapterList.getApplication().getFlattenedSolution().getRelation(dataProviderID.substring(0, dataProviderID.indexOf('.')));
if (relation != null && relation.isGlobal())
{
globalRelationName = relation.getName();
globalRelatedFoundsetListener = new IFoundSetEventListener()
{
@Override
public void foundSetChanged(FoundSetEvent e)
{
if (e.getType() == FoundSetEvent.CONTENTS_CHANGED)
{
dataProviderOrRecordChanged(DataproviderTypeSabloValue.this.dataAdapterList.getRecord(), null, false, false, true);
}
}
};
}
relationName = dataProviderID.substring(0, dataProviderID.lastIndexOf('.'));
relatedFoundsetSelectionListener = new ListSelectionListener()
{
public void valueChanged(ListSelectionEvent e)
{
dataProviderOrRecordChanged(DataproviderTypeSabloValue.this.dataAdapterList.getRecord(), null, false, false, true);
}
};
relatedRecordModificationListener = new IModificationListener()
{
@Override
public void valueChanged(ModificationEvent e)
{
dataProviderOrRecordChanged(DataproviderTypeSabloValue.this.dataAdapterList.getRecord(), null, false, false, true);
}
};
}
this.dataAdapterList = dataAdapterList;
this.servoyDataConverterContext = component.getDataConverterContext();
this.dpPD = dpPD;
this.formElement = component.getFormElement();
}
protected DataproviderConfig getDataProviderConfig()
{
return (DataproviderConfig)dpPD.getConfig();
}
public String getDataProviderID()
{
return dataProviderID;
}
/**
* Returns the actual value (that is already full converted by an ui converter) that this dataProvider has.
*/
public Object getValue()
{
if (!findMode && fieldFormat != null)
{
return ComponentFormat.applyUIConverterFromObject(value, dataProviderID, servoyDataConverterContext.getApplication().getFoundSetManager(),
fieldFormat);
}
return value;
}
@Override
public void attachToBaseObject(IChangeListener changeNotifier, BaseWebObject component)
{
FormElement formElement = ((WebFormComponent)component).getFormElement();
this.changeMonitor = changeNotifier;
// register data link and find mode listeners as needed
dataLinks = (TargetDataLinks)formElement.getPreprocessedPropertyInfo(IDataLinkedType.class, dpPD);
if (dataLinks == null)
{
// they weren't cached in form element; get them again
dataLinks = ((DataproviderPropertyType)dpPD.getType()).getDataLinks(dataProviderID, dpPD, servoyDataConverterContext.getSolution(), formElement);
}
dataAdapterList.addDataLinkedProperty(this, dataLinks);
Boolean isFindModeAware = (Boolean)formElement.getPreprocessedPropertyInfo(IFindModeAwareType.class, dpPD);
if (isFindModeAware == null)
{
// they weren't cached in form element; get them again
isFindModeAware = ((DataproviderPropertyType)dpPD.getType()).isFindModeAware(dataProviderID, dpPD, servoyDataConverterContext.getSolution(),
formElement);
}
if (isFindModeAware != null && isFindModeAware.booleanValue() == true) dataAdapterList.addFindModeAwareProperty(this);
DataproviderConfig config = (DataproviderConfig)dpPD.getConfig();
String dtpn = config.getDisplayTagsPropertyName();
Object dtPropVal = null;
if (dtpn != null)
{
dtPropVal = formElement.getPropertyValue(dtpn);
if (dtPropVal == null) dtPropVal = Boolean.FALSE;
}
displaysTags = dtpn != null && ((Boolean)dtPropVal).booleanValue() == true || (dtpn == null && config.shouldDisplayTags());
dataProviderOrRecordChanged(dataAdapterList.getRecord(), null, false, false, false);
}
@Override
public void detach()
{
// unregister listeners
dataAdapterList.removeDataLinkedProperty(this);
dataAdapterList.removeFindModeAwareProperty(this);
if (globalRelatedFoundset != null)
{
globalRelatedFoundset.removeFoundSetEventListener(globalRelatedFoundsetListener);
}
globalRelatedFoundset = null;
globalRelatedFoundsetListener = null;
for (IFoundSetInternal relatedFoundset : relatedFoundsets)
{
((ISwingFoundSet)relatedFoundset).getSelectionModel().removeListSelectionListener(relatedFoundsetSelectionListener);
}
relatedFoundsetSelectionListener = null;
}
@Override
public void findModeChanged(boolean newFindMode)
{
// this normally only gets called for foundset based dataproviders (so not for global/form variables); DataproviderPropertyType.isFindModeAware(...)
if (findMode != newFindMode)
{
findMode = newFindMode;
changeMonitor.valueChanged();
}
}
@Override
public void dataProviderOrRecordChanged(final IRecordInternal record, final String dataProvider, final boolean isFormDP, final boolean isGlobalDP,
boolean fireChangeEvent)
{
// TODO can type or fieldFormat change, for example in scripting the format is reset (but type shouldn't really change)
IDataProviderLookup dpLookup = new FormAndTableDataProviderLookup(servoyDataConverterContext.getApplication().getFlattenedSolution(),
servoyDataConverterContext.getForm().getForm(), record != null ? record.getParentFoundSet().getTable() : null);
Collection<PropertyDescription> properties = formElement.getWebComponentSpec().getProperties(TypesRegistry.getType("format"));
for (PropertyDescription formatPd : properties)
{
// compare whether format and valuelist property are for same property (dataprovider) or if format is used for valuelist property itself
Object formatConfig = formatPd.getConfig();
if (formatConfig instanceof String[] && Arrays.asList((String[])formatConfig).indexOf(dpPD.getName()) != -1)
{
INGApplication application = servoyDataConverterContext.getApplication();
String format = (String)formElement.getPropertyValue(formatPd.getName());
if (format != null)
{
fieldFormat = ComponentFormat.getComponentFormat(format, dataProviderID, dpLookup, application);
break;
}
}
}
if (fieldFormat != null)
{
typeOfDP = NGUtils.getDataProviderPropertyDescription(fieldFormat.uiType, getDataProviderConfig().hasParseHtml());
}
else
{
// see type of dataprovider; this is done only once - first time we get a new record
typeOfDP = NGUtils.getDataProviderPropertyDescription(dataProviderID, servoyDataConverterContext.getApplication().getFlattenedSolution(),
servoyDataConverterContext.getForm().getForm(), record != null ? record.getParentFoundSet().getTable() : null,
getDataProviderConfig().hasParseHtml());
}
if (dpPD.hasTag("typeName"))
{
IPropertyType< ? > specType = TypesRegistry.getType((String)dpPD.getTag("typeName"));
if (specType != null && (typeOfDP == null || !specType.getClass().isAssignableFrom(typeOfDP.getClass())))
{
typeOfDP = new PropertyDescription("Spec type hint", specType);
}
}
if (globalRelationName != null)
{
try
{
IFoundSetInternal newRelatedFoundset = servoyDataConverterContext.getApplication().getFoundSetManager().getGlobalRelatedFoundSet(
globalRelationName);
if (newRelatedFoundset != globalRelatedFoundset)
{
if (globalRelatedFoundset != null)
{
globalRelatedFoundset.removeFoundSetEventListener(globalRelatedFoundsetListener);
}
globalRelatedFoundset = newRelatedFoundset;
globalRelatedFoundset.addFoundSetEventListener(globalRelatedFoundsetListener);
}
}
catch (Exception ex)
{
Debug.error(ex);
}
}
if (relatedFoundsetSelectionListener != null)
{
try
{
ArrayList<IFoundSetInternal> newRelatedFoundsets = getRelatedFoundsets(record, relationName);
if (!newRelatedFoundsets.equals(relatedFoundsets))
{
for (IFoundSetInternal relatedFoundset : relatedFoundsets)
{
((ISwingFoundSet)relatedFoundset).getSelectionModel().removeListSelectionListener(relatedFoundsetSelectionListener);
}
relatedFoundsets = newRelatedFoundsets;
for (IFoundSetInternal relatedFoundset : relatedFoundsets)
{
((ISwingFoundSet)relatedFoundset).getSelectionModel().addListSelectionListener(relatedFoundsetSelectionListener);
}
}
}
catch (Exception ex)
{
Debug.error(ex);
}
}
if (relatedRecordModificationListener != null)
{
try
{
ArrayList<IRecordInternal> newRelatedRecords = getRelatedRecords(record, relationName);
if (!newRelatedRecords.equals(relatedRecords))
{
for (IRecordInternal relatedRecord : relatedRecords)
{
relatedRecord.removeModificationListener(relatedRecordModificationListener);
}
relatedRecords = newRelatedRecords;
for (IRecordInternal relatedRecord : relatedRecords)
{
relatedRecord.addModificationListener(relatedRecordModificationListener);
}
}
}
catch (Exception ex)
{
Debug.error(ex);
}
}
String dpID = dataProviderID;
if (dpLookup != null)
{
IDataProvider dp;
try
{
dp = dpLookup.getDataProvider(dataProviderID);
if (dp != null)
{
dpID = dp.getDataProviderID();
}
}
catch (RepositoryException e)
{
Debug.error(e);
}
}
Object v = com.servoy.j2db.dataprocessing.DataAdapterList.getValueObject(record, servoyDataConverterContext.getForm().getFormScope(), dpID);
if (v == Scriptable.NOT_FOUND) v = null;
if (fieldFormat != null && !findMode)
{
v = ComponentFormat.applyUIConverterToObject(v, dataProviderID, servoyDataConverterContext.getApplication().getFoundSetManager(), fieldFormat);
}
v = replaceTagsIfNeeded(v);
if (v instanceof UUID) v = v.toString();
boolean changed = ((v != value) && (v == null || !v.equals(value)));
value = v;
if (changed)
{
jsonValue = null;
}
if (fireChangeEvent && changed) // TODO I don't get here why changeMonitor.valueChanged() shouldn't be done if fireChangeEvent is false; but kept it as it was before refactor...
{
changeMonitor.valueChanged();
}
}
private ArrayList<IFoundSetInternal> getRelatedFoundsets(IRecordInternal record, String relName)
{
ArrayList<IFoundSetInternal> returnRelatedFoundsets = new ArrayList<IFoundSetInternal>();
if (record != null)
{
StringTokenizer st = new StringTokenizer(relName, "."); //$NON-NLS-1$
String r = null;
while (st.hasMoreTokens())
{
if (r == null) r = st.nextToken();
else r = r + "." + st.nextToken(); //$NON-NLS-1$
IFoundSetInternal fs = record.getRelatedFoundSet(r);
if (fs != null)
{
returnRelatedFoundsets.add(fs);
}
}
}
return returnRelatedFoundsets;
}
private ArrayList<IRecordInternal> getRelatedRecords(IRecordInternal record, String relName)
{
// similar code as the loop below is also in class DisplaysAdapter - just in case future fixes need to apply to both places
ArrayList<IRecordInternal> returnRelatedRecords = new ArrayList<IRecordInternal>();
if (record != null)
{
// get the new records were are depending on
IRecordInternal currRecord = record;
String[] parts = relName.split("\\."); //$NON-NLS-1$
for (int i = 0; currRecord != null && i < parts.length; i++)
{
Object v = currRecord.getValue(parts[i]);
if (v instanceof ISwingFoundSet)
{
currRecord = ((ISwingFoundSet)v).getRecord(((ISwingFoundSet)v).getSelectedIndex());
if (currRecord == null) currRecord = ((ISwingFoundSet)v).getPrototypeState();
returnRelatedRecords.add(currRecord);
}
else
{
currRecord = null;
}
}
}
return returnRelatedRecords;
}
/**
* Replaces tagstrings if displaysTags is true.
* Also updates the datalinks for this property.
* @param v the value of the dataprovider
* @return
*/
private Object replaceTagsIfNeeded(Object v)
{
if (!displaysTags || !(v instanceof String)) return v;
String val = (String)v;
String result = val;
if (val.contains("%%") || val.startsWith("i18n:"))
{
final Set<String> dataProviders = new HashSet<>();
final boolean recordDP[] = new boolean[1];
result = Text.processTags(val, new ITagResolver()
{
@Override
public String getStringValue(String name)
{
String dp = name;
if (dp.startsWith(ScriptVariable.GLOBALS_DOT_PREFIX))
{
dp = ScriptVariable.SCOPES_DOT_PREFIX + dp;
}
dataProviders.add(dp);
// TODO Can't it be something special like record count or current record which are special cases and could still not depend on record...?
recordDP[0] = recordDP[0] || (!ScopesUtils.isVariableScope(dp) && dataAdapterList.getForm().getForm().getScriptVariable(dp) == null);
return dataAdapterList.getStringValue(dp);
}
});
if (result.startsWith("i18n:")) result = dataAdapterList.getApplication().getI18NMessage(result.substring(5));
if (tagsDataProviders == null || tagsDataProviders.size() != dataProviders.size() || !tagsDataProviders.containsAll(dataProviders))
{
dataAdapterList.removeDataLinkedProperty(this);
dataAdapterList.addDataLinkedProperty(this, dataLinks.concatDataLinks(dataProviders.toArray(new String[dataProviders.size()]), recordDP[0]));
tagsDataProviders = dataProviders;
}
}
else if (tagsDataProviders != null)
{
//remove links if the dataprovider value doesn't contain tags anymore
dataAdapterList.removeDataLinkedProperty(this);
dataAdapterList.addDataLinkedProperty(this, dataLinks);
tagsDataProviders = null;
}
return result;
}
public void toJSON(JSONWriter writer, String key, DataConversion clientConversion, IBrowserConverterContext dataConverterContext) throws JSONException
{
// TODO UUIDs are now just seen as strings
if (value instanceof UUID)
{
value = value.toString();
}
JSONUtils.addKeyIfPresent(writer, key);
if (jsonValue == null)
{
jsonValue = getValueForToJSON(value, dataConverterContext);
}
writer.value(jsonValue);
if (jsonValue instanceof IJSONStringWithConversions) clientConversion.convert(((IJSONStringWithConversions)jsonValue).getDataConversions());
}
protected Object getValueForToJSON(Object dpValue, IBrowserConverterContext dataConverterContext) throws JSONException
{
Object jsonValueRepresentation;
if (findMode)
{
// in UI show only strings in find mode (just like SC/WC do); if they are something else like real dates/numbers which could happen
// from scripting, then show string representation
jsonValueRepresentation = dpValue instanceof String ? dpValue : (dpValue != null ? String.valueOf(dpValue) : "");
}
else if (typeOfDP != null)
{
EmbeddableJSONWriter ejw = new EmbeddableJSONWriter(true); // that 'true' is a workaround for allowing directly a value instead of object or array
DataConversion jsonDataConversion = new DataConversion();
FullValueToJSONConverter.INSTANCE.toJSONValue(ejw, null, dpValue, typeOfDP, jsonDataConversion, dataConverterContext);
if (jsonDataConversion.getConversions().size() == 0) jsonDataConversion = null;
String str = ejw.toJSONString();
if (str == null || str.trim().length() == 0)
{
Debug.error("A dataprovider that is not able to send itself to client... (" + typeOfDP + ", " + dpValue + ")");
str = "null";
}
jsonValueRepresentation = new JSONStringWithConversions(str, jsonDataConversion);
}
else
{
jsonValueRepresentation = dpValue;
}
return jsonValueRepresentation;
}
public void browserUpdateReceived(Object newJSONValue, IBrowserConverterContext dataConverterContext)
{
Object oldValue = value;
ValueReference<Boolean> serverSideValueIsNotTheSameAsClient = new ValueReference<>(Boolean.FALSE);
if (!findMode && typeOfDP != null)
{
if (typeOfDP.getType() instanceof IPropertyConverterForBrowser< ? >)
{
value = ((IPropertyConverterForBrowser)typeOfDP.getType()).fromJSON(newJSONValue, value, typeOfDP, dataConverterContext,
serverSideValueIsNotTheSameAsClient);
}
else value = newJSONValue;
}
else value = newJSONValue;
if (oldValue != value && (oldValue == null || !oldValue.equals(value)))
{
jsonValue = null;
}
if (serverSideValueIsNotTheSameAsClient.value.booleanValue())
{
// if we detect that the new server value (it's representation on client) is no longer what the client has showing, we must update the client's value
jsonValue = null;
changeMonitor.valueChanged(); // value changed from client so why do we need this one might ask (client already has the value)?
// because for example in a field an INTEGER dataprovider might be shown with format ##0.00 and if the user enters non-int value client side
// the server will trunc/round to an INTEGER and then the client shows double value while the server DP has the int value (which are not the same)
}
}
}