/*
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.server.headlessclient.dataui;
import java.util.HashMap;
import java.util.Map;
import org.apache.wicket.Component;
import org.apache.wicket.Session;
import org.apache.wicket.model.AbstractWrapModel;
import org.apache.wicket.model.IComponentInheritedModel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.IWrapModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.mozilla.javascript.Scriptable;
import com.servoy.base.util.ITagResolver;
import com.servoy.j2db.ApplicationException;
import com.servoy.j2db.component.ComponentFormat;
import com.servoy.j2db.dataprocessing.DataAdapterList;
import com.servoy.j2db.dataprocessing.FindState;
import com.servoy.j2db.dataprocessing.IDisplay;
import com.servoy.j2db.dataprocessing.IDisplayData;
import com.servoy.j2db.dataprocessing.IRecord;
import com.servoy.j2db.dataprocessing.IRecordInternal;
import com.servoy.j2db.dataprocessing.TagResolver;
import com.servoy.j2db.dataprocessing.ValueFactory.DbIdentValue;
import com.servoy.j2db.scripting.FormScope;
import com.servoy.j2db.scripting.IScriptableProvider;
import com.servoy.j2db.server.headlessclient.WebClientSession;
import com.servoy.j2db.server.headlessclient.WebForm;
import com.servoy.j2db.ui.scripting.IFormatScriptComponent;
import com.servoy.j2db.util.Debug;
import com.servoy.j2db.util.IDelegate;
import com.servoy.j2db.util.Pair;
import com.servoy.j2db.util.ScopesUtils;
import com.servoy.j2db.util.ServoyException;
import com.servoy.j2db.util.Text;
import com.servoy.j2db.util.Utils;
/**
* A model that holds 1 {@link IRecord}.
*
* @author jcompagner
*/
public abstract class RecordItemModel extends LoadableDetachableModel implements IComponentInheritedModel
{
private static final Object NONE = new Object();
private final Map<Component, Object> lastRenderedValues = new HashMap<Component, Object>();
public Object lastInvalidValue = NONE;
/**
* @see wicket.model.IModel#getNestedModel()
*/
public IModel getNestedModel()
{
return null;
}
/**
* @see wicket.model.LoadableDetachableModel#load()
*/
@Override
protected Object load()
{
IRecordInternal rec = getRecord();
if (rec == null)
{
Debug.trace("no record found", new RuntimeException()); //$NON-NLS-1$
}
return rec;
}
protected abstract IRecordInternal getRecord();
public IWrapModel wrapOnInheritance(Component component)
{
return new WrapModel(component);
}
class WrapModel extends AbstractWrapModel implements ITagResolver
{
private static final long serialVersionUID = 1L;
private final Component component;
WrapModel(Component component)
{
this.component = component;
if (component instanceof IDisplayData) ((IDisplayData)component).setTagResolver(this);
}
/**
* @see wicket.model.IWrapModel#getNestedModel()
*/
public IModel getWrappedModel()
{
return RecordItemModel.this;
}
@Override
public void detach()
{
RecordItemModel.this.detach();
}
/**
* @see wicket.model.IModel#getObject()
*/
@Override
public Object getObject()
{
if (component instanceof WebRect)
{
// special check
return ""; //$NON-NLS-1$
}
if ((component instanceof IDisplayData) && !((IDisplayData)component).isValueValid() && lastInvalidValue != NONE)
{
return lastInvalidValue;
}
String dataProviderID = getDataProviderID(component);
if (dataProviderID == null) return null;
Object value = getValue(component, dataProviderID);
if (((IDisplayData)component).needEntireState())
{
if (value instanceof String)
{
value = Text.processTags((String)value, this);
}
// Tooltip should also be pulled! (component.getTooltip())
// if (tooltip != null)
// {
// setToolTipText(Text.processTags(tooltip, resolver));
// }
}
if (component instanceof IResolveObject)
{
value = ((IResolveObject)component).resolveDisplayValue(value);
}
return value;
}
@Override
public void setObject(Object obj)
{
if (component instanceof IDisplay)
{
// ignore fields that are read only or disabled
IDisplay display = (IDisplay)component;
if (display.isReadOnly() || !display.isEnabled()) return;
}
if (component instanceof IDisplayData)
{
((IDisplayData)component).setTagResolver(this);
}
String dataProviderID = getDataProviderID(component);
if (dataProviderID == null) return;
if (!((IDisplayData)component).isValueValid() || !Utils.equalObjects(lastRenderedValues.get(component), obj))
{
lastRenderedValues.put(component, obj);
// this is normally called as a result of a change in the browser (so component in browser shows this value already); if this is called manually from server side code, setChanged() might also be needed on that component separately when it needs to be rendered back to the browser;
// this is needed not to interfere with components that use lots of JS like type-aheads when field contents change;
// if the field uses a formatter for example that would display the value different then it parsed it, setChanged() should be manually called (see FormatConverter use of StateFullSimpleDateFormat)
setValue(component, dataProviderID, obj);
}
}
public String getStringValue(String name)
{
IRecordInternal currentRecord = (IRecordInternal)RecordItemModel.this.getObject();
WebForm webForm = component.findParent(WebForm.class);
if (webForm != null)
{
FormScope fs = webForm.getController().getFormScope();
Object value = DataAdapterList.getValueObject(currentRecord, fs, name);
String stringValue = TagResolver.formatObject(value, webForm.getController().getApplication().getLocale(),
webForm.getController().getApplication().getSettings());
return DataAdapterList.processValue(
stringValue,
name,
webForm.getController().getApplication().getFlattenedSolution().getDataproviderLookup(
webForm.getController().getApplication().getFoundSetManager(), webForm.getController().getForm()));
}
return null;
}
}
private String getDataProviderID(final Component component)
{
String dataProviderID = null;
if (component instanceof IDisplayData)
{
dataProviderID = ((IDisplayData)component).getDataProviderID();
}
return dataProviderID;
}
public void updateRenderedValue(Component comp)
{
lastRenderedValues.put(comp, comp.getDefaultModelObject());
}
public Object getLastRenderedValue(Component comp)
{
Object lrv = lastRenderedValues.get(comp);
if (lrv == null && comp instanceof IDelegate< ? >) // for example WebDataCalendar actually stores it's last-rendered-value through it's child WebDataField, so it should get it that way as well
{
Object dlg = ((IDelegate< ? >)comp).getDelegate();
if (dlg instanceof Component && dlg != comp) lrv = getLastRenderedValue((Component)dlg);
}
return lrv;
}
/**
* @param obj
* @param dataProviderID
* @param prevValue
*/
public void setValue(Component component, String dataProviderID, Object value)
{
Object obj = value;
String compDpid = getDataProviderID(component);
boolean ownComponentsValue = compDpid != null && dataProviderID.endsWith(compDpid);
Object prevValue = null;
if (ownComponentsValue && component instanceof IResolveObject)
{
obj = ((IResolveObject)component).resolveRealValue(obj);
}
if (component instanceof IDisplayData)
{
obj = Utils.removeJavascripLinkFromDisplay((IDisplayData)component, new Object[] { obj });
}
WebForm webForm = component.findParent(WebForm.class);
IRecordInternal record = (IRecordInternal)RecordItemModel.this.getObject();
// use UI converter to convert from UI value to record value
if (!(record instanceof FindState))
{
obj = ComponentFormat.applyUIConverterFromObject(component, obj, dataProviderID, webForm.getController().getApplication().getFoundSetManager());
}
FormScope fs = webForm.getController().getFormScope();
try
{
Pair<String, String> scope = ScopesUtils.getVariableScope(dataProviderID);
if (scope.getLeft() != null)
{
if (record == null)
{
webForm.getController().getApplication().getScriptEngine().getSolutionScope().getScopesScope().getGlobalScope(scope.getLeft()).put(
scope.getRight(), obj);
}
else
{
//does an additional fire in foundset!
prevValue = record.getParentFoundSet().setDataProviderValue(dataProviderID, obj);
}
}
else if (fs.has(dataProviderID, fs))
{
prevValue = fs.get(dataProviderID);
fs.put(dataProviderID, obj);
}
else
{
if (record != null && record.startEditing())
{
try
{
prevValue = record.getValue(dataProviderID);
record.setValue(dataProviderID, obj);
}
catch (IllegalArgumentException e)
{
Debug.trace(e);
((WebClientSession)Session.get()).getWebClient().handleException(null, new ApplicationException(ServoyException.INVALID_INPUT, e));
Object stateValue = record.getValue(dataProviderID);
if (!Utils.equalObjects(prevValue, stateValue))
{
// reset display to changed value in validator method
obj = stateValue;
}
if (ownComponentsValue)
{
((IDisplayData)component).setValueValid(false, prevValue);
}
return;
}
if (ownComponentsValue && record instanceof FindState && component instanceof IScriptableProvider &&
((IScriptableProvider)component).getScriptObject() instanceof IFormatScriptComponent &&
((IFormatScriptComponent)((IScriptableProvider)component).getScriptObject()).getComponentFormat() != null)
{
((FindState)record).setFormat(dataProviderID,
((IFormatScriptComponent)((IScriptableProvider)component).getScriptObject()).getComponentFormat().parsedFormat);
}
}
}
// this can be called on another dataprovider id then the component (media upload)
// then dont call notify
if (ownComponentsValue)
{
((IDisplayData)component).notifyLastNewValueWasChange(prevValue, obj);
}
}
finally
{
// this can be called on another dataprovider id then the component (media upload)
// then touch the lastInvalidValue
if (ownComponentsValue)
{
if (((IDisplayData)component).isValueValid())
{
lastInvalidValue = NONE;
}
else
{
lastInvalidValue = obj;
}
}
}
return;
}
/**
* Gets the value for dataProviderID in the context of this record and the form determined using given component. The component is only used for getting the
* form, otherwise it does not affect the returned value.
*
* @param component the component used to determine the form (in case of global or form variables).
* @param dataProviderID the data provider id pointing to a data provider in the record, a form or a global variable.
* @return the value.
*/
public Object getValue(Component component, String dataProviderID)
{
Object value = null;
WebForm webForm = component.findParent(WebForm.class);
if (webForm == null)
{
// component.toString() may cause stackoverflow here
Debug.error("Component " + component.getClass() + " with dp: " + dataProviderID + " already removed from its parent form.", new RuntimeException()); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
return null;
}
FormScope fs = webForm.getController().getFormScope();
IRecord record = (IRecord)RecordItemModel.this.getObject();
if (ScopesUtils.isVariableScope(dataProviderID))
{
value = webForm.getController().getApplication().getScriptEngine().getSolutionScope().getScopesScope().get(null, dataProviderID);
}
else if (record != null)
{
value = record.getValue(dataProviderID);
}
if (value == Scriptable.NOT_FOUND && fs != null && fs.has(dataProviderID, fs))
{
value = fs.get(dataProviderID);
}
if (value instanceof DbIdentValue)
{
value = ((DbIdentValue)value).getPkValue();
}
if (value == Scriptable.NOT_FOUND)
{
value = null;
}
else if (!(record instanceof FindState))
{
// use UI converter to convert from record value to UI value
value = ComponentFormat.applyUIConverterToObject(component, value, dataProviderID, webForm.getController().getApplication().getFoundSetManager());
}
return value;
}
}