Repository: serverless/serverless (JavaScript)
Date: 2017-12-20 15:37:03+00:00
Task type: Bug Fix

Issue:
can't invoke scala locally
<!--
1. If you have a question and not a bug/feature request please ask it at http://forum.serverless.com
2. Please check if an issue already exists so there are no duplicates
3. Check out and follow our Guidelines: https://github.com/serverless/serverless/blob/master/CONTRIBUTING.md
4. Fill out the whole template so we have a good overview on the issue
5. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the Issue
6. Please follow the template, otherwise we'll have to ask you to update it
-->

# This is a Bug Report

## Description

* What went wrong? Function was not invoked
* What did you expect should have happened? Function was invoked
* What was the config you used? newly generated project from aws-scala-sbt template
* What stacktrace or error message from your provider did you see? n/a

Similar or dependent issues:
#2864 

## Additional Data

This happens because the handler takes an object as input, but InvokeBridge is trying to call it with a `Map<String,Object>` (AWS supports [other types](http://docs.aws.amazon.com/lambda/latest/dg/java-programming-model-req-resp.html) too).
I'm working on a patch that will instantiate the object and set its properties via reflection using java beans.

* ***Serverless Framework Version you're using***: 1.24.1
* ***Operating System***: macOS 10.13.1 (17B1003)
* ***Stack Trace***:
```
~/D/I/s/aws-scala-sbt ❯ sls invoke local -f hello
Serverless: In order to get human-readable output, please implement "toString()" method of your "ApiGatewayResponse" object.
java.lang.IllegalArgumentException: argument type mismatch

	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at com.serverless.InvokeBridge.invoke(InvokeBridge.java:61)
	at com.serverless.InvokeBridge.<init>(InvokeBridge.java:32)
	at com.serverless.InvokeBridge.main(InvokeBridge.java:86)
```


Issue discussion:
Thank you for reporting this bug @zydeco! 💯 

Glad that you're willing to fix it. Shoot me questions if anything is unclear. I'm not Java pro but I might help with SLS internals.

Solution patch:
diff --git a/lib/plugins/aws/invokeLocal/index.js b/lib/plugins/aws/invokeLocal/index.js
index f73c5cb10fe..49cd79b349f 100644
--- a/lib/plugins/aws/invokeLocal/index.js
+++ b/lib/plugins/aws/invokeLocal/index.js
@@ -118,10 +118,10 @@ class AwsInvokeLocal {
       || this.serverless.service.provider.runtime
       || 'nodejs4.3';
     const handler = this.options.functionObj.handler;
-    const handlerPath = handler.split('.')[0];
-    const handlerName = handler.split('.')[1];
 
     if (runtime.startsWith('nodejs')) {
+      const handlerPath = handler.split('.')[0];
+      const handlerName = handler.split('.')[1];
       return this.invokeLocalNodeJs(
         handlerPath,
         handlerName,
@@ -130,6 +130,8 @@ class AwsInvokeLocal {
     }
 
     if (runtime === 'python2.7' || runtime === 'python3.6') {
+      const handlerPath = handler.split('.')[0];
+      const handlerName = handler.split('.')[1];
       return this.invokeLocalPython(
         process.platform === 'win32' ? 'python.exe' : runtime,
         handlerPath,
@@ -139,9 +141,12 @@ class AwsInvokeLocal {
     }
 
     if (runtime === 'java8') {
+      const className = handler.split('::')[0];
+      const handlerName = handler.split('::')[1] || 'handleRequest';
       return this.invokeLocalJava(
         'java',
-        handler,
+        className,
+        handlerName,
         this.serverless.service.package.artifact,
         this.options.data,
         this.options.context);
@@ -177,11 +182,12 @@ class AwsInvokeLocal {
     });
   }
 
-  callJavaBridge(artifactPath, className, input) {
+  callJavaBridge(artifactPath, className, handlerName, input) {
     return new BbPromise((resolve) => fs.statAsync(artifactPath).then(() => {
       const java = spawn('java', [
         `-DartifactPath=${artifactPath}`,
         `-DclassName=${className}`,
+        `-DhandlerName=${handlerName}`,
         '-jar',
         path.join(__dirname, 'java', 'target', 'invoke-bridge-1.0.jar'),
       ]);
@@ -201,7 +207,7 @@ class AwsInvokeLocal {
     }));
   }
 
-  invokeLocalJava(runtime, className, artifactPath, event, customContext) {
+  invokeLocalJava(runtime, className, handlerName, artifactPath, event, customContext) {
     const timeout = Number(this.options.functionObj.timeout)
       || Number(this.serverless.service.provider.timeout)
       || 6;
@@ -220,7 +226,7 @@ class AwsInvokeLocal {
     const executablePath = path.join(javaBridgePath, 'target');
 
     return new BbPromise(resolve => fs.statAsync(executablePath)
-      .then(() => this.callJavaBridge(artifactPath, className, input))
+      .then(() => this.callJavaBridge(artifactPath, className, handlerName, input))
       .then(resolve)
       .catch(() => {
         const mvn = spawn('mvn', [
@@ -235,7 +241,8 @@ class AwsInvokeLocal {
         mvn.stderr.on('data', (buf) => this.serverless.cli.consoleLog(`mvn - ${buf.toString()}`));
         mvn.stdin.end();
 
-        mvn.on('close', () => this.callJavaBridge(artifactPath, className, input).then(resolve));
+        mvn.on('close', () => this.callJavaBridge(artifactPath, className, handlerName, input)
+          .then(resolve));
       }));
   }
 
diff --git a/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/InvokeBridge.java b/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/InvokeBridge.java
index ab962942669..879c52f2fc5 100644
--- a/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/InvokeBridge.java
+++ b/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/InvokeBridge.java
@@ -4,6 +4,8 @@
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
+import java.beans.Introspector;
+import java.beans.PropertyDescriptor;
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.IOException;
@@ -16,12 +18,14 @@
 public class InvokeBridge {
   private File artifact;
   private String className;
+  private String handlerName;
   private Object instance;
   private Class clazz;
 
   private InvokeBridge() {
     this.artifact = new File(new File("."), System.getProperty("artifactPath"));
     this.className = System.getProperty("className");
+    this.handlerName = System.getProperty("handlerName");
 
     try {
       HashMap<String, Object> parsedInput = parseInput(getInput());
@@ -56,9 +60,52 @@ private Object getInstance() throws Exception {
   }
 
   private Object invoke(HashMap<String, Object> event, Context context) throws Exception {
-    Method[] methods = this.clazz.getDeclaredMethods();
+    Method method = findHandlerMethod(this.clazz, this.handlerName);
+    Class requestClass = method.getParameterTypes()[0];
+
+    Object request = event;
+    if (!requestClass.isAssignableFrom(event.getClass())) {
+      request = requestClass.newInstance();
+      PropertyDescriptor[] properties = Introspector.getBeanInfo(requestClass).getPropertyDescriptors();
+      for(int i=0; i < properties.length; i++) {
+        if (properties[i].getWriteMethod() == null) continue;
+        String propertyName = properties[i].getName();
+        if (event.containsKey(propertyName)) {
+          properties[i].getWriteMethod().invoke(request, event.get(propertyName));
+        }
+      }
+    }
+
+    if (method.getParameterCount() == 1) {
+      return method.invoke(this.instance, request);
+    } else if (method.getParameterCount() == 2) {
+      return method.invoke(this.instance, request, context);
+    } else {
+      throw new NoSuchMethodException("Handler should take 1 or 2 arguments: " + method);
+    }
+  }
+
+  private Method findHandlerMethod(Class clazz, String handlerName) throws Exception {
+    Method candidateMethod = null;
+    for(Method method: clazz.getDeclaredMethods()) {
+      if (method.getName().equals(handlerName) && !method.isBridge()) {
+        // Select the method with the largest number of parameters
+        // If two or more methods have the same number of parameters, AWS Lambda selects the method that has
+        // the Context as the last parameter.
+        // If none or all of these methods have the Context parameter, then the behavior is undefined.
+        int paramCount = method.getParameterCount();
+        boolean lastParamIsContext = paramCount >= 1 && method.getParameterTypes()[paramCount-1].getName().equals("com.amazonaws.services.lambda.runtime.Context");
+        if (candidateMethod == null || paramCount > candidateMethod.getParameterCount() || (paramCount == candidateMethod.getParameterCount() && lastParamIsContext)) {
+          candidateMethod = method;
+        }
+      }
+    }
+
+    if (candidateMethod == null) {
+      throw new NoSuchMethodException("Could not find handler for " + handlerName + " in " + clazz.getName());
+    }
 
-    return methods[1].invoke(this.instance, event, context);
+    return candidateMethod;
   }
 
   private HashMap<String, Object> parseInput(String input) throws IOException {


Test patch:
diff --git a/lib/plugins/aws/invokeLocal/index.test.js b/lib/plugins/aws/invokeLocal/index.test.js
index d2542acaeb6..c2e83743507 100644
--- a/lib/plugins/aws/invokeLocal/index.test.js
+++ b/lib/plugins/aws/invokeLocal/index.test.js
@@ -379,6 +379,7 @@ describe('AwsInvokeLocal', () => {
           expect(invokeLocalJavaStub.calledWithExactly(
             'java',
             'handler.hello',
+            'handleRequest',
             undefined,
             {},
             undefined
@@ -588,6 +589,7 @@ describe('AwsInvokeLocal', () => {
       awsInvokeLocalMocked.callJavaBridge(
         __dirname,
         'com.serverless.Handler',
+        'handleRequest',
         '{}'
       ).then(() => {
         expect(writeChildStub.calledOnce).to.be.equal(true);
@@ -625,6 +627,7 @@ describe('AwsInvokeLocal', () => {
       awsInvokeLocal.invokeLocalJava(
         'java',
         'com.serverless.Handler',
+        'handleRequest',
         __dirname,
         {}
       ).then(() => {
@@ -632,6 +635,7 @@ describe('AwsInvokeLocal', () => {
         expect(callJavaBridgeStub.calledWithExactly(
           __dirname,
           'com.serverless.Handler',
+          'handleRequest',
           JSON.stringify({
             event: {},
             context: {
@@ -694,6 +698,7 @@ describe('AwsInvokeLocal', () => {
         awsInvokeLocalMocked.invokeLocalJava(
           'java',
           'com.serverless.Handler',
+          'handleRequest',
           __dirname,
           {}
         ).then(() => {
@@ -701,6 +706,7 @@ describe('AwsInvokeLocal', () => {
           expect(callJavaBridgeMockedStub.calledWithExactly(
             __dirname,
             'com.serverless.Handler',
+            'handleRequest',
             JSON.stringify({
               event: {},
               context: {