/* This file belongs to the Servoy development and deployment environment, Copyright (C) 1997-2013 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.component; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.json.JSONArray; import org.json.JSONException; import org.mozilla.javascript.BaseFunction; import org.mozilla.javascript.Callable; import org.mozilla.javascript.Context; import org.mozilla.javascript.Function; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.Undefined; import org.sablo.Container; import org.sablo.WebComponent; import org.sablo.specification.PropertyDescription; import org.sablo.specification.WebObjectApiDefinition; import org.sablo.specification.WebObjectSpecification; import org.sablo.specification.property.IPropertyType; import org.sablo.specification.property.types.VisiblePropertyType; import org.sablo.websocket.CurrentWindow; import org.sablo.websocket.IWindow; import com.servoy.j2db.FormController; import com.servoy.j2db.persistence.IAnchorConstants; import com.servoy.j2db.persistence.ISupportAnchors; import com.servoy.j2db.persistence.StaticContentSpecLoader; import com.servoy.j2db.persistence.TabPanel; import com.servoy.j2db.scripting.IInstanceOf; import com.servoy.j2db.server.ngclient.ComponentFactory; import com.servoy.j2db.server.ngclient.DataAdapterList; import com.servoy.j2db.server.ngclient.FormElement; import com.servoy.j2db.server.ngclient.INGClientWindow; import com.servoy.j2db.server.ngclient.IWebFormUI; import com.servoy.j2db.server.ngclient.WebFormComponent; import com.servoy.j2db.server.ngclient.WebFormUI; import com.servoy.j2db.server.ngclient.property.types.LabelForPropertyType; import com.servoy.j2db.server.ngclient.property.types.NGConversions; import com.servoy.j2db.server.ngclient.property.types.NGConversions.IRhinoToSabloComponent; import com.servoy.j2db.server.ngclient.property.types.NGConversions.ISabloComponentToRhino; import com.servoy.j2db.server.ngclient.scripting.WebComponentFunction; import com.servoy.j2db.server.ngclient.scripting.WebServiceScriptable; import com.servoy.j2db.util.Pair; import com.servoy.j2db.util.Utils; /** * @author lvostinar * */ public class RuntimeWebComponent implements Scriptable, IInstanceOf { private final WebFormComponent component; private Scriptable prototypeScope; private final Set<String> specProperties; private final Map<String, Function> apiFunctions; private final WebObjectSpecification webComponentSpec; private Scriptable parentScope; private Scriptable scopeObject; public RuntimeWebComponent(WebFormComponent component, WebObjectSpecification webComponentSpec) { this.component = component; setParentScope(component.getDataConverterContext().getApplication().getScriptEngine().getSolutionScope()); this.specProperties = new HashSet<String>(); this.apiFunctions = new HashMap<String, Function>(); this.webComponentSpec = webComponentSpec; URL serverScript = webComponentSpec.getServerScript(); Scriptable apiObject = null; if (serverScript != null) { scopeObject = WebServiceScriptable.compileServerScript(serverScript, this, component.getDataConverterContext().getApplication()); apiObject = (Scriptable)scopeObject.get("api", scopeObject); } if (webComponentSpec != null) { for (WebObjectApiDefinition def : webComponentSpec.getApiFunctions().values()) { Function func = null; if (apiObject != null) { Object serverSideFunction = apiObject.get(def.getName(), apiObject); if (serverSideFunction instanceof Function) { func = (Function)serverSideFunction; } } if (func != null) apiFunctions.put(def.getName(), func); else apiFunctions.put(def.getName(), new WebComponentFunction(component, def)); } Map<String, PropertyDescription> specs = webComponentSpec.getProperties(); for (String propName : specs.keySet()) { if (!component.isDesignOnlyProperty(propName)) { // design properties and private properties cannot be accessed at runtime // all handlers are design properties, all api is runtime specProperties.add(propName); } } } } public Object executeScopeFunction(String function, JSONArray args) { Object object = scopeObject.get(function, scopeObject); if (object instanceof Function) { Context context = Context.enter(); try { Object[] array = new Object[args.length()]; for (int i = 0; i < args.length(); i++) { array[i] = args.get(i); } Object retValue = ((Function)object).call(context, scopeObject, scopeObject, array); return retValue == Undefined.instance ? null : retValue; } catch (JSONException e) { e.printStackTrace(); return null; } finally { Context.exit(); } } else { throw new RuntimeException("trying to call a function '" + function + "' that does not exists on a component '" + component.getName() + " with spec: " + webComponentSpec.getName()); } } protected boolean isApiFunctionEnabled(String functionName) { boolean isLocationOrSize = "getLocationX".equals(functionName) || "getLocationY".equals(functionName) || "getWidth".equals(functionName) || "getHeight".equals(functionName); if (isLocationOrSize) { // if parent form not visible, don't call api, let it get from the model on the server Container parent = component.getParent(); while (parent != null) { if (parent instanceof WebFormUI) { boolean isFormVisible = false; IWindow currentWindow = CurrentWindow.safeGet(); if (currentWindow instanceof INGClientWindow) { isFormVisible = ((INGClientWindow)currentWindow).hasForm(parent.getName()); } if (isFormVisible) { // it is reported to be still on the client // but do check also if it is not about to be hidden, by having // formVisible already set to false in the controller isFormVisible = ((WebFormUI)parent).getController().isFormVisible(); } if (!isFormVisible) { return false; } break; } parent = parent.getParent(); } // if it is not table view (it can have columns moved/resize) and not anchored, no need to call api, let it get from the model on the server FormElement fe = component.getFormElement(); if (fe.isLegacy() && fe.getPersistIfAvailable() instanceof ISupportAnchors && (fe.getForm().getView() != FormController.TABLE_VIEW && fe.getForm().getView() != FormController.LOCKED_TABLE_VIEW)) { int anchor = Utils.getAsInteger(component.getProperty(StaticContentSpecLoader.PROPERTY_ANCHORS.getPropertyName()));//((ISupportAnchors)fe.getPersistIfAvailable()).getAnchors(); if ((anchor == 0 || anchor == (IAnchorConstants.NORTH + IAnchorConstants.WEST))) { return false; } } } return true; } @Override public boolean isInstance(String name) { if ("RuntimeComponent".equals(name)) return true; if (getPrototype() instanceof IInstanceOf) { return ((IInstanceOf)getPrototype()).isInstance(name); } return false; } @Override public String getClassName() { return "RuntimeComponent"; } @Override public Object get(final String name, final Scriptable start) { if (specProperties != null && specProperties.contains(name)) { PropertyDescription pd = webComponentSpec.getProperties().get(name); if (WebFormComponent.isDesignOnlyProperty(pd)) return Scriptable.NOT_FOUND; return NGConversions.INSTANCE.convertSabloComponentToRhinoValue(component.getProperty(name), pd, component, start); } if ("getFormName".equals(name)) { return new Callable() { @Override public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { IWebFormUI parent = component.findParent(IWebFormUI.class); if (parent != null) { return parent.getController().getName(); } return null; } }; } final Function func = apiFunctions.get(name); if (func != null && isApiFunctionEnabled(name)) { final List<Pair<String, String>> oldVisibleForms = getVisibleForms(); return new BaseFunction() { @Override public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { Object retValue = func.call(cx, scope, thisObj, args); if (!(func instanceof WebComponentFunction)) { WebObjectApiDefinition def = webComponentSpec.getApiFunctions().get(name); retValue = NGConversions.INSTANCE.convertSabloComponentToRhinoValue(retValue, def.getReturnType(), component, null); } updateVisibleContainers(oldVisibleForms); return retValue; } }; } // check if we have a setter/getter for this property if (name != null && name.length() > 0) { String uName = new StringBuffer(name.substring(0, 1).toUpperCase()).append(name.substring(1)).toString(); if (apiFunctions.containsKey("set" + uName) && apiFunctions.containsKey("get" + uName)) { // call getter Function propertyGetter = apiFunctions.get("get" + uName); return propertyGetter.call(Context.getCurrentContext(), start, start, new Object[] { }); } } if ("svyMarkupId".equals(name)) { return ComponentFactory.getMarkupId(component.getFormElement().getForm().getName(), component.getName()); } if (prototypeScope != null) { return prototypeScope.get(name, start); } return Scriptable.NOT_FOUND; } @Override public Object get(int index, Scriptable start) { return null; } @Override public boolean has(String name, Scriptable start) { if (specProperties != null && specProperties.contains(name)) { PropertyDescription pd = webComponentSpec.getProperty(name); IPropertyType< ? > type = pd.getType(); // it is available by default, so if it doesn't have conversion, or if it has conversion and is explicitly available return !(type instanceof ISabloComponentToRhino< ? >) || ((ISabloComponentToRhino)type).isValueAvailableInRhino(component.getProperty(name), pd, component); } if (apiFunctions.containsKey(name)) return true; // check if we have a setter/getter for this property if (name != null && name.length() > 0) { String uName = new StringBuffer(name.substring(0, 1).toUpperCase()).append(name.substring(1)).toString(); return (apiFunctions.containsKey("set" + uName) && apiFunctions.containsKey("get" + uName)); } if ("getFormName".equals(name)) //$NON-NLS-1$ { return true; } return false; } @Override public boolean has(int index, Scriptable start) { return false; } @Override public void put(String name, Scriptable start, Object value) { if (isInvalidValue(name, value)) return; List<Pair<String, String>> oldVisibleForms = getVisibleForms(); if (specProperties != null && specProperties.contains(name)) { Object previousVal = null; PropertyDescription pd = webComponentSpec.getProperties().get(name); if (pd.getType() instanceof ISabloComponentToRhino && !(pd.getType() instanceof IRhinoToSabloComponent)) { // the it has sablo to rhino conversion but not the other way around then we should just use the // value from the conversion so call get(String,Scriptable) previousVal = get(name, start); } else previousVal = component.getProperty(name); Object val = NGConversions.INSTANCE.convertRhinoToSabloComponentValue(value, previousVal, pd, component); if (val != previousVal) component.setProperty(name, val); if (pd != null && pd.getType() instanceof VisiblePropertyType) { // search all labelfor elements for (WebComponent siblingComponent : component.getParent().getComponents()) { Collection<PropertyDescription> labelFors = siblingComponent.getSpecification().getProperties(LabelForPropertyType.INSTANCE); if (labelFors != null) { for (PropertyDescription labelForProperty : labelFors) { if (Utils.equalObjects(component.getName(), siblingComponent.getProperty(labelForProperty.getName()))) { // sibling component is labelfor, so set value to all its visible properties Collection<PropertyDescription> visibleProperties = siblingComponent.getSpecification().getProperties( VisiblePropertyType.INSTANCE); if (visibleProperties != null) { for (PropertyDescription visibleProperty : visibleProperties) { previousVal = siblingComponent.getProperty(visibleProperty.getName()); val = NGConversions.INSTANCE.convertRhinoToSabloComponentValue(value, previousVal, visibleProperty, siblingComponent); if (val != previousVal) siblingComponent.setProperty(name, val); } } break; } } } } } } else if (prototypeScope != null) { if (!apiFunctions.containsKey(name)) { // check if we have a setter for this property if (name != null && name.length() > 0) { String uName = new StringBuffer(name.substring(0, 1).toUpperCase()).append(name.substring(1)).toString(); if (apiFunctions.containsKey("set" + uName) && apiFunctions.containsKey("get" + uName)) { // call setter Function propertySetter = apiFunctions.get("set" + uName); propertySetter.call(Context.getCurrentContext(), start, start, new Object[] { value }); } else { prototypeScope.put(name, start, value); } } } } updateVisibleContainers(oldVisibleForms); } private boolean isInvalidValue(String name, Object value) { if (component.getFormElement() != null && component.getFormElement().getPersistIfAvailable() instanceof TabPanel && Utils.equalObjects(name, "tabIndex")) { Object tabs = component.getProperty("tabs"); if (tabs instanceof List && value instanceof Number) { int index = ((Number)value).intValue() - 1; if (index < 0 || index >= ((List)tabs).size()) { return true; } } } return false; } private List<Pair<String, String>> getVisibleForms() { List<Pair<String, String>> visibleContainedForms = new ArrayList<Pair<String, String>>(); // legacy for now, should we do it more general, from the spec if (component.getFormElement() != null && component.getFormElement().getPersistIfAvailable() instanceof TabPanel) { Object tabIndex = component.getProperty("tabIndex"); Object tabs = component.getProperty("tabs"); if (tabs instanceof List && ((List)tabs).size() > 0) { List tabsList = (List)tabs; TabPanel tabpanel = (TabPanel)component.getFormElement().getPersistIfAvailable(); if (tabpanel.getTabOrientation() == TabPanel.SPLIT_HORIZONTAL || tabpanel.getTabOrientation() == TabPanel.SPLIT_VERTICAL) { for (int i = 0; i < tabsList.size(); i++) { Map<String, Object> tab = (Map<String, Object>)tabsList.get(i); if (tab != null) { String relationName = tab.get("relationName") != null ? tab.get("relationName").toString() : null; Object form = tab.get("containsFormId"); if (form != null) { visibleContainedForms.add(new Pair<String, String>(form.toString(), relationName)); } } } } else { Map<String, Object> visibleTab = null; if (tabIndex instanceof Number && tabsList.size() > 0 && ((Number)tabIndex).intValue() <= tabsList.size()) { int index = ((Number)tabIndex).intValue() - 1; if (index < 0) { index = 0; } visibleTab = (Map<String, Object>)(tabsList.get(index)); } else if (tabIndex instanceof String || tabIndex instanceof CharSequence) { for (int i = 0; i < tabsList.size(); i++) { Map<String, Object> tab = (Map<String, Object>)tabsList.get(i); if (Utils.equalObjects(tabIndex, tab.get("name"))) { visibleTab = tab; break; } } } if (visibleTab != null) { String relationName = visibleTab.get("relationName") != null ? visibleTab.get("relationName").toString() : null; Object form = visibleTab.get("containsFormId"); if (form != null) { visibleContainedForms.add(new Pair<String, String>(form.toString(), relationName)); } } } } } return visibleContainedForms; } private void updateVisibleContainers(List<Pair<String, String>> oldForms) { ((DataAdapterList)component.getDataConverterContext().getForm().getFormUI().getDataAdapterList()).updateRelatedVisibleForms(oldForms, getVisibleForms()); } @Override public void put(int index, Scriptable start, Object value) { } @Override public void delete(String name) { } @Override public void delete(int index) { } @Override public Scriptable getPrototype() { return prototypeScope; } @Override public void setPrototype(Scriptable prototype) { this.prototypeScope = prototype; } @Override public Scriptable getParentScope() { return parentScope; } @Override public void setParentScope(Scriptable parent) { parentScope = parent; } @Override public Object[] getIds() { ArrayList<String> al = new ArrayList<>(); for (String name : specProperties) { PropertyDescription pd = webComponentSpec.getProperty(name); if (WebFormComponent.isDesignOnlyProperty(pd)) continue; IPropertyType< ? > type = pd.getType(); if (!(type instanceof ISabloComponentToRhino< ? >) || ((ISabloComponentToRhino)type).isValueAvailableInRhino(component.getProperty(name), pd, component)) { al.add(name); } } al.addAll(apiFunctions.keySet()); return al.toArray(); } @Override public Object getDefaultValue(Class< ? > hint) { return null; } @Override public boolean hasInstance(Scriptable instance) { return false; } public WebFormComponent getComponent() { return component; } }