/* 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.scripting; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.json.JSONArray; import org.json.JSONException; import org.mozilla.javascript.Context; import org.mozilla.javascript.Function; import org.mozilla.javascript.NativeJavaObject; import org.mozilla.javascript.Script; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.Undefined; import org.mozilla.javascript.debug.Debugger; import org.sablo.BaseWebObject; import org.sablo.specification.PropertyDescription; import org.sablo.specification.WebObjectApiDefinition; import org.sablo.specification.WebObjectSpecification; import org.sablo.specification.property.IPropertyType; import com.servoy.j2db.IApplication; import com.servoy.j2db.scripting.InstanceJavaMembers; import com.servoy.j2db.scripting.SolutionScope; import com.servoy.j2db.server.ngclient.INGApplication; import com.servoy.j2db.server.ngclient.property.types.NGConversions; import com.servoy.j2db.server.ngclient.property.types.NGConversions.ISabloComponentToRhino; import com.servoy.j2db.util.Debug; import com.servoy.j2db.util.Pair; import com.servoy.j2db.util.Utils; /** * A {@link Scriptable} for wrapping a client side service. * So that model values can be get and set and api functions can be called. * * @author jcompagner */ public class WebServiceScriptable implements Scriptable { private static final ConcurrentMap<URI, Pair<Script, Long>> scripts = new ConcurrentHashMap<>(); private static Script getScript(Context context, URL serverScript) throws URISyntaxException, IOException { Pair<Script, Long> pair = scripts.get(serverScript.toURI()); long lastModified = serverScript.openConnection().getLastModified(); if (pair == null || pair.getRight().longValue() < lastModified) { String name = ""; URI uri = serverScript.toURI(); if ("file".equals(uri.getScheme())) { File file = new File(uri); if (file.exists()) { name = file.getAbsolutePath(); } } Debugger debugger = null; int lvl = context.getOptimizationLevel(); Script script; try { if ("".endsWith(name)) { context.setGeneratingDebug(false); debugger = context.getDebugger(); if (debugger != null) { context.setOptimizationLevel(9); context.setDebugger(null, null); } } context.setGeneratingSource(false); script = context.compileString(Utils.getURLContent(serverScript), name, 1, null); } finally { if (debugger != null) { context.setDebugger(debugger, null); context.setOptimizationLevel(lvl); } } pair = new Pair<Script, Long>(script, Long.valueOf(lastModified)); scripts.put(uri, pair); } return pair.getLeft(); } /** * Compiles the server side script, enabled debugging if possible. * It returns the $scope object * * @param serverScript * @param app */ public static Scriptable compileServerScript(URL serverScript, Scriptable model, IApplication app) { Scriptable scopeObject = null; Context context = Context.enter(); try { Scriptable topLevel = ScriptableObject.getTopLevelScope(model); if (topLevel == null) { // This should not really happen anymore. Debug.log("toplevel object not found for creating serverside script: " + serverScript); topLevel = context.initStandardObjects(); } Scriptable apiObject = null; Scriptable execScope = context.newObject(topLevel); execScope.setParentScope(topLevel); scopeObject = context.newObject(execScope); apiObject = context.newObject(execScope); scopeObject.put("api", scopeObject, apiObject); scopeObject.put("model", scopeObject, model); execScope.put("$scope", execScope, scopeObject); getScript(context, serverScript).exec(context, execScope); apiObject.setPrototype(model); execScope.put("console", execScope, new NativeJavaObject(execScope, new ConsoleObject(app), new InstanceJavaMembers(execScope, ConsoleObject.class))); } catch (Exception ex) { Debug.error(ex); } finally { Context.exit(); } return scopeObject; } private final INGApplication application; private final WebObjectSpecification serviceSpecification; private Scriptable prototype; private Scriptable parent; private Scriptable apiObject; private Scriptable scopeObject; /** * @param ngClient * @param serviceSpecification * @param solutionScope */ public WebServiceScriptable(INGApplication application, WebObjectSpecification serviceSpecification, SolutionScope solutionScope) { this.application = application; setParentScope(solutionScope); this.serviceSpecification = serviceSpecification; URL serverScript = serviceSpecification.getServerScript(); if (serverScript != null) { scopeObject = compileServerScript(serverScript, this, application); apiObject = (Scriptable)scopeObject.get("api", scopeObject); } } 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 the service with spec: " + serviceSpecification.getName()); } } @Override public String getClassName() { return "WebServiceScriptable"; } @Override public Object get(String name, Scriptable start) { WebObjectApiDefinition apiFunction = serviceSpecification.getApiFunction(name); if (apiFunction != null && apiObject != null) { Object serverSideFunction = apiObject.get(apiFunction.getName(), apiObject); if (serverSideFunction instanceof Function) { return serverSideFunction; } } if (apiFunction != null) { return new WebServiceFunction(application.getWebsocketSession(), apiFunction, serviceSpecification.getName()); } BaseWebObject service = (BaseWebObject)application.getWebsocketSession().getClientService(serviceSpecification.getName()); Object value = service.getProperty(name); PropertyDescription desc = serviceSpecification.getProperty(name); if (desc != null) { if (desc.getType() instanceof ISabloComponentToRhino< ? >) { return NGConversions.INSTANCE.convertSabloComponentToRhinoValue(value, desc, service, start); } else { return value; // types that don't implement the sablo <-> rhino conversions are by default available and their value is accessible directly } } else return getParentScope().get(name, start); } @Override public Object get(int index, Scriptable start) { return null; } @Override public boolean has(String name, Scriptable start) { PropertyDescription desc = serviceSpecification.getProperty(name); if (desc != null) { BaseWebObject service = (BaseWebObject)application.getWebsocketSession().getClientService(serviceSpecification.getName()); IPropertyType< ? > type = desc.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(service.getProperty(name), desc, service); } WebObjectApiDefinition apiFunction = serviceSpecification.getApiFunction(name); if (apiFunction != null) return true; return application.getWebsocketSession().getClientService(serviceSpecification.getName()).getProperties().content.containsKey(name); } @Override public boolean has(int index, Scriptable start) { return false; } @Override public void put(String name, Scriptable start, Object value) { PropertyDescription desc = serviceSpecification.getProperty(name); BaseWebObject service = (BaseWebObject)application.getWebsocketSession().getClientService(serviceSpecification.getName()); if (desc != null) { Object previousVal = service.getProperty(name); Object val = NGConversions.INSTANCE.convertRhinoToSabloComponentValue(value, previousVal, desc, service); if (val != previousVal) service.setProperty(name, val); } else { WebObjectApiDefinition apiFunction = serviceSpecification.getApiFunction(name); // don't allow api to be overwritten. if (apiFunction != null) return; // TODO conversion should happen from string (color representation) to Color object. // DesignConversion.toObject(value, type) service.setProperty(name, value); } } @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 prototype; } @Override public void setPrototype(Scriptable prototype) { this.prototype = prototype; } @Override public Scriptable getParentScope() { return parent; } @Override public void setParentScope(Scriptable parent) { this.parent = parent; } @Override public Object[] getIds() { ArrayList<String> al = new ArrayList<>(); BaseWebObject service = null; if (application != null) { service = (BaseWebObject)application.getWebsocketSession().getClientService(serviceSpecification.getName()); } for (String name : serviceSpecification.getAllPropertiesNames()) { PropertyDescription pd = serviceSpecification.getProperty(name); IPropertyType< ? > type = pd.getType(); if (service == null || !(type instanceof ISabloComponentToRhino< ? >) || ((ISabloComponentToRhino)type).isValueAvailableInRhino(service.getProperty(name), pd, service)) { al.add(name); } } al.addAll(serviceSpecification.getApiFunctions().keySet()); return al.toArray(); } @Override public Object getDefaultValue(Class< ? > hint) { return serviceSpecification.toString(); } @Override public boolean hasInstance(Scriptable instance) { return false; } }