Repository: localstack/localstack

Issue:
Allow deployment of single JAR for Java Lambdas
AWS supports two forms of archives for deploying Java Lambda functions:
1. "Fat" JAR - single jar file with all dependencies
2. ZIP - zip archive with class files in `/` and jar files in `/lib`

http://docs.aws.amazon.com/lambda/latest/dg/lambda-java-how-to-create-deployment-package.html

LocalStack currently expects a single JAR file within a ZIP file, which is incompatible with what AWS expects.


Discussion:
Another thing which doesn't work is having a python handler in a subdirectory. It's the same piece of code which is to blame it seems to me: https://github.com/localstack/localstack/blob/debb24a792a7e2a1751ddf1f30d5c79f80b4885f/localstack/services/awslambda/lambda_api.py#L373-L391

Resolving patch:
diff --git a/localstack/services/awslambda/lambda_api.py b/localstack/services/awslambda/lambda_api.py
--- a/localstack/services/awslambda/lambda_api.py
+++ b/localstack/services/awslambda/lambda_api.py
@@ -461,7 +461,7 @@ def execute(event, context):
             result, log_output = lambda_executors.EXECUTOR_LOCAL.execute_java_lambda(
                 event, context, handler=handler, main_file=main_file)
             return result
-        return execute
+        return execute, zip_file_content
     raise ClientError(error_response(
         'Unable to extract Java Lambda handler - file is not a valid zip/jar files', 400, error_type='ValidationError'))
 
@@ -536,14 +536,12 @@ def generic_handler(event, context):
         # which runs Lambda in Docker by passing all *.jar files in the function
         # working directory as part of the classpath. Because of this, we need to
         # save the zip_file_content as a .jar here.
+        lambda_handler, zip_file_content = get_java_handler(zip_file_content, handler_name, tmp_file)
         if is_jar_archive(zip_file_content):
             jar_tmp_file = '{working_dir}/{file_name}'.format(
                 working_dir=lambda_cwd, file_name=LAMBDA_JAR_FILE_NAME)
             save_file(jar_tmp_file, zip_file_content)
 
-        lambda_handler = get_java_handler(zip_file_content, handler_name, tmp_file)
-        if isinstance(lambda_handler, Response):
-            return lambda_handler
     else:
         handler_file = get_handler_file_from_name(handler_name, runtime=runtime)
         handler_function = get_handler_function_from_name(handler_name, runtime=runtime)
diff --git a/localstack/utils/common.py b/localstack/utils/common.py
--- a/localstack/utils/common.py
+++ b/localstack/utils/common.py
@@ -741,20 +741,17 @@ def _unzip_file_entry(zip_ref, file_entry, target_dir):
 
 
 def is_jar_archive(content):
-    has_class_content = False
-    try:
-        has_class_content = 'class' in content
-    except TypeError:
-        # in Python 3 we need to use byte strings for byte-based file content
-        has_class_content = b'class' in content
-    if not has_class_content:
-        return False
+    """ Determine whether `content` contains valid zip bytes representing a JAR archive
+        that contains at least one *.class file and a META-INF/MANIFEST.MF file. """
     try:
         with tempfile.NamedTemporaryFile() as tf:
             tf.write(content)
             tf.flush()
             with zipfile.ZipFile(tf.name, 'r') as zf:
-                zf.infolist()
+                class_files = [e for e in zf.infolist() if e.filename.endswith('.class')]
+                manifest_file = [e for e in zf.infolist() if e.filename.upper() == 'META-INF/MANIFEST.MF']
+                if not class_files or not manifest_file:
+                    return False
     except Exception:
         return False
     return True


Test patch:
diff --git a/tests/integration/test_lambda.py b/tests/integration/test_lambda.py
--- a/tests/integration/test_lambda.py
+++ b/tests/integration/test_lambda.py
@@ -453,6 +453,7 @@ def setUpClass(cls):
         # Lambda supports single JAR deployments without the zip,
         # so we upload the JAR directly.
         cls.test_java_jar = load_file(TEST_LAMBDA_JAVA, mode='rb')
+        cls.test_java_zip = testutil.create_zip_file(TEST_LAMBDA_JAVA, get_content=True)
         testutil.create_lambda_function(
             func_name=TEST_LAMBDA_NAME_JAVA,
             zip_file=cls.test_java_jar,
@@ -471,7 +472,7 @@ def setUpClass(cls):
         # deploy lambda - Java with serializable input object
         testutil.create_lambda_function(
             func_name=TEST_LAMBDA_NAME_JAVA_SERIALIZABLE,
-            zip_file=cls.test_java_jar,
+            zip_file=cls.test_java_zip,
             runtime=LAMBDA_RUNTIME_JAVA8,
             handler='cloud.localstack.sample.SerializedInputLambdaHandler'
         )