How would I go about adding multiple RPC handlers to the gateway? The current module I’m working on has a fairly large API and I would like to split it over organize it over multiple classes. Do I have to do some sort of switch statement in the getRPCHandler
class of the GatewayHook? I see that the ModuleRPCFactory allows you to request separate interfaces.
You can put your implementations anywhere you want them. The actual RPC implementation returned from the gateway hook can be full of one-liners that delegate to the actual class and method that is appropriate.
The RPC engine does the first “switch” for you with the method name. Nothing prevents you from defining a second level of naming for some of the first level calls.
But I’d have to setup a ton of 1 line methods to do this?
So just to clarify what I’m trying to do - I have a bunch of data classes and service classes doing CRUD operations on the gateway representing some objects in my module and I was hoping I could make a abstract RPC handler like this:
public abstract class RpcHandler<T> {}...
Then I’d like to find a way to have a top level RPC handler class return a reference to that so I can save myself from having to write 1000’s of one liners delegating everything to the correct member service method.
I think the best you’re going to get is the “delegate” pattern.
Imagine you have interfaces IFoo
and IBar
, and class ModuleRPC
.
Implement ModuleRPC
like this:
public class ModuleRPC implements IFoo, IBar {
IFoo fooImpl;
IBar barImpl;
@Override
public void bar() {
barImpl.bar();
}
@Override
public void foo() {
fooImpl.foo();
}
}
but let IntelliJ generate all the delegated methods for you once you’ve added the impl references via Code > Generate... > Delegate Methods...
.
And if you’re allergic to boilerplate, “just” switch to Kotlin
class ModuleRPC(context: GatewayContext) : IFoo by FooImpl(context), IBar by BarImpl(context)
Strictly speaking, the RPC system just does switching on string identifiers. First is the module ID, where Ignition looks up the module to get your handler. It then looks up the method name (second string identifier) via reflection and hands off the rest of the arguments to that method.
Nothing prevents you from setting up another layer of indirection, into classes of your choice. Or some mixture, where some methods do further indirection and some perform a direct action.
You don't even have to make interfaces for everything if you simply define methods that accept Object...
variants.
This is what I ended up doing actually. I thought I’d post the solution here if anybody is looking for the same thing here in the future.
I made a hash-map of the various RPC service I would like to be exposed then defined a single method in my gateway RPC handler:
@Override
public <T> T invoke(Class<?> apiClass, String method, Object... args) {
var service = serviceMap.get(apiClass);
Objects.requireNonNull(service, "No service found for class " + apiClass);
try {
return ReflectionUtils.invokeMethod(service, method, args);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
Then added a method to a reflections utility class to call the method I want:
@SuppressWarnings("unchecked")
public static <T> T invokeMethod(Object bean, String methodName, Object... args) throws IllegalAccessException,
InvocationTargetException, NoSuchMethodException {
Class<?> beanClass = bean.getClass();
Class<?>[] argTypes = new Class<?>[args.length];
for (int i = 0; i < args.length; i++) {
argTypes[i] = args[i].getClass();
}
Method method = beanClass.getMethod(methodName, argTypes);
return (T) method.invoke(bean, args);
}
I quite liked having a common interface so that I have to fulfill a contract on both ends of the RPC call because I think that will prevent a lot of bugs…
Hello @cody.tamaki,
I'm trying to reproduce the same RPC Handler as you.
But I don't understand how you override the invoke method.
Does your RPC Handler extends/implements from another class?
I'm also trying to use multiple RPC Handlers, but can't figure it out even after reading through this Post a hundred times.
Here is what I've got so far.
In the Common scope:
In the Gateway scope:
In the Client Scope:
Under ClientTagHandler I want to have a rpc call that calls the methods defined under GatewayTagHandler. And under ClientLV_Scripts I want to have a rpc call that calls the methods defined under GatewayLV_Scripts.
What do I need to do to make this work?
You can't. A module can only have one RPC handler.
Good news: You will be able to have arbitrarily many RPC handlers in 8.3, under custom "namespaces" within your module.
Any word on when 8.3 is coming out?
No earlier than the end of this year.
A beta release may be available around ICC, in September.
Are there still plans to provide early access for module authors once API changes are finalized?
The beta will be the early access for most developers.
Here is what I've done now.
GatewayHook:
@Override
public Object getRPCHandler(ClientReqSession session, String projectName) {
return new RPCHandler();
}
RPCHandler:
public class RPCHandler implements RPCHandlerIntf {
@Override
public TagHandler getTagHandler(){
return new GatewayTagHandler();
}
@Override
public ScriptInterface getScripts(){
return new GatewayScripts();
}
@Override
public double square(double value){
return value*value;
}
}
RPCHandlerIntf:
public interface RPCHandlerIntf {
public TagHandler getTagHandler();
public ScriptInterface getScripts();
public double square(double value);
}
ClientScripts:
public class ClientScripts extends AbstractScripts {
public static RPCHandlerIntf rpc = ModuleRPCFactory.create(
"com.lvcontrol.ignition",
RPCHandlerIntf.class);
public ClientScripts(){
}
@Override
public double multiply(double val1, double val2) {
return rpc.getScripts().multiply(val1, val2);
}
public double square(double value){
return rpc.square(value);
}
}
ClientHook:
public void initializeScriptManager(ScriptManager manager) {
super.initializeScriptManager(manager);
manager.addScriptModule("system.lvcontrol",
new ClientScripts(),
new PropertiesFileDocProvider()
);
}
The square(value)
script works, but the multiply(val1,val2)
script doesn't.
The multiply
script throws the following, anyone know why?
>>>
Java Traceback:
Traceback (most recent call last):
File "<input>", line 1, in <module>
at jdk.proxy2/jdk.proxy2.$Proxy51.getScripts(Unknown Source)
at com.lvcontrol.ignition.scripting.ClientScripts.multiply(ClientScripts.java:28)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
java.lang.reflect.UndeclaredThrowableException: java.lang.reflect.UndeclaredThrowableException
at org.python.core.Py.JavaError(Py.java:545)
at org.python.core.Py.JavaError(Py.java:536)
at org.python.core.PyReflectedFunction.__call__(PyReflectedFunction.java:192)
at com.inductiveautomation.ignition.common.script.ScriptManager$ReflectedInstanceFunction.__call__(ScriptManager.java:553)
at org.python.core.PyObject.__call__(PyObject.java:477)
at org.python.core.PyObject.__call__(PyObject.java:481)
at org.python.pycode._pyx10.f$0(<input>:1)
at org.python.pycode._pyx10.call_function(<input>)
at org.python.core.PyTableCode.call(PyTableCode.java:173)
at org.python.core.PyCode.call(PyCode.java:18)
at org.python.core.Py.runCode(Py.java:1703)
at org.python.core.Py.exec(Py.java:1747)
at org.python.util.PythonInterpreter.exec(PythonInterpreter.java:277)
at org.python.util.InteractiveInterpreter.runcode(InteractiveInterpreter.java:130)
at com.inductiveautomation.ignition.designer.gui.tools.jythonconsole.JythonConsole$ConsoleWorker.doInBackground(JythonConsole.java:628)
at com.inductiveautomation.ignition.designer.gui.tools.jythonconsole.JythonConsole$ConsoleWorker.doInBackground(JythonConsole.java:616)
at java.desktop/javax.swing.SwingWorker$1.call(Unknown Source)
at java.base/java.util.concurrent.FutureTask.run(Unknown Source)
at java.desktop/javax.swing.SwingWorker.run(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.lang.reflect.UndeclaredThrowableException
at jdk.proxy2/jdk.proxy2.$Proxy51.getScripts(Unknown Source)
at com.lvcontrol.ignition.scripting.ClientScripts.multiply(ClientScripts.java:28)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
at org.python.core.PyReflectedFunction.__call__(PyReflectedFunction.java:190)
... 19 more
Caused by: com.inductiveautomation.ignition.client.gateway_interface.GatewayException: class com.lvcontrol.ignition.scripting.GatewayScripts cannot be cast to class java.io.Serializable (com.lvcontrol.ignition.scripting.GatewayScripts is in unnamed module of loader com.inductiveautomation.ignition.gateway.modules.ModuleClassLoader @430e86c0; java.io.Serializable is in module java.base of loader 'bootstrap')
at com.inductiveautomation.ignition.client.gateway_interface.GatewayInterface.newGatewayException(GatewayInterface.java:360)
at com.inductiveautomation.ignition.client.gateway_interface.GatewayInterface.sendMessage(GatewayInterface.java:334)
at com.inductiveautomation.ignition.client.gateway_interface.GatewayInterface.sendMessage(GatewayInterface.java:287)
at com.inductiveautomation.ignition.client.gateway_interface.GatewayInterface.moduleInvokeSafe(GatewayInterface.java:930)
at com.inductiveautomation.ignition.client.gateway_interface.ModuleRPCFactory$DynamicRPCHandler.invoke(ModuleRPCFactory.java:53)
... 26 more
Caused by: java.lang.ClassCastException: class com.lvcontrol.ignition.scripting.GatewayScripts cannot be cast to class java.io.Serializable (com.lvcontrol.ignition.scripting.GatewayScripts is in unnamed module of loader com.inductiveautomation.ignition.gateway.modules.ModuleClassLoader @430e86c0; java.io.Serializable is in module java.base of loader 'bootstrap')
at com.inductiveautomation.ignition.gateway.servlets.gateway.functions.ModuleInvoke.invoke(ModuleInvoke.java:172)
at com.inductiveautomation.ignition.gateway.servlets.Gateway.doPost(Gateway.java:435)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:523)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:590)
at com.inductiveautomation.ignition.gateway.bootstrap.MapServlet.service(MapServlet.java:86)
at org.eclipse.jetty.servlet.ServletHolder$NotAsync.service(ServletHolder.java:1410)
at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:764)
at org.eclipse.jetty.servlet.ServletHandler$ChainEnd.doFilter(ServletHandler.java:1665)
at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:527)
at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:131)
at org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:578)
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)
at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:223)
at org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:1570)
at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:221)
at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1384)
at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:176)
at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:484)
at org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:1543)
at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:174)
at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1306)
at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:129)
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)
at com.inductiveautomation.catapult.handlers.RemoteHostNameLookupHandler.handle(RemoteHostNameLookupHandler.java:121)
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)
at org.eclipse.jetty.rewrite.handler.RewriteHandler.handle(RewriteHandler.java:301)
at org.eclipse.jetty.server.handler.HandlerList.handle(HandlerList.java:51)
at org.eclipse.jetty.server.handler.HandlerCollection.handle(HandlerCollection.java:141)
at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:122)
at org.eclipse.jetty.server.Server.handle(Server.java:563)
at org.eclipse.jetty.server.HttpChannel.lambda$handle$0(HttpChannel.java:505)
at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:762)
at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:497)
at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:282)
at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:314)
at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:100)
at org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53)
at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.runTask(AdaptiveExecutionStrategy.java:416)
at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.consumeTask(AdaptiveExecutionStrategy.java:385)
at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.tryProduce(AdaptiveExecutionStrategy.java:272)
at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.lambda$new$0(AdaptiveExecutionStrategy.java:140)
at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:411)
at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:969)
at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1194)
at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1149)
at java.lang.Thread.run(null)
Traceback (most recent call last):
File "<input>", line 1, in <module>
at jdk.proxy2/jdk.proxy2.$Proxy51.getScripts(Unknown Source)
at com.lvcontrol.ignition.scripting.ClientScripts.multiply(ClientScripts.java:28)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
java.lang.reflect.UndeclaredThrowableException: java.lang.reflect.UndeclaredThrowableException
>>>
The RPC proxy stuff is trying to return an instance of your GatewayScripts
class to the client, because of this line in ClientScripts
:
rpc.getScripts().multiply(val1, val2)