diff --git a/docs/providers/aws/cli-reference/invoke-local.md b/docs/providers/aws/cli-reference/invoke-local.md
index e52daead2..20a618a98 100644
--- a/docs/providers/aws/cli-reference/invoke-local.md
+++ b/docs/providers/aws/cli-reference/invoke-local.md
@@ -96,7 +96,9 @@ This example will pass the json context in the `lib/context.json` file (relative
### Limitations
-Currently, `invoke local` only supports the NodeJs and Python runtimes.
+Currently, `invoke local` only supports the NodeJs, Python & Java runtimes.
+
+**Note:** In order to get correct output when using Java runtime, your Response class must implement `toString()` method.
## Resource permissions
diff --git a/lib/plugins/aws/invokeLocal/.gitignore b/lib/plugins/aws/invokeLocal/.gitignore
index 0d20b6487..376be9d3e 100644
--- a/lib/plugins/aws/invokeLocal/.gitignore
+++ b/lib/plugins/aws/invokeLocal/.gitignore
@@ -1 +1,5 @@
*.pyc
+java/target
+java/.project
+java/.settings
+java/.classpath
diff --git a/lib/plugins/aws/invokeLocal/index.js b/lib/plugins/aws/invokeLocal/index.js
index 927c212a1..2bdb17365 100644
--- a/lib/plugins/aws/invokeLocal/index.js
+++ b/lib/plugins/aws/invokeLocal/index.js
@@ -2,6 +2,7 @@
const BbPromise = require('bluebird');
const _ = require('lodash');
+const fs = BbPromise.promisifyAll(require('fs'));
const path = require('path');
const validate = require('../lib/validate');
const chalk = require('chalk');
@@ -136,8 +137,17 @@ class AwsInvokeLocal {
this.options.context);
}
+ if (runtime === 'java8') {
+ return this.invokeLocalJava(
+ 'java',
+ handler,
+ this.serverless.service.package.artifact,
+ this.options.data,
+ this.options.context);
+ }
+
throw new this.serverless.classes
- .Error('You can only invoke Node.js & Python functions locally.');
+ .Error('You can only invoke Node.js, Python & Java functions locally.');
}
invokeLocalPython(runtime, handlerPath, handlerName, event, context) {
@@ -161,6 +171,68 @@ class AwsInvokeLocal {
});
}
+ callJavaBridge(artifactPath, className, input) {
+ return new BbPromise((resolve) => fs.statAsync(artifactPath).then(() => {
+ const java = spawn('java', [
+ `-DartifactPath=${artifactPath}`,
+ `-DclassName=${className}`,
+ '-jar',
+ path.join(__dirname, 'java', 'target', 'invoke-bridge-1.0.jar'),
+ ]);
+
+ this.serverless.cli.log([
+ 'In order to get human-readable output,',
+ ' please implement "toString()" method of your "ApiGatewayResponse" object.',
+ ].join(''));
+
+ java.stdout.on('data', (buf) => this.serverless.cli.consoleLog(buf.toString()));
+ java.stderr.on('data', (buf) => this.serverless.cli.consoleLog(buf.toString()));
+ java.stdin.write(input);
+ java.stdin.end();
+ java.on('close', () => resolve());
+ }).catch(() => {
+ throw new Error(`Artifact ${artifactPath} doesn't exists, please compile it first.`);
+ }));
+ }
+
+ invokeLocalJava(runtime, className, artifactPath, event, customContext) {
+ const timeout = Number(this.options.functionObj.timeout)
+ || Number(this.serverless.service.provider.timeout)
+ || 6;
+ const context = {
+ name: this.options.functionObj.name,
+ version: 'LATEST',
+ logGroupName: this.provider.naming.getLogGroupName(this.options.functionObj.name),
+ timeout,
+ };
+ const input = JSON.stringify({
+ event: event || {},
+ context: customContext || context,
+ });
+
+ const javaBridgePath = path.join(__dirname, 'java');
+ const executablePath = path.join(javaBridgePath, 'target');
+
+ return new BbPromise(resolve => fs.statAsync(executablePath)
+ .then(() => this.callJavaBridge(artifactPath, className, input))
+ .then(resolve)
+ .catch(() => {
+ const mvn = spawn('mvn', [
+ 'package',
+ '-f',
+ path.join(javaBridgePath, 'pom.xml'),
+ ]);
+
+ this.serverless.cli
+ .log('Building Java bridge, first invocation might take a bit longer.');
+
+ 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));
+ }));
+ }
+
invokeLocalNodeJs(handlerPath, handlerName, event, customContext) {
let lambda;
diff --git a/lib/plugins/aws/invokeLocal/index.test.js b/lib/plugins/aws/invokeLocal/index.test.js
index 4222be2c6..8e73d67ec 100644
--- a/lib/plugins/aws/invokeLocal/index.test.js
+++ b/lib/plugins/aws/invokeLocal/index.test.js
@@ -3,6 +3,9 @@
const expect = require('chai').expect;
const sinon = require('sinon');
const path = require('path');
+const mockRequire = require('mock-require');
+const EventEmitter = require('events');
+const fse = require('fs-extra');
const AwsInvokeLocal = require('./index');
const AwsProvider = require('../provider/awsProvider');
const Serverless = require('../../../Serverless');
@@ -276,12 +279,15 @@ describe('AwsInvokeLocal', () => {
describe('#invokeLocal()', () => {
let invokeLocalNodeJsStub;
let invokeLocalPythonStub;
+ let invokeLocalJavaStub;
beforeEach(() => {
invokeLocalNodeJsStub =
sinon.stub(awsInvokeLocal, 'invokeLocalNodeJs').resolves();
invokeLocalPythonStub =
sinon.stub(awsInvokeLocal, 'invokeLocalPython').resolves();
+ invokeLocalJavaStub =
+ sinon.stub(awsInvokeLocal, 'invokeLocalJava').resolves();
awsInvokeLocal.serverless.service.service = 'new-service';
awsInvokeLocal.options = {
@@ -298,6 +304,7 @@ describe('AwsInvokeLocal', () => {
afterEach(() => {
awsInvokeLocal.invokeLocalNodeJs.restore();
awsInvokeLocal.invokeLocalPython.restore();
+ awsInvokeLocal.invokeLocalJava.restore();
});
it('should call invokeLocalNodeJs when no runtime is set', () => awsInvokeLocal.invokeLocal()
@@ -352,12 +359,26 @@ describe('AwsInvokeLocal', () => {
{},
undefined
)).to.be.equal(true);
- delete awsInvokeLocal.options.functionObj.runtime;
});
});
- it('throw error when using runtime other than Node.js or Python', () => {
+ it('should call invokeLocalJava when java8 runtime is set', () => {
awsInvokeLocal.options.functionObj.runtime = 'java8';
+ return awsInvokeLocal.invokeLocal()
+ .then(() => {
+ expect(invokeLocalJavaStub.calledOnce).to.be.equal(true);
+ expect(invokeLocalJavaStub.calledWithExactly(
+ 'java',
+ 'handler.hello',
+ undefined,
+ {},
+ undefined
+ )).to.be.equal(true);
+ });
+ });
+
+ it('throw error when using runtime other than Node.js or Python', () => {
+ awsInvokeLocal.options.functionObj.runtime = 'invalid-runtime';
expect(() => awsInvokeLocal.invokeLocal()).to.throw(Error);
delete awsInvokeLocal.options.functionObj.runtime;
});
@@ -507,4 +528,183 @@ describe('AwsInvokeLocal', () => {
});
});
});
+
+ describe('#callJavaBridge()', () => {
+ let awsInvokeLocalMocked;
+ let writeChildStub;
+ let endChildStub;
+
+ beforeEach(() => {
+ writeChildStub = sinon.stub();
+ endChildStub = sinon.stub();
+
+ mockRequire('child_process', {
+ spawn: () => ({
+ stderr: new EventEmitter().on('data', () => {}),
+ stdout: new EventEmitter().on('data', () => {}),
+ stdin: {
+ write: writeChildStub,
+ end: endChildStub,
+ },
+ on: (key, callback) => callback(),
+ }),
+ });
+
+ // Remove Node.js internal "require cache" contents and re-require ./index.js
+ delete require.cache[require.resolve('./index')];
+ delete require.cache[require.resolve('child_process')];
+
+ const AwsInvokeLocalMocked = require('./index'); // eslint-disable-line global-require
+
+ serverless.setProvider('aws', new AwsProvider(serverless));
+ awsInvokeLocalMocked = new AwsInvokeLocalMocked(serverless, options);
+
+ awsInvokeLocalMocked.options = {
+ stage: 'dev',
+ function: 'first',
+ functionObj: {
+ handler: 'handler.hello',
+ name: 'hello',
+ timeout: 4,
+ },
+ data: {},
+ };
+ });
+
+ afterEach(() => {
+ delete require.cache[require.resolve('./index')];
+ delete require.cache[require.resolve('child_process')];
+ });
+
+ it('spawns java process with correct arguments', () =>
+ awsInvokeLocalMocked.callJavaBridge(
+ __dirname,
+ 'com.serverless.Handler',
+ '{}'
+ ).then(() => {
+ expect(writeChildStub.calledOnce).to.be.equal(true);
+ expect(endChildStub.calledOnce).to.be.equal(true);
+ expect(writeChildStub.calledWithExactly('{}')).to.be.equal(true);
+ })
+ );
+ });
+
+ describe('#invokeLocalJava()', () => {
+ const bridgePath = path.join(__dirname, 'java', 'target');
+ let callJavaBridgeStub;
+
+ beforeEach(() => {
+ fse.mkdirsSync(bridgePath);
+ callJavaBridgeStub = sinon.stub(awsInvokeLocal, 'callJavaBridge').resolves();
+ awsInvokeLocal.options = {
+ stage: 'dev',
+ function: 'first',
+ functionObj: {
+ handler: 'handler.hello',
+ name: 'hello',
+ timeout: 4,
+ },
+ data: {},
+ };
+ });
+
+ afterEach(() => {
+ awsInvokeLocal.callJavaBridge.restore();
+ fse.removeSync(bridgePath);
+ });
+
+ it('should invoke callJavaBridge when bridge is built', () =>
+ awsInvokeLocal.invokeLocalJava(
+ 'java',
+ 'com.serverless.Handler',
+ __dirname,
+ {}
+ ).then(() => {
+ expect(callJavaBridgeStub.calledOnce).to.be.equal(true);
+ expect(callJavaBridgeStub.calledWithExactly(
+ __dirname,
+ 'com.serverless.Handler',
+ JSON.stringify({
+ event: {},
+ context: {
+ name: 'hello',
+ version: 'LATEST',
+ logGroupName: '/aws/lambda/hello',
+ timeout: 4,
+ },
+ })
+ )).to.be.equal(true);
+ })
+ );
+
+ describe('when attempting to build the Java bridge', () => {
+ let awsInvokeLocalMocked;
+ let callJavaBridgeMockedStub;
+
+ beforeEach(() => {
+ mockRequire('child_process', {
+ spawn: () => ({
+ stderr: new EventEmitter().on('data', () => {}),
+ stdout: new EventEmitter().on('data', () => {}),
+ stdin: {
+ write: () => {},
+ end: () => {},
+ },
+ on: (key, callback) => callback(),
+ }),
+ });
+
+ // Remove Node.js internal "require cache" contents and re-require ./index.js
+ delete require.cache[require.resolve('./index')];
+ delete require.cache[require.resolve('child_process')];
+
+ const AwsInvokeLocalMocked = require('./index'); // eslint-disable-line global-require
+
+ serverless.setProvider('aws', new AwsProvider(serverless));
+ awsInvokeLocalMocked = new AwsInvokeLocalMocked(serverless, options);
+ callJavaBridgeMockedStub = sinon.stub(awsInvokeLocalMocked, 'callJavaBridge').resolves();
+
+ awsInvokeLocalMocked.options = {
+ stage: 'dev',
+ function: 'first',
+ functionObj: {
+ handler: 'handler.hello',
+ name: 'hello',
+ timeout: 4,
+ },
+ data: {},
+ };
+ });
+
+ afterEach(() => {
+ awsInvokeLocalMocked.callJavaBridge.restore();
+ delete require.cache[require.resolve('./index')];
+ delete require.cache[require.resolve('child_process')];
+ });
+
+ it('if it\'s not present yet', () =>
+ awsInvokeLocalMocked.invokeLocalJava(
+ 'java',
+ 'com.serverless.Handler',
+ __dirname,
+ {}
+ ).then(() => {
+ expect(callJavaBridgeMockedStub.calledOnce).to.be.equal(true);
+ expect(callJavaBridgeMockedStub.calledWithExactly(
+ __dirname,
+ 'com.serverless.Handler',
+ JSON.stringify({
+ event: {},
+ context: {
+ name: 'hello',
+ version: 'LATEST',
+ logGroupName: '/aws/lambda/hello',
+ timeout: 4,
+ },
+ })
+ )).to.be.equal(true);
+ })
+ );
+ });
+ });
});
diff --git a/lib/plugins/aws/invokeLocal/java/MANIFEST.mf b/lib/plugins/aws/invokeLocal/java/MANIFEST.mf
new file mode 100644
index 000000000..3cc01de12
--- /dev/null
+++ b/lib/plugins/aws/invokeLocal/java/MANIFEST.mf
@@ -0,0 +1,2 @@
+Manifest-version: 1.0
+Main-Class: com.serverless.InvokeBridge
diff --git a/lib/plugins/aws/invokeLocal/java/pom.xml b/lib/plugins/aws/invokeLocal/java/pom.xml
new file mode 100644
index 000000000..c47be034b
--- /dev/null
+++ b/lib/plugins/aws/invokeLocal/java/pom.xml
@@ -0,0 +1,71 @@
+
+
+ 4.0.0
+
+ com.serverless
+ invoke-bridge
+ 1.0
+
+
+ 1.8
+ 1.8
+
+
+
+
+ com.amazonaws
+ aws-lambda-java-core
+ 1.1.0
+
+
+ com.amazonaws
+ aws-lambda-java-log4j
+ 1.0.0
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ 2.8.5
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.8.5
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ 2.8.5
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 2.3
+
+ false
+
+
+
+ package
+
+ shade
+
+
+
+
+ com.serverless.InvokeBridge
+
+
+
+
+
+
+
+
+
diff --git a/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/Context.java b/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/Context.java
new file mode 100644
index 000000000..b0c93945c
--- /dev/null
+++ b/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/Context.java
@@ -0,0 +1,86 @@
+package com.serverless;
+
+import com.amazonaws.services.lambda.runtime.Client;
+import com.amazonaws.services.lambda.runtime.ClientContext;
+import com.amazonaws.services.lambda.runtime.CognitoIdentity;
+import com.amazonaws.services.lambda.runtime.LambdaLogger;
+
+import java.util.Map;
+
+public class Context implements com.amazonaws.services.lambda.runtime.Context {
+ private String name;
+ private String version;
+ private String logGroupName;
+ private long endTime;
+
+ Context(String name, String version, String logGroupName, int timeout) {
+ this.name = name;
+ this.version = version;
+ this.logGroupName = logGroupName;
+ this.endTime = System.currentTimeMillis() + (timeout * 1000);
+ }
+
+ public String getAwsRequestId() {
+ return "1234567890";
+ }
+
+ public String getLogGroupName() {
+ return this.logGroupName;
+ }
+
+ public String getLogStreamName() {
+ return "LogStream_" + this.name;
+ }
+
+ public String getFunctionName() {
+ return this.name;
+ }
+
+ public String getFunctionVersion() {
+ return this.version;
+ }
+
+ public String getInvokedFunctionArn() {
+ return "arn:aws:lambda:serverless:" + this.name;
+ }
+
+ public CognitoIdentity getIdentity() {
+ return new CognitoIdentity() {
+ public String getIdentityId() {
+ return "1";
+ }
+
+ public String getIdentityPoolId() {
+ return "1";
+ }
+ };
+ }
+
+ public ClientContext getClientContext() {
+ return new ClientContext() {
+ public Client getClient() {
+ return null;
+ }
+
+ public Map getCustom() {
+ return null;
+ }
+
+ public Map getEnvironment() {
+ return System.getenv();
+ }
+ };
+ }
+
+ public int getRemainingTimeInMillis() {
+ return Math.max(0, (int) (this.endTime - System.currentTimeMillis()));
+ }
+
+ public int getMemoryLimitInMB() {
+ return 1024;
+ }
+
+ public LambdaLogger getLogger() {
+ return System.out::println;
+ }
+}
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
new file mode 100644
index 000000000..ab9629426
--- /dev/null
+++ b/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/InvokeBridge.java
@@ -0,0 +1,88 @@
+package com.serverless;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.reflect.Method;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.HashMap;
+
+public class InvokeBridge {
+ private File artifact;
+ private String className;
+ private Object instance;
+ private Class clazz;
+
+ private InvokeBridge() {
+ this.artifact = new File(new File("."), System.getProperty("artifactPath"));
+ this.className = System.getProperty("className");
+
+ try {
+ HashMap parsedInput = parseInput(getInput());
+ HashMap eventMap = (HashMap) parsedInput.get("event");
+
+ this.instance = this.getInstance();
+
+ System.out.println(this.invoke(eventMap, this.getContext(parsedInput)).toString());
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private Context getContext(HashMap parsedInput) {
+ HashMap contextMap = (HashMap) parsedInput.get("context");
+
+ String name = (String) contextMap.getOrDefault("name", "functionName");
+ String version = (String) contextMap.getOrDefault("version", "LATEST");
+ String logGroupName = (String) contextMap.getOrDefault("logGroupName", "logGroup");
+ int timeout = Integer.parseInt(String.valueOf(contextMap.getOrDefault("timeout", 5)));
+
+ return new Context(name, version, logGroupName, timeout);
+ }
+
+ private Object getInstance() throws Exception {
+ URL[] urls = {this.artifact.toURI().toURL()};
+ URLClassLoader child = new URLClassLoader(urls, this.getClass().getClassLoader());
+
+ this.clazz = Class.forName(this.className, true, child);
+
+ return this.clazz.newInstance();
+ }
+
+ private Object invoke(HashMap event, Context context) throws Exception {
+ Method[] methods = this.clazz.getDeclaredMethods();
+
+ return methods[1].invoke(this.instance, event, context);
+ }
+
+ private HashMap parseInput(String input) throws IOException {
+ TypeReference> typeRef = new TypeReference>() {};
+ ObjectMapper mapper = new ObjectMapper();
+
+ JsonNode jsonNode = mapper.readTree(input);
+
+ return mapper.convertValue(jsonNode, typeRef);
+ }
+
+ private String getInput() throws IOException {
+ BufferedReader streamReader = new BufferedReader(new InputStreamReader(System.in, "UTF-8"));
+ StringBuilder inputStringBuilder = new StringBuilder();
+ String inputStr;
+
+ while ((inputStr = streamReader.readLine()) != null) {
+ inputStringBuilder.append(inputStr);
+ }
+
+ return inputStringBuilder.toString();
+ }
+
+ public static void main(String[] args) {
+ new InvokeBridge();
+ }
+}
diff --git a/lib/plugins/create/templates/aws-java-maven/src/main/java/com/serverless/Handler.java b/lib/plugins/create/templates/aws-java-maven/src/main/java/com/serverless/Handler.java
index 80a6f78e8..6d6c670e5 100644
--- a/lib/plugins/create/templates/aws-java-maven/src/main/java/com/serverless/Handler.java
+++ b/lib/plugins/create/templates/aws-java-maven/src/main/java/com/serverless/Handler.java
@@ -3,6 +3,7 @@ package com.serverless;
import java.util.Collections;
import java.util.Map;
+import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;
import com.amazonaws.services.lambda.runtime.Context;
@@ -14,6 +15,8 @@ public class Handler implements RequestHandler