From 6f2b3c5facbe5be514648fa7dc8dcc7193d93416 Mon Sep 17 00:00:00 2001 From: Slawek Walkowski <12511618+swalkowski@users.noreply.github.com> Date: Tue, 16 Apr 2019 11:04:02 +0200 Subject: [PATCH 001/364] Set Sonatype OSS as the parent artifact. (#1) --- functions-framework-api/pom.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/functions-framework-api/pom.xml b/functions-framework-api/pom.xml index a60105ae..36c18bc9 100644 --- a/functions-framework-api/pom.xml +++ b/functions-framework-api/pom.xml @@ -15,6 +15,13 @@ --> 4.0.0 + + + org.sonatype.oss + oss-parent + 9 + + com.google.cloud functions-framework-api 1.0.0-alpha-1 From 76748d89dfd5c5ec2f39d936f02ca06af327a6a4 Mon Sep 17 00:00:00 2001 From: Slawek Walkowski <12511618+swalkowski@users.noreply.github.com> Date: Tue, 16 Apr 2019 11:05:49 +0200 Subject: [PATCH 002/364] Change groupId to com.google.cloud.functions. (#2) --- functions-framework-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions-framework-api/pom.xml b/functions-framework-api/pom.xml index 36c18bc9..9e048e7b 100644 --- a/functions-framework-api/pom.xml +++ b/functions-framework-api/pom.xml @@ -22,7 +22,7 @@ 9 - com.google.cloud + com.google.cloud.functions functions-framework-api 1.0.0-alpha-1 From 4953e8d7d954403f568a12a82a49ce4b99d69a40 Mon Sep 17 00:00:00 2001 From: Grant Timmerman Date: Tue, 14 May 2019 02:36:21 -0700 Subject: [PATCH 003/364] feat: Add basic Java gitignore (#4) --- .gitignore | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ca0f542f --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear +*.zip +*.tar.gz +*.rar + +# Gradle +.gradle +.idea/ +build/ +credentials/ +out/ +gradlew +gradlew.bat +*.iml +*.properties + +client_secret.json +credentials.json +tokens/ + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +.DS_Store +.settings/ +.classpath +.project +properties + +# IDE +.vscode/ From fad41b7f6a69527123cfd909b7789477aa46fd6c Mon Sep 17 00:00:00 2001 From: Grant Timmerman Date: Wed, 15 May 2019 02:50:39 -0700 Subject: [PATCH 004/364] feat: Add Travis (#3) * feat: Add Travis * fix: Remove JDK 9 --- .travis.yml | 7 +++++++ README.md | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..0b2f8768 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: java +jdk: + - oraclejdk8 +# Use a Build Matrix for subdirs (https://lord.io/blog/2014/travis-multiple-subdirs/) +env: + - DIR=functions-framework-api/ +script: cd $DIR && gradle build # gradle -q run diff --git a/README.md b/README.md index 2d201a8b..bba0dfa9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# Functions Framework API for Java +# Functions Framework API for Java [![Build Status](https://travis-ci.org/GoogleCloudPlatform/functions-framework-java.svg?branch=master)](https://travis-ci.org/GoogleCloudPlatform/functions-framework-java) + **This is a placeholder for the Functions Framework for Java. Stay tuned for updates.** From f5a0722ab1386092b13176aa19466f09454d8156 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Tue, 17 Sep 2019 16:45:34 -0700 Subject: [PATCH 005/364] Add the new API for HTTP functions. (#6) --- .../google/cloud/functions/HttpFunction.java | 28 ++++++ .../google/cloud/functions/HttpMessage.java | 98 +++++++++++++++++++ .../google/cloud/functions/HttpRequest.java | 86 ++++++++++++++++ .../google/cloud/functions/HttpResponse.java | 93 ++++++++++++++++++ 4 files changed, 305 insertions(+) create mode 100644 functions-framework-api/src/main/java/com/google/cloud/functions/HttpFunction.java create mode 100644 functions-framework-api/src/main/java/com/google/cloud/functions/HttpMessage.java create mode 100644 functions-framework-api/src/main/java/com/google/cloud/functions/HttpRequest.java create mode 100644 functions-framework-api/src/main/java/com/google/cloud/functions/HttpResponse.java diff --git a/functions-framework-api/src/main/java/com/google/cloud/functions/HttpFunction.java b/functions-framework-api/src/main/java/com/google/cloud/functions/HttpFunction.java new file mode 100644 index 00000000..3f8e4239 --- /dev/null +++ b/functions-framework-api/src/main/java/com/google/cloud/functions/HttpFunction.java @@ -0,0 +1,28 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.cloud.functions; + +/** + * Represents a Cloud Function that is activated by an HTTP request. + */ +@FunctionalInterface +public interface HttpFunction { + /** + * Called to service an incoming HTTP request. This interface is implemented by user code to + * provide the action for a given function. If the method throws any exception (including any + * {@link Error}) then the HTTP response will have a 500 status code. + */ + void service(HttpRequest request, HttpResponse response) throws Exception; +} diff --git a/functions-framework-api/src/main/java/com/google/cloud/functions/HttpMessage.java b/functions-framework-api/src/main/java/com/google/cloud/functions/HttpMessage.java new file mode 100644 index 00000000..b946cafc --- /dev/null +++ b/functions-framework-api/src/main/java/com/google/cloud/functions/HttpMessage.java @@ -0,0 +1,98 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.cloud.functions; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Represents an HTTP message, either an HTTP request or a part of a multipart HTTP request. + */ +public interface HttpMessage { + /** Returns the value of the {@code Content-Type} header, if any. */ + Optional getContentType(); + + /** Returns the numeric value of the {@code Content-Length} header. */ + long getContentLength(); + + /** + * Returns the character encoding specified in the {@code Content-Type} header, + * or {@code Optional.empty()} if there is no {@code Content-Type} header or it does not have the + * {@code charset} parameter. + */ + Optional getCharacterEncoding(); + + /** + * Returns an {@link InputStream} that can be used to read the body of this HTTP request. + * Every call to this method on the same {@link HttpMessage} will return the same object. + * This method is typically used to read binary data. If the body is text, the + * {@link #getReader()} method is more appropriate. + * + * @throws IOException if a valid {@link InputStream} cannot be returned for some reason. + * @throws IllegalStateException if {@link #getReader()} has already been called on this instance. + */ + InputStream getInputStream() throws IOException; + + /** + * Returns a {@link BufferedReader} that can be used to read the text body of this HTTP request. + * Every call to this method on the same {@link HttpMessage} will return the same object. + * + * @throws IOException if a valid {@link BufferedReader} cannot be returned for some reason. + * @throws IllegalStateException if {@link #getInputStream()} has already been called on this + * instance. + */ + BufferedReader getReader() throws IOException; + + /** + * Returns a map describing the headers of this HTTP request, or this part of a multipart + * request. If the headers look like this... + * + *
+   *   Content-Type: text/plain
+   *   Some-Header: some value
+   *   Some-Header: another value
+   * 
+ * + * ...then the returned value will map {@code Content-Type} to a one-element list containing + * {@code text/plain}, and {@code Some-Header} to a two-element list containing {@code some value} + * and {@code another value}. + */ + Map> getHeaders(); + + /** + * Convenience method that returns the value of the first header with the given name. If the + * headers look like this... + * + *
+   *   Content-Type: text/plain
+   *   Some-Header: some value
+   *   Some-Header: another value
+   * 
+ * + * ...then {@code getFirstHeader("Some-Header")} will return {@code Optional.of("some value")}, + * and {@code getFirstHeader("Another-Header")} will return {@code Optional.empty()}. + */ + default Optional getFirstHeader(String name) { + List headers = getHeaders().get(name); + if (headers == null || headers.isEmpty()) { + return Optional.empty(); + } + return Optional.of(headers.get(0)); + } +} diff --git a/functions-framework-api/src/main/java/com/google/cloud/functions/HttpRequest.java b/functions-framework-api/src/main/java/com/google/cloud/functions/HttpRequest.java new file mode 100644 index 00000000..829f309b --- /dev/null +++ b/functions-framework-api/src/main/java/com/google/cloud/functions/HttpRequest.java @@ -0,0 +1,86 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.cloud.functions; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Represents the contents of an HTTP request that is being serviced by a Cloud Function. + */ +public interface HttpRequest extends HttpMessage { + /** The HTTP method of this request, such as {@code "POST"} or {@code "GET"}. */ + String getMethod(); + + /** The full URI of this request as it arrived at the server. */ + String getUri(); + + /** + * The path part of the URI for this request, without any query. If the full URI is + * {@code http://foo.com/bar/baz?this=that}, then this method will return {@code /bar/baz}. + */ + String getPath(); + + /** + * The query part of the URI for this request. If the full URI is + * {@code http://foo.com/bar/baz?this=that}, then this method will return {@code this=that}. + * If there is no query part, the returned {@code Optional} is empty. + */ + Optional getQuery(); + + /** + * The query parameters of this request. If the full URI is + * {@code http://foo.com/bar?thing=thing1&thing=thing2&cat=hat}, then the returned map will map + * {@code thing} to the list {@code "thing1", "thing2"} and {@code cat} to the list with the + * single element {@code "hat"}. + */ + Map> getQueryParameters(); + + /** + * The first query parameter with the given name, if any. If the full URI is + * {@code http://foo.com/bar?thing=thing1&thing=thing2&cat=hat}, then + * {@code getFirstQueryParameter("thing")} will return {@code Optional.of("thing1")} and + * {@code getFirstQueryParameter("something")} will return {@code Optional.empty()}. This is a + * more convenient alternative to {@link #getQueryParameters}. + */ + default Optional getFirstQueryParameter(String name) { + List parameters = getQueryParameters().get(name); + if (parameters == null || parameters.isEmpty()) { + return Optional.empty(); + } + return Optional.of(parameters.get(0)); + } + + /** + * Represents one part inside a multipart ({@code multipart/form-data}) HTTP request. Each such + * part can have its own HTTP headers, which can be retrieved with the methods inherited from + * {@link HttpMessage}. + */ + interface HttpPart extends HttpMessage { + /** Returns the filename associated with this part, if any. */ + Optional getFileName(); + } + + /** + * Returns the parts inside this multipart ({@code multipart/form-data}) HTTP request. Each entry + * in the returned map has the name of the part as its key and the contents as the associated + * value. + * + * @throws IllegalStateException if the {@link #getContentType() content type} is not + * {@code multipart/form-data}. + */ + Map getParts(); +} diff --git a/functions-framework-api/src/main/java/com/google/cloud/functions/HttpResponse.java b/functions-framework-api/src/main/java/com/google/cloud/functions/HttpResponse.java new file mode 100644 index 00000000..7d5188a1 --- /dev/null +++ b/functions-framework-api/src/main/java/com/google/cloud/functions/HttpResponse.java @@ -0,0 +1,93 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.cloud.functions; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Represents the contents of an HTTP response that is being sent by a Cloud Function in response + * to an HTTP request. + */ +public interface HttpResponse { + /** + * Sets the numeric HTTP + * status + * code to use in the response. Most often this will be 200, which is the OK status. + */ + void setStatusCode(int code); + + /** + * Sets the numeric HTTP + * status + * code and reason message to use in the response. For example
+ * {@code setStatusCode(400, "Something went wrong")}. + */ + void setStatusCode(int code, String message); + + /** + * Sets the value to use for the {@code Content-Type} header in the response. This may include + * a character encoding, for example {@code setContentType("text/plain; charset=utf-8")}. + */ + void setContentType(String contentType); + + /** + * Returns the {@code Content-Type} that was previously set by {@link #setContentType}, or by + * {@link #appendHeader} with a header name of {@code Content-Type}. If no {@code Content-Type} + * has been set, returns {@code Optional.empty()}. + */ + Optional getContentType(); + + /** + * Includes the given header name with the given value in the response. This method may be called + * several times for the same header, in which case the response will contain the header the same + * number of times. + */ + void appendHeader(String header, String value); + + /** + * Returns the headers that have been defined for the response so far. This will contain at least + * the headers that have been set via {@link #appendHeader} or {@link #setContentType}, and may + * contain additional headers such as {@code Date}. + */ + Map> getHeaders(); + + /** + * Returns an {@link OutputStream} that can be used to write the body of the response. + * This method is typically used to write binary data. If the body is text, the + * {@link #getWriter()} method is more appropriate. + * + * @throws IOException if a valid {@link OutputStream} cannot be returned for some reason. + * @throws IllegalStateException if {@link #getWriter} has already been called on this instance. + */ + OutputStream getOutputStream() throws IOException; + + /** + * Returns a {@link BufferedWriter} that can be used to write the text body of the response. + * If the written text will not be US-ASCII, you should specify a character encoding by calling + * {@link #setContentType setContentType("text/foo; charset=bar")} or + * {@link #appendHeader appendHeader("Content-Type", "text/foo; charset=bar")} + * before calling this method. + * + * @throws IOException if a valid {@link BufferedWriter} cannot be returned for some reason. + * @throws IllegalStateException if {@link #getOutputStream} has already been called on this + * instance. + */ + BufferedWriter getWriter() throws IOException; +} From 2c9edc2b90adfb3c798bf48d214a7697938a08f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Tue, 17 Sep 2019 17:57:20 -0700 Subject: [PATCH 006/364] Update pom.xml with javadoc and Maven Central incantations copied from Guava. (#7) * Update pom.xml with javadoc and Maven Central incantations copied from Guava. * [maven-release-plugin] prepare release functions-framework-api-1.0.0-alpha-2-rc1 * [maven-release-plugin] prepare for next development iteration * Adjust maven-release-plugin configuration to reflect non-root pom.xml. --- functions-framework-api/pom.xml | 129 +++++++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 3 deletions(-) diff --git a/functions-framework-api/pom.xml b/functions-framework-api/pom.xml index 9e048e7b..97c77911 100644 --- a/functions-framework-api/pom.xml +++ b/functions-framework-api/pom.xml @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - + 4.0.0 @@ -24,14 +24,29 @@ com.google.cloud.functions functions-framework-api - 1.0.0-alpha-1 + 1.0.0-alpha-2-rc2-SNAPSHOT UTF-8 3.8.0 + 3.1.0 5.3.2 + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + scm:git:https://github.com/GoogleCloudPlatform/functions-framework-java.git + scm:git:git@github.com:GoogleCloudPlatform/functions-framework-java.git + https://github.com/GoogleCloudPlatform/functions-framework-java + + @@ -43,6 +58,114 @@ 1.8 + + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + org.apache.maven.plugins + maven-release-plugin + 2.5.3 + + + default + + perform + + + functions-framework-api/pom.xml + + + + + + + + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + true + true + UTF-8 + UTF-8 + UTF-8 + + -XDignore.symbol.file + + true + 8 + + + + attach-docs + post-integration-test + jar + + + + + - \ No newline at end of file + + + sonatype-nexus-snapshots + Sonatype Nexus Snapshots + https://oss.sonatype.org/content/repositories/snapshots/ + + + sonatype-nexus-staging + Nexus Release Repository + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + sonatype-oss-release + + + + org.apache.maven.plugins + maven-source-plugin + 3.0.1 + + + attach-sources + + jar-no-fork + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + + + + + + From 52bb625db82c82469d467008dfd33203140b22c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Mon, 28 Oct 2019 16:53:47 -0700 Subject: [PATCH 007/364] Add BackgroundFunction to the new API for cloud functions. (#8) --- functions-framework-api/pom.xml | 8 ++ .../cloud/functions/BackgroundFunction.java | 73 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java diff --git a/functions-framework-api/pom.xml b/functions-framework-api/pom.xml index 97c77911..0a02bfa5 100644 --- a/functions-framework-api/pom.xml +++ b/functions-framework-api/pom.xml @@ -168,4 +168,12 @@ + + + com.google.code.gson + gson + 2.8.6 + jar + +
diff --git a/functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java b/functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java new file mode 100644 index 00000000..7d4bd8aa --- /dev/null +++ b/functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java @@ -0,0 +1,73 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.cloud.functions; + +import com.google.gson.JsonElement; + +/** + * Represents a Cloud Function that is activated by an event. + * + *

Here is an example of an implementation that operates on the JSON payload of the event + * directly: + * + *

+ * public class Example implements BackgroundFunction {
+ *   private static final Logger logger = Logger.getLogger(Example.class.getName());
+ *
+ *   {@code @Override}
+ *   public void accept(JsonElement json, Context context) {
+ *     JsonElement messageId = json.getAsJsonObject().get("messageId");
+ *     String messageIdString = messageId.getAsJsonString();
+ *     logger.info("Got messageId " + messageIdString);
+ *   }
+ * }
+ * 
+ * + *

Here is an example of an implementation that deserializes the JSON payload into a Java + * object for simpler access: + * + *

+ * public class Example implements BackgroundFunction {
+ *   private static final Logger logger = Logger.getLogger(Example.class.getName());
+ *
+ *   {@code @Override}
+ *   public void accept(JsonElement json, Context context) {
+ *     PubSubMessage message = Gson.fromJson(json, PubSubMessage.class);
+ *     logger.info("Got messageId " + message.messageId);
+ *   }
+ * }
+ *
+ * // Where PubSubMessage is a user-defined class like this:
+ * public class PubSubMessage {
+ *   String data;
+ *   {@code Map} attributes;
+ *   String messageId;
+ *   String publishTime;
+ * }
+ * 
+ */ +@FunctionalInterface +public interface BackgroundFunction { + /** + * Called to service an incoming event. This interface is implemented by user code to + * provide the action for a given background function. + * (including any {@link Error}) then the HTTP response will have a 500 status code. + * + * @param json the payload of the event, as a parsed JSON object. + * @param context the context of the event. This is a set of values that every event has, + * separately from the payload, such as timestamp and event type. + */ + public void accept(JsonElement json, Context context); +} From 3e2544575a8e7006b92ed74b91db118b26e60b98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Thu, 7 Nov 2019 16:39:03 -0800 Subject: [PATCH 008/364] Configure Travis to use openjdk11. (#10) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0b2f8768..0e2a7547 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: java jdk: - - oraclejdk8 + - openjdk11 # Use a Build Matrix for subdirs (https://lord.io/blog/2014/travis-multiple-subdirs/) env: - DIR=functions-framework-api/ From a21066d5aab9c1206e54d729381b489d6295305a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89amonn=20McManus?= Date: Thu, 7 Nov 2019 16:43:32 -0800 Subject: [PATCH 009/364] Revise background functions API. (#9) * Revise background functions API. We no longer have an explicit dependency on Gson. Instead, background functions either receive the JSON payload as a String, or they specify the Java class into which it should be deserialized. We do this last part using Gson. Also avoid deprecated Gson methods, and fix some incorrect code in the code samples in the javadoc. --- functions-framework-api/pom.xml | 24 +++--- .../cloud/functions/BackgroundFunction.java | 44 ++++------- .../functions/RawBackgroundFunction.java | 74 +++++++++++++++++++ 3 files changed, 103 insertions(+), 39 deletions(-) create mode 100644 functions-framework-api/src/main/java/com/google/cloud/functions/RawBackgroundFunction.java diff --git a/functions-framework-api/pom.xml b/functions-framework-api/pom.xml index 0a02bfa5..396c6a3f 100644 --- a/functions-framework-api/pom.xml +++ b/functions-framework-api/pom.xml @@ -24,7 +24,7 @@ com.google.cloud.functions functions-framework-api - 1.0.0-alpha-2-rc2-SNAPSHOT + 1.0.0-alpha-2-rc3-SNAPSHOT UTF-8 @@ -45,12 +45,12 @@ scm:git:https://github.com/GoogleCloudPlatform/functions-framework-java.git scm:git:git@github.com:GoogleCloudPlatform/functions-framework-java.git https://github.com/GoogleCloudPlatform/functions-framework-java + HEAD - org.apache.maven.plugins maven-compiler-plugin ${maven-compiler-plugin.version} @@ -62,6 +62,18 @@ maven-javadoc-plugin ${maven-javadoc-plugin.version} + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + org.apache.maven.plugins maven-release-plugin @@ -168,12 +180,4 @@ - - - com.google.code.gson - gson - 2.8.6 - jar - - diff --git a/functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java b/functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java index 7d4bd8aa..bc7bf601 100644 --- a/functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java +++ b/functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java @@ -14,38 +14,22 @@ package com.google.cloud.functions; -import com.google.gson.JsonElement; - /** - * Represents a Cloud Function that is activated by an event. + * Represents a Cloud Function that is activated by an event and parsed into a user-supplied class. + * The payload of the event is a JSON object, which is deserialized into a user-defined class as + * described for + * Gson. * - *

Here is an example of an implementation that operates on the JSON payload of the event - * directly: + *

Here is an example of an implementation that accesses the {@code messageId} property from + * a payload that matches a user-defined {@code PubSubMessage} class: * *

- * public class Example implements BackgroundFunction {
+ * public class Example implements {@code BackgroundFunction} {
  *   private static final Logger logger = Logger.getLogger(Example.class.getName());
  *
  *   {@code @Override}
- *   public void accept(JsonElement json, Context context) {
- *     JsonElement messageId = json.getAsJsonObject().get("messageId");
- *     String messageIdString = messageId.getAsJsonString();
- *     logger.info("Got messageId " + messageIdString);
- *   }
- * }
- * 
- * - *

Here is an example of an implementation that deserializes the JSON payload into a Java - * object for simpler access: - * - *

- * public class Example implements BackgroundFunction {
- *   private static final Logger logger = Logger.getLogger(Example.class.getName());
- *
- *   {@code @Override}
- *   public void accept(JsonElement json, Context context) {
- *     PubSubMessage message = Gson.fromJson(json, PubSubMessage.class);
- *     logger.info("Got messageId " + message.messageId);
+ *   public void accept(PubSubMessage pubSubMessage, Context context) {
+ *     logger.info("Got messageId " + pubSubMessage.messageId);
  *   }
  * }
  *
@@ -57,17 +41,19 @@
  *   String publishTime;
  * }
  * 
+ * + * @param the class of payload objects that this function expects. */ @FunctionalInterface -public interface BackgroundFunction { +public interface BackgroundFunction { /** * Called to service an incoming event. This interface is implemented by user code to - * provide the action for a given background function. + * provide the action for a given background function. If this method throws any exception * (including any {@link Error}) then the HTTP response will have a 500 status code. * - * @param json the payload of the event, as a parsed JSON object. + * @param payload the payload of the event, deserialized from the original JSON string. * @param context the context of the event. This is a set of values that every event has, * separately from the payload, such as timestamp and event type. */ - public void accept(JsonElement json, Context context); + void accept(T payload, Context context); } diff --git a/functions-framework-api/src/main/java/com/google/cloud/functions/RawBackgroundFunction.java b/functions-framework-api/src/main/java/com/google/cloud/functions/RawBackgroundFunction.java new file mode 100644 index 00000000..472f7c4d --- /dev/null +++ b/functions-framework-api/src/main/java/com/google/cloud/functions/RawBackgroundFunction.java @@ -0,0 +1,74 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.cloud.functions; + +/** + * Represents a Cloud Function that is activated by an event. The payload of the event is a JSON + * object, which can be parsed using a JSON package such as + * GSON. + * + *

Here is an example of an implementation that parses the JSON payload using Gson, to access its + * {@code messageId} property: + * + *

+ * public class Example implements RawBackgroundFunction {
+ *   private static final Logger logger = Logger.getLogger(Example.class.getName());
+ *
+ *   {@code @Override}
+ *   public void accept(String json, Context context) {
+ *     JsonObject jsonObject = new Gson().fromJson(json, JsonObject.class);
+ *     JsonElement messageId = jsonObject.get("messageId");
+ *     String messageIdString = messageId.getAsJsonString();
+ *     logger.info("Got messageId " + messageIdString);
+ *   }
+ * }
+ * 
+ * + *

Here is an example of an implementation that deserializes the JSON payload into a Java + * object for simpler access, again using Gson: + * + *

+ * public class Example implements RawBackgroundFunction {
+ *   private static final Logger logger = Logger.getLogger(Example.class.getName());
+ *
+ *   {@code @Override}
+ *   public void accept(String json, Context context) {
+ *     PubSubMessage message = new Gson().fromJson(json, PubSubMessage.class);
+ *     logger.info("Got messageId " + message.messageId);
+ *   }
+ * }
+ *
+ * // Where PubSubMessage is a user-defined class like this:
+ * public class PubSubMessage {
+ *   String data;
+ *   {@code Map} attributes;
+ *   String messageId;
+ *   String publishTime;
+ * }
+ * 
+ */ +@FunctionalInterface +public interface RawBackgroundFunction { + /** + * Called to service an incoming event. This interface is implemented by user code to + * provide the action for a given background function. If this method throws any exception + * (including any {@link Error}) then the HTTP response will have a 500 status code. + * + * @param json the payload of the event, as a JSON string. + * @param context the context of the event. This is a set of values that every event has, + * separately from the payload, such as timestamp and event type. + */ + void accept(String json, Context context); +} From b9dfe0db6802bfec04261e39199e1533432a01b8 Mon Sep 17 00:00:00 2001 From: GCF Team Date: Mon, 21 Oct 2019 14:29:24 -0700 Subject: [PATCH 010/364] Move functions framework code to //third_party/functions_framework_java as suggested by third-party-help@ and third-party-go@. Also in line with the suggestion to move existing packages to the top-level in https://cs.corp.google.com/piper///depot/google3/third_party/cloud/NO_NEW_PACKAGES?g=0. gcf-relnote: n/a production-risk: low (moving the code to a new location) Not sure why I need the below since I'm effectively moving this NOW but whatevs: PiperOrigin-RevId: 275923889 --- LICENSE | 202 ------- README.md | 10 - functions-framework-api/pom.xml | 3 +- .../cloud/functions/BackgroundFunction.java | 59 --- .../functions/RawBackgroundFunction.java | 74 --- invoker-core/pom.xml | 158 ++++++ .../invoker/BackgroundCloudFunction.java | 39 ++ .../invoker/BackgroundFunctionExecutor.java | 45 ++ .../BackgroundFunctionSignatureMatcher.java | 45 ++ .../functions/invoker/CloudFunction.java | 30 ++ .../invoker/CloudFunctionsContext.java | 27 + .../google/cloud/functions/invoker/Event.java | 72 +++ .../functions/invoker/FunctionLoader.java | 55 ++ .../invoker/FunctionSignatureMatcher.java | 27 + .../functions/invoker/HttpCloudFunction.java | 19 + .../invoker/HttpFunctionExecutor.java | 27 + .../invoker/HttpFunctionSignatureMatcher.java | 35 ++ .../functions/invoker/URLRequestWrapper.java | 26 + .../invoker/http/HttpRequestImpl.java | 164 ++++++ .../invoker/http/HttpResponseImpl.java | 89 ++++ .../functions/invoker/runner/Invoker.java | 175 +++++++ .../src/main/resources/logging.properties | 4 + .../invoker/BackgroundFunctionTest.java | 88 ++++ .../functions/invoker/HttpFunctionTest.java | 57 ++ .../functions/invoker/IntegrationTest.java | 220 ++++++++ .../functions/invoker/http/HttpTest.java | 492 ++++++++++++++++++ .../testfunctions/BackgroundSnoop.java | 38 ++ .../functions/invoker/testfunctions/Echo.java | 15 + .../invoker/testfunctions/EchoUrl.java | 15 + .../invoker/testfunctions/HelloWorld.java | 12 + .../test/resources/adder_gcf_beta_event.json | 10 + .../adder_gcf_beta_event_json_resource.json | 14 + .../test/resources/adder_gcf_ga_event.json | 16 + 33 files changed, 2015 insertions(+), 347 deletions(-) delete mode 100644 LICENSE delete mode 100644 README.md delete mode 100644 functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java delete mode 100644 functions-framework-api/src/main/java/com/google/cloud/functions/RawBackgroundFunction.java create mode 100644 invoker-core/pom.xml create mode 100644 invoker-core/src/main/java/com/google/cloud/functions/invoker/BackgroundCloudFunction.java create mode 100644 invoker-core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java create mode 100644 invoker-core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionSignatureMatcher.java create mode 100644 invoker-core/src/main/java/com/google/cloud/functions/invoker/CloudFunction.java create mode 100644 invoker-core/src/main/java/com/google/cloud/functions/invoker/CloudFunctionsContext.java create mode 100644 invoker-core/src/main/java/com/google/cloud/functions/invoker/Event.java create mode 100644 invoker-core/src/main/java/com/google/cloud/functions/invoker/FunctionLoader.java create mode 100644 invoker-core/src/main/java/com/google/cloud/functions/invoker/FunctionSignatureMatcher.java create mode 100644 invoker-core/src/main/java/com/google/cloud/functions/invoker/HttpCloudFunction.java create mode 100644 invoker-core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionExecutor.java create mode 100644 invoker-core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionSignatureMatcher.java create mode 100644 invoker-core/src/main/java/com/google/cloud/functions/invoker/URLRequestWrapper.java create mode 100644 invoker-core/src/main/java/com/google/cloud/functions/invoker/http/HttpRequestImpl.java create mode 100644 invoker-core/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java create mode 100644 invoker-core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java create mode 100644 invoker-core/src/main/resources/logging.properties create mode 100644 invoker-core/src/test/java/com/google/cloud/functions/invoker/BackgroundFunctionTest.java create mode 100644 invoker-core/src/test/java/com/google/cloud/functions/invoker/HttpFunctionTest.java create mode 100644 invoker-core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java create mode 100644 invoker-core/src/test/java/com/google/cloud/functions/invoker/http/HttpTest.java create mode 100644 invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/BackgroundSnoop.java create mode 100644 invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Echo.java create mode 100644 invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/EchoUrl.java create mode 100644 invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/HelloWorld.java create mode 100644 invoker-core/src/test/resources/adder_gcf_beta_event.json create mode 100644 invoker-core/src/test/resources/adder_gcf_beta_event_json_resource.json create mode 100644 invoker-core/src/test/resources/adder_gcf_ga_event.json diff --git a/LICENSE b/LICENSE deleted file mode 100644 index d6456956..00000000 --- a/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/README.md b/README.md deleted file mode 100644 index bba0dfa9..00000000 --- a/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# Functions Framework API for Java [![Build Status](https://travis-ci.org/GoogleCloudPlatform/functions-framework-java.svg?branch=master)](https://travis-ci.org/GoogleCloudPlatform/functions-framework-java) - - -**This is a placeholder for the Functions Framework for Java. Stay tuned for -updates.** - -For now, it only includes definitions of types used in Java function signatures. - -* `com.google.cloud.functions.Context` - optional second argument of a - background function. diff --git a/functions-framework-api/pom.xml b/functions-framework-api/pom.xml index 396c6a3f..d3cdf30c 100644 --- a/functions-framework-api/pom.xml +++ b/functions-framework-api/pom.xml @@ -24,7 +24,7 @@ com.google.cloud.functions functions-framework-api - 1.0.0-alpha-2-rc3-SNAPSHOT + 1.0.0-alpha-2-rc2-SNAPSHOT UTF-8 @@ -45,7 +45,6 @@ scm:git:https://github.com/GoogleCloudPlatform/functions-framework-java.git scm:git:git@github.com:GoogleCloudPlatform/functions-framework-java.git https://github.com/GoogleCloudPlatform/functions-framework-java - HEAD diff --git a/functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java b/functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java deleted file mode 100644 index bc7bf601..00000000 --- a/functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions; - -/** - * Represents a Cloud Function that is activated by an event and parsed into a user-supplied class. - * The payload of the event is a JSON object, which is deserialized into a user-defined class as - * described for - * Gson. - * - *

Here is an example of an implementation that accesses the {@code messageId} property from - * a payload that matches a user-defined {@code PubSubMessage} class: - * - *

- * public class Example implements {@code BackgroundFunction} {
- *   private static final Logger logger = Logger.getLogger(Example.class.getName());
- *
- *   {@code @Override}
- *   public void accept(PubSubMessage pubSubMessage, Context context) {
- *     logger.info("Got messageId " + pubSubMessage.messageId);
- *   }
- * }
- *
- * // Where PubSubMessage is a user-defined class like this:
- * public class PubSubMessage {
- *   String data;
- *   {@code Map} attributes;
- *   String messageId;
- *   String publishTime;
- * }
- * 
- * - * @param the class of payload objects that this function expects. - */ -@FunctionalInterface -public interface BackgroundFunction { - /** - * Called to service an incoming event. This interface is implemented by user code to - * provide the action for a given background function. If this method throws any exception - * (including any {@link Error}) then the HTTP response will have a 500 status code. - * - * @param payload the payload of the event, deserialized from the original JSON string. - * @param context the context of the event. This is a set of values that every event has, - * separately from the payload, such as timestamp and event type. - */ - void accept(T payload, Context context); -} diff --git a/functions-framework-api/src/main/java/com/google/cloud/functions/RawBackgroundFunction.java b/functions-framework-api/src/main/java/com/google/cloud/functions/RawBackgroundFunction.java deleted file mode 100644 index 472f7c4d..00000000 --- a/functions-framework-api/src/main/java/com/google/cloud/functions/RawBackgroundFunction.java +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.cloud.functions; - -/** - * Represents a Cloud Function that is activated by an event. The payload of the event is a JSON - * object, which can be parsed using a JSON package such as - * GSON. - * - *

Here is an example of an implementation that parses the JSON payload using Gson, to access its - * {@code messageId} property: - * - *

- * public class Example implements RawBackgroundFunction {
- *   private static final Logger logger = Logger.getLogger(Example.class.getName());
- *
- *   {@code @Override}
- *   public void accept(String json, Context context) {
- *     JsonObject jsonObject = new Gson().fromJson(json, JsonObject.class);
- *     JsonElement messageId = jsonObject.get("messageId");
- *     String messageIdString = messageId.getAsJsonString();
- *     logger.info("Got messageId " + messageIdString);
- *   }
- * }
- * 
- * - *

Here is an example of an implementation that deserializes the JSON payload into a Java - * object for simpler access, again using Gson: - * - *

- * public class Example implements RawBackgroundFunction {
- *   private static final Logger logger = Logger.getLogger(Example.class.getName());
- *
- *   {@code @Override}
- *   public void accept(String json, Context context) {
- *     PubSubMessage message = new Gson().fromJson(json, PubSubMessage.class);
- *     logger.info("Got messageId " + message.messageId);
- *   }
- * }
- *
- * // Where PubSubMessage is a user-defined class like this:
- * public class PubSubMessage {
- *   String data;
- *   {@code Map} attributes;
- *   String messageId;
- *   String publishTime;
- * }
- * 
- */ -@FunctionalInterface -public interface RawBackgroundFunction { - /** - * Called to service an incoming event. This interface is implemented by user code to - * provide the action for a given background function. If this method throws any exception - * (including any {@link Error}) then the HTTP response will have a 500 status code. - * - * @param json the payload of the event, as a JSON string. - * @param context the context of the event. This is a set of values that every event has, - * separately from the payload, such as timestamp and event type. - */ - void accept(String json, Context context); -} diff --git a/invoker-core/pom.xml b/invoker-core/pom.xml new file mode 100644 index 00000000..6843c19d --- /dev/null +++ b/invoker-core/pom.xml @@ -0,0 +1,158 @@ + + 4.0.0 + com.google.cloud.functions.invoker + java-function-invoker-core + 1.0.0-alpha-1 + + + UTF-8 + 3.8.0 + 5.3.2 + + + + + com.google.cloud.functions + functions-framework-api + 1.0.0-alpha-2-rc1 + + + javax.servlet + javax.servlet-api + 4.0.1 + + + javax.annotation + javax.annotation-api + 1.3.2 + + + com.google.code.gson + gson + 2.8.5 + + + com.ryanharter.auto.value + auto-value-gson + 0.8.0 + + + com.ryanharter.auto.value + auto-value-gson-annotations + 0.8.0 + + + com.squareup + javapoet + 1.11.1 + + + com.google.auto.value + auto-value + 1.6.2 + + + com.google.auto.value + auto-value-annotations + 1.6.2 + + + org.eclipse.jetty + jetty-servlet + 9.4.11.v20180605 + + + org.eclipse.jetty + jetty-server + 9.4.11.v20180605 + + + commons-cli + commons-cli + 1.4 + + + + + org.mockito + mockito-core + 2.21.0 + test + + + org.mockito + mockito-junit-jupiter + 2.23.0 + test + + + junit + junit + 4.12 + test + + + com.google.truth + truth + 1.0 + test + + + com.google.truth.extensions + truth-java8-extension + 1.0 + test + + + org.eclipse.jetty + jetty-client + 9.4.11.v20180605 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + 1.8 + 1.8 + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + package + + shade + + + + + com + shaded.com.google.cloud.functions + + com.google.cloud.functions.** + com.google.gson.** + + + + + + + com.google.cloud.functions.invoker.runner.Invoker + + + + + + + + + diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/BackgroundCloudFunction.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/BackgroundCloudFunction.java new file mode 100644 index 00000000..12e1c92f --- /dev/null +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/BackgroundCloudFunction.java @@ -0,0 +1,39 @@ +package com.google.cloud.functions.invoker; + +import com.google.cloud.functions.Context; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** Wrapper for user background function. */ +public class BackgroundCloudFunction extends CloudFunction { + + BackgroundCloudFunction(Object o, Method m) { + super(o, m); + } + + void execute(JsonElement dataJson, Context context) throws InvocationTargetException { + + Object data; + Class dataParameterType = functionMethod.getParameterTypes()[0]; + if (dataParameterType == JsonElement.class) { + data = dataJson; + } else { + Gson gson = new Gson(); + try { + data = gson.fromJson(dataJson, dataParameterType); + } catch (JsonParseException e) { + throw new RuntimeException("Could not parse received event payload into type " + + dataParameterType.getCanonicalName(), e); + } + } + + if (functionMethod.getParameterCount() == 2) { + rawExecute(data, context); + } else { + rawExecute(data); + } + } +} diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java new file mode 100644 index 00000000..7aa1cf6a --- /dev/null +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java @@ -0,0 +1,45 @@ +package com.google.cloud.functions.invoker; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import java.io.BufferedReader; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** Executes the user's background function. */ +public class BackgroundFunctionExecutor extends HttpServlet { + + private final BackgroundCloudFunction function; + + public BackgroundFunctionExecutor(BackgroundCloudFunction function) { + this.function = function; + } + + /** Executes the user's background function, can handle all HTTP type methods. */ + @Override + public void service(HttpServletRequest req, HttpServletResponse res) throws IOException { + BufferedReader body = req.getReader(); + + // A Type Adapter is required to set the type of the JsonObject because CloudFunctionsContext + // is abstract and Gson default behavior instantiates the type provided. + TypeAdapter typeAdapter = + CloudFunctionsContext.typeAdapter(new Gson()); + Gson gson = new GsonBuilder() + .registerTypeAdapter(CloudFunctionsContext.class, typeAdapter) + .registerTypeAdapter(Event.class, new Event.EventDeserializer()) + .create(); + + Event event = gson.fromJson(body, Event.class); + try { + function.execute(event.getData(), event.getContext()); + res.setStatus(HttpServletResponse.SC_OK); + } catch (InvocationTargetException e) { + res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + e.getCause().printStackTrace(); + } + } +} diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionSignatureMatcher.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionSignatureMatcher.java new file mode 100644 index 00000000..1b7089b7 --- /dev/null +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionSignatureMatcher.java @@ -0,0 +1,45 @@ +package com.google.cloud.functions.invoker; + +import com.google.cloud.functions.Context; +import java.lang.reflect.Method; + +/** + * Implements {@link FunctionSignatureMatcher} for background functions. A valid background + * functions has a first parameter that can be of any type (JSON payload is deserialized into this + * parameter), and an optional second parameter of type {@link Context}. + */ +public class BackgroundFunctionSignatureMatcher + implements FunctionSignatureMatcher { + + @Override + public BackgroundCloudFunction match( + Class functionClass, + Object functionInstance, + String functionMethodName, + String functionTarget) { + + for (Method method : functionClass.getMethods()) { + if (method.getName().equals(functionMethodName)) { + switch (method.getParameterCount()) { + case 1: + break; + case 2: + if (method.getParameterTypes()[1] == Context.class) { + break; + } + continue; + default: + continue; + } + } + return new BackgroundCloudFunction(functionInstance, method); + } + throw new RuntimeException( + String.format( + "%1$s didn't match any of the supported handler signatures for background functions." + + " Expected method signature of the form:" + + " %1$s(com.google.gson.JsonElement [, com.google.functions.Context]) or" + + " %1$s(your.CustomType [, com.google.functions.Context])", + functionTarget)); + } +} diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/CloudFunction.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/CloudFunction.java new file mode 100644 index 00000000..29bd62a8 --- /dev/null +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/CloudFunction.java @@ -0,0 +1,30 @@ +package com.google.cloud.functions.invoker; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * A container for user function method and an instance of the associated class. + * It allows to execute the function via {@link #rawExecute(Object...)}. + */ +class CloudFunction { + final Object functionObject; + final Method functionMethod; + + CloudFunction(Object functionObject, Method functionMethod) { + this.functionObject = functionObject; + this.functionMethod = functionMethod; + + if (!functionMethod.getDeclaringClass().isAssignableFrom(functionObject.getClass())) { + throw new RuntimeException("Internal error: function object and method type mismatch"); + } + } + + Object rawExecute(Object... args) throws InvocationTargetException { + try { + return functionMethod.invoke(functionObject, args); + } catch (IllegalAccessException e) { + throw new RuntimeException("Could not access function method: is it public?", e); + } + } +} diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/CloudFunctionsContext.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/CloudFunctionsContext.java new file mode 100644 index 00000000..0a2def52 --- /dev/null +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/CloudFunctionsContext.java @@ -0,0 +1,27 @@ +package com.google.cloud.functions.invoker; + +import com.google.auto.value.AutoValue; +import com.google.cloud.functions.Context; +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import javax.annotation.Nullable; + +/** Event context (metadata) for events handled by Cloud Functions. */ +@AutoValue +abstract class CloudFunctionsContext implements Context { + @Nullable + public abstract String eventId(); + + @Nullable + public abstract String timestamp(); + + @Nullable + public abstract String eventType(); + + @Nullable + public abstract String resource(); + + public static TypeAdapter typeAdapter(Gson gson) { + return new AutoValue_CloudFunctionsContext.GsonTypeAdapter(gson); + } +} diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/Event.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/Event.java new file mode 100644 index 00000000..c6fb7a7a --- /dev/null +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/Event.java @@ -0,0 +1,72 @@ +package com.google.cloud.functions.invoker; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import java.lang.reflect.Type; + +/** + * Represents an event that should be handled by a background function. This is an internal format + * which is later converted to actual background function parameter types. + */ +class Event { + + private JsonElement data; + private CloudFunctionsContext context; + + Event(JsonElement data, CloudFunctionsContext context) { + this.data = data; + this.context = context; + } + + JsonElement getData() { + return data; + } + + CloudFunctionsContext getContext() { + return context; + } + + /** Custom deserializer that supports both GCF beta and GCF GA event formats. */ + static class EventDeserializer implements JsonDeserializer { + + @Override + public Event deserialize( + JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) + throws JsonParseException { + JsonObject root = jsonElement.getAsJsonObject(); + + JsonElement data = root.get("data"); + CloudFunctionsContext context; + + if (root.has("context")) { + JsonObject contextCopy = root.getAsJsonObject("context").deepCopy(); + context = + jsonDeserializationContext.deserialize( + adjustContextResource(contextCopy), CloudFunctionsContext.class); + } else { + JsonObject rootCopy = root.deepCopy(); + rootCopy.remove("data"); + context = + jsonDeserializationContext.deserialize( + adjustContextResource(rootCopy), CloudFunctionsContext.class); + } + return new Event(data, context); + } + + /** + * Replaces 'resource' member from context JSON with its string equivalent. The original + * 'resource' member can be a JSON object itself while {@link CloudFunctionsContext} requires it + * to be a string. + */ + private JsonObject adjustContextResource(JsonObject contextObject) { + String resourceValue = + contextObject.has("resource") ? contextObject.get("resource").toString() : ""; + contextObject.remove("resource"); + contextObject.addProperty("resource", resourceValue); + return contextObject; + } + } +} diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/FunctionLoader.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/FunctionLoader.java new file mode 100644 index 00000000..582f4936 --- /dev/null +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/FunctionLoader.java @@ -0,0 +1,55 @@ +package com.google.cloud.functions.invoker; + +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Optional; + +/** + * Dynamically loads the user's function class and returns an instance of {@link CloudFunction}. + */ +public class FunctionLoader { + + private final String functionTarget; + private final FunctionSignatureMatcher matcher; + private final Optional userJarFile; + + public FunctionLoader( + String functionTarget, + Optional userJarFile, + FunctionSignatureMatcher matcher) { + this.functionTarget = functionTarget; + this.matcher = matcher; + this.userJarFile = userJarFile; + } + + /** + * Tries to dynamically load the class containing user function using a class loader. + * Automatically determines function signature type from the method signature and returns an + * instance of either {@link HttpCloudFunction} or {@link BackgroundCloudFunction}. + */ + public T loadUserFunction() throws Exception { + int lastDotIndex = functionTarget.lastIndexOf("."); + if (lastDotIndex == -1) { + throw new RuntimeException( + "Expected target of format .., but got " + functionTarget); + } + String targetClassName = functionTarget.substring(0, lastDotIndex); + String targetMethodName = functionTarget.substring(lastDotIndex + 1); + + ClassLoader classLoader; + + if (userJarFile.isPresent()) { + classLoader = + new URLClassLoader( + new URL[]{userJarFile.get().toURI().toURL()}, + Thread.currentThread().getContextClassLoader()); + } else { + classLoader = Thread.currentThread().getContextClassLoader(); + } + Class targetClass = classLoader.loadClass(targetClassName); + + Object targetInstance = targetClass.getDeclaredConstructor().newInstance(); + return matcher.match(targetClass, targetInstance, targetMethodName, functionTarget); + } +} diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/FunctionSignatureMatcher.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/FunctionSignatureMatcher.java new file mode 100644 index 00000000..fd1b5477 --- /dev/null +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/FunctionSignatureMatcher.java @@ -0,0 +1,27 @@ +package com.google.cloud.functions.invoker; + + +/** + * An interface for classes that contain logic for matching user function method signature with + * one of the supported signatures. + * + * @param subtype of CloudFunction that is matched + */ +public interface FunctionSignatureMatcher { + + /** + * Matches user function method with given name with the expected signature. + * + * @param functionClass user function class + * @param functionInstance instance of user function class + * @param functionMethodName name of the user function method specified in function target + * @param functionTarget + * @return instance of T if successfully matched + */ + T match( + Class functionClass, + Object functionInstance, + String functionMethodName, + String functionTarget) + throws RuntimeException; +} diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/HttpCloudFunction.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/HttpCloudFunction.java new file mode 100644 index 00000000..85b0808e --- /dev/null +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/HttpCloudFunction.java @@ -0,0 +1,19 @@ +package com.google.cloud.functions.invoker; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** Wrapper for user HTTP function. */ +public class HttpCloudFunction extends CloudFunction { + + public HttpCloudFunction(Object o, Method m) { + super(o, m); + } + + public void execute(HttpServletRequest req, HttpServletResponse res) + throws InvocationTargetException { + rawExecute(req, res); + } +} diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionExecutor.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionExecutor.java new file mode 100644 index 00000000..8c4ab8b5 --- /dev/null +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionExecutor.java @@ -0,0 +1,27 @@ +package com.google.cloud.functions.invoker; + +import java.lang.reflect.InvocationTargetException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** Executes the user's method. */ +public class HttpFunctionExecutor extends HttpServlet { + private final HttpCloudFunction function; + + public HttpFunctionExecutor(HttpCloudFunction function) { + this.function = function; + } + + /** Executes the user's method, can handle all HTTP type methods. */ + @Override + public void service(HttpServletRequest req, HttpServletResponse res) { + URLRequestWrapper wrapper = new URLRequestWrapper(req); + try { + function.execute(wrapper, res); + } catch (InvocationTargetException e) { + res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + e.getCause().printStackTrace(); + } + } +} diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionSignatureMatcher.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionSignatureMatcher.java new file mode 100644 index 00000000..ca55b1b0 --- /dev/null +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionSignatureMatcher.java @@ -0,0 +1,35 @@ +package com.google.cloud.functions.invoker; + +import java.lang.reflect.Method; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Implements {@link FunctionSignatureMatcher} for HTTP functions with signature: + * void function(HttpServletRequest req, HttpServletResponse res); + */ +public class HttpFunctionSignatureMatcher implements FunctionSignatureMatcher { + + @Override + public HttpCloudFunction match( + Class functionClass, + Object functionInstance, + String functionMethodName, + String functionTarget) { + Method functionMethod; + try { + functionMethod = + functionClass.getMethod( + functionMethodName, HttpServletRequest.class, HttpServletResponse.class); + } catch (NoSuchMethodException e) { + throw new RuntimeException( + String.format( + "%1$s didn't match any of the supported handler signatures for HTTP functions." + + " Expected method signature of the form:" + + " %1$s(javax.servlet.http.HttpServletRequest," + + " javax.servlet.http.HttpServletResponse)", + functionTarget)); + } + return new HttpCloudFunction(functionInstance, functionMethod); + } +} diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/URLRequestWrapper.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/URLRequestWrapper.java new file mode 100644 index 00000000..6a0299d1 --- /dev/null +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/URLRequestWrapper.java @@ -0,0 +1,26 @@ +package com.google.cloud.functions.invoker; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; + +/** Removes the servlet path from the request URL seen by the client. */ +class URLRequestWrapper extends HttpServletRequestWrapper { + private final String newValue; + + URLRequestWrapper(HttpServletRequest req) { + super(req); + if (req.getRequestURL() != null && req.getServletPath() != null) { + this.newValue = req.getRequestURL().toString().replaceFirst(req.getServletPath(), ""); + } else { + this.newValue = null; + } + } + + @Override + public StringBuffer getRequestURL() { + if (newValue == null) { + return super.getRequestURL(); + } + return new StringBuffer(newValue); + } +} diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/http/HttpRequestImpl.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/http/HttpRequestImpl.java new file mode 100644 index 00000000..4eacb7f2 --- /dev/null +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/http/HttpRequestImpl.java @@ -0,0 +1,164 @@ +package com.google.cloud.functions.invoker.http; + +import static java.util.stream.Collectors.toMap; + +import com.google.cloud.functions.HttpRequest; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.Part; + +public class HttpRequestImpl implements HttpRequest { + private final HttpServletRequest request; + + public HttpRequestImpl(HttpServletRequest request) { + this.request = request; + } + + @Override + public String getMethod() { + return request.getMethod(); + } + + @Override + public String getUri() { + String url = request.getRequestURL().toString(); + if (request.getQueryString() != null) { + url += "?" + request.getQueryString(); + } + return url; + } + + @Override + public String getPath() { + return request.getRequestURI(); + } + + @Override + public Optional getQuery() { + return Optional.ofNullable(request.getQueryString()); + } + + @Override + public Map> getQueryParameters() { + return request.getParameterMap().entrySet().stream() + .collect(toMap(Map.Entry::getKey, e -> Arrays.asList(e.getValue()))); + } + + @Override + public Map getParts() { + String contentType = request.getContentType(); + if (contentType == null || !request.getContentType().startsWith("multipart/form-data")) { + throw new IllegalStateException("Content-Type must be multipart/form-data: " + contentType); + } + try { + return request.getParts().stream().collect(toMap(Part::getName, HttpPartImpl::new)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } catch (ServletException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + @Override + public Optional getContentType() { + return Optional.ofNullable(request.getContentType()); + } + + @Override + public long getContentLength() { + return request.getContentLength(); + } + + @Override + public Optional getCharacterEncoding() { + return Optional.ofNullable(request.getCharacterEncoding()); + } + + @Override + public InputStream getInputStream() throws IOException { + return request.getInputStream(); + } + + @Override + public BufferedReader getReader() throws IOException { + return request.getReader(); + } + + @Override + public Map> getHeaders() { + return Collections.list(request.getHeaderNames()).stream() + .map(name -> new SimpleEntry<>(name, Collections.list(request.getHeaders(name)))) + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private static class HttpPartImpl implements HttpPart { + private final Part part; + + private HttpPartImpl(Part part) { + this.part = part; + } + + @Override + public Optional getFileName() { + return Optional.ofNullable(part.getSubmittedFileName()); + } + + @Override + public Optional getContentType() { + return Optional.ofNullable(part.getContentType()); + } + + @Override + public long getContentLength() { + return part.getSize(); + } + + @Override + public Optional getCharacterEncoding() { + String contentType = getContentType().orElse(null); + if (contentType == null) { + return Optional.empty(); + } + Pattern charsetPattern = Pattern.compile("(?i).*;\\s*charset\\s*=([^;\\s]*)\\s*(;|$)"); + Matcher matcher = charsetPattern.matcher(contentType); + return matcher.matches() ? Optional.of(matcher.group(1)) : Optional.empty(); + } + + @Override + public InputStream getInputStream() throws IOException { + return part.getInputStream(); + } + + @Override + public BufferedReader getReader() throws IOException { + String encoding = getCharacterEncoding().orElse("utf-8"); + return new BufferedReader(new InputStreamReader(getInputStream(), encoding)); + } + + @Override + public Map> getHeaders() { + return part.getHeaderNames().stream() + .map(name -> new SimpleEntry<>(name, list(part.getHeaders(name)))) + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private static List list(Collection collection) { + return (collection instanceof List) ? (List) collection : new ArrayList<>(collection); + } + } +} diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java new file mode 100644 index 00000000..93deb2b1 --- /dev/null +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java @@ -0,0 +1,89 @@ +package com.google.cloud.functions.invoker.http; + +import static java.util.stream.Collectors.toMap; + +import com.google.cloud.functions.HttpResponse; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Writer; +import java.lang.reflect.Field; +import java.util.AbstractMap.SimpleEntry; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import javax.servlet.http.HttpServletResponse; + +public class HttpResponseImpl implements HttpResponse { + private final HttpServletResponse response; + + public HttpResponseImpl(HttpServletResponse response) { + this.response = response; + } + + @Override + public void setStatusCode(int code) { + response.setStatus(code); + } + + @Override + @SuppressWarnings("deprecation") + public void setStatusCode(int code, String message) { + response.setStatus(code, message); + } + + @Override + public void setContentType(String contentType) { + response.setContentType(contentType); + } + + @Override + public Optional getContentType() { + return Optional.ofNullable(response.getContentType()); + } + + @Override + public void appendHeader(String key, String value) { + response.addHeader(key, value); + } + + @Override + public Map> getHeaders() { + return response.getHeaderNames().stream() + .map(header -> new SimpleEntry<>(header, list(response.getHeaders(header)))) + .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private static List list(Collection collection) { + return (collection instanceof List) ? (List) collection : new ArrayList<>(collection); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return response.getOutputStream(); + } + + @Override + public BufferedWriter getWriter() throws IOException { + // We could just wrap a BufferedWriter around the PrintWriter that the Servlet API gives us, + // but this slightly clunky alternative potentially avoids two intermediate objects in the + // writer chain. + PrintWriter printWriter = response.getWriter(); + Writer wrappedWriter; + try { + // This is a protected field, so it is part of the documented API and we know it will be + // there, but we need to use reflection to get at it. + Field outField = PrintWriter.class.getDeclaredField("out"); + outField.setAccessible(true); + wrappedWriter = (Writer) outField.get(printWriter); + } catch (ReflectiveOperationException e) { + throw new IOException("Reflection failed", e); + } + return (wrappedWriter instanceof BufferedWriter) + ? (BufferedWriter) wrappedWriter + : new BufferedWriter(wrappedWriter); + } +} diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java new file mode 100644 index 00000000..95ae3bab --- /dev/null +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java @@ -0,0 +1,175 @@ +package com.google.cloud.functions.invoker.runner; + +import com.google.cloud.functions.invoker.BackgroundCloudFunction; +import com.google.cloud.functions.invoker.BackgroundFunctionExecutor; +import com.google.cloud.functions.invoker.BackgroundFunctionSignatureMatcher; +import com.google.cloud.functions.invoker.FunctionLoader; +import com.google.cloud.functions.invoker.HttpCloudFunction; +import com.google.cloud.functions.invoker.HttpFunctionExecutor; +import com.google.cloud.functions.invoker.HttpFunctionSignatureMatcher; +import java.io.File; +import java.io.IOException; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; + +/** + * Java server that runs the user's code (a jar file) on HTTP request and an HTTP response is sent + * once the user's function is completed. The server accepts HTTP requests at '/' for executing the + * user's function, handles all HTTP methods. + * + *

This class requires the following environment variables: + * + *

    + *
  • PORT - defines the port on which this server listens to HTTP requests. + *
  • FUNCTION_TARGET - defines the name of the method within user's class to execute. + *
  • FUNCTION_SIGNATURE_TYPE - determines whether the loaded code defines an HTTP or event + * function. + *
+ */ +public class Invoker { + + private static final Logger logger; + + static { + try { + LogManager.getLogManager() + .readConfiguration(Invoker.class.getResourceAsStream("/logging.properties")); + } catch (IOException e) { + System.out.println("Failed to read logging configuration file, error: " + e.getMessage()); + } + logger = Logger.getLogger(Invoker.class.getName()); + } + + public Invoker( + Integer port, + String functionTarget, + String functionSignatureType, + Optional functionJarPath) { + this.port = port; + this.functionTarget = functionTarget; + this.functionSignatureType = functionSignatureType; + this.functionJarPath = functionJarPath; + } + + public static void main(String[] args) throws Exception { + + CommandLine line = parseCommandLineOptions(args); + + int port = + Integer.parseInt( + firstNonNull(line.getOptionValue("port"), System.getenv("PORT"), "8080")); + String functionTarget = + firstNonNull( + line.getOptionValue("target"), + System.getenv("FUNCTION_TARGET"), + "TestFunction.function"); + Optional functionJarPath = + isLocalRun() + ? Optional.of( + firstNonNull(line.getOptionValue("jar"), System.getenv("FUNCTION_JAR"))) + : Optional.empty(); + + Invoker invoker = + new Invoker( + port, + functionTarget, + // TODO: remove once function signature type is inferred from the method signature. + System.getenv("FUNCTION_SIGNATURE_TYPE"), + functionJarPath); + invoker.startServer(); + } + + private static boolean isLocalRun() { + return System.getenv("K_SERVICE") == null; + } + + private static CommandLine parseCommandLineOptions(String[] args) { + CommandLineParser parser = new DefaultParser(); + Options options = new Options(); + options.addOption("port", true, "the port on which server listens to HTTP requests"); + options.addOption("target", true, "fully qualified name of the target method to execute"); + options.addOption("jar", true, "path to function jar"); + + try { + CommandLine line = parser.parse(options, args); + return line; + } catch (ParseException e) { + logger.log(Level.SEVERE, "Failed to parse command line options", e); + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp("Invoker", options); + System.exit(1); + } + return null; + } + + @SafeVarargs + private static T firstNonNull(T... objects) { + for (T t : objects) { + if (t != null) { + return t; + } + } + return null; + } + + private final Integer port; + private final String functionTarget; + private final String functionSignatureType; + private final Optional functionJarPath; + + public void startServer() throws Exception { + Server server = new Server(port); + + ServletContextHandler context = new ServletContextHandler(); + context.setContextPath("/"); + server.setHandler(context); + + Optional functionJarFile = + functionJarPath.isPresent() + ? Optional.of(new File(functionJarPath.get())) + : Optional.empty(); + if (functionJarFile.isPresent() && !functionJarFile.get().exists()) { + throw new IllegalArgumentException( + "functionJarPath points to an non-existing file: " + + functionJarFile.get().getAbsolutePath()); + } + + if ("http".equals(functionSignatureType)) { + FunctionLoader loader = + new FunctionLoader<>(functionTarget, functionJarFile, new HttpFunctionSignatureMatcher()); + HttpCloudFunction function = loader.loadUserFunction(); + context.addServlet(new ServletHolder(new HttpFunctionExecutor(function)), "/*"); + } else if ("event".equals(functionSignatureType)) { + FunctionLoader loader = + new FunctionLoader<>( + functionTarget, functionJarFile, new BackgroundFunctionSignatureMatcher()); + BackgroundCloudFunction function = loader.loadUserFunction(); + context.addServlet(new ServletHolder(new BackgroundFunctionExecutor(function)), "/*"); + } else { + throw new RuntimeException("Unknown function signature type: " + functionSignatureType); + } + + server.start(); + logServerInfo(); + server.join(); + } + + private void logServerInfo() { + if (isLocalRun()) { + logger.log(Level.INFO, "Serving function..."); + logger.log(Level.INFO, "Function: {0}", functionTarget); + logger.log(Level.INFO, "URL: http://localhost:{0,number,#}/", port); + } + } +} diff --git a/invoker-core/src/main/resources/logging.properties b/invoker-core/src/main/resources/logging.properties new file mode 100644 index 00000000..2f2ea883 --- /dev/null +++ b/invoker-core/src/main/resources/logging.properties @@ -0,0 +1,4 @@ +handlers = java.util.logging.ConsoleHandler +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.ConsoleHandler.level = ALL +java.util.logging.SimpleFormatter.format = %4$s: %5$s%n \ No newline at end of file diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/BackgroundFunctionTest.java b/invoker-core/src/test/java/com/google/cloud/functions/invoker/BackgroundFunctionTest.java new file mode 100644 index 00000000..a717391a --- /dev/null +++ b/invoker-core/src/test/java/com/google/cloud/functions/invoker/BackgroundFunctionTest.java @@ -0,0 +1,88 @@ +package com.google.cloud.functions.invoker; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.google.cloud.functions.Context; +import com.google.gson.JsonElement; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.Arrays; +import java.util.Collection; +import java.util.Optional; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; +import org.mockito.Mockito; + +@RunWith(Parameterized.class) +public class BackgroundFunctionTest { + + @Parameters + public static Collection data() { + return Arrays.asList(new Object[][]{ + {"JsonAdder.add", "/adder_gcf_ga_event.json"}, + {"CustomTypeAdder.add", "/adder_gcf_ga_event.json"}, + {"JsonAdder.add", "/adder_gcf_beta_event.json"}, + {"CustomTypeAdder.add", "/adder_gcf_beta_event.json"}, + {"JsonAdder.add", "/adder_gcf_beta_event_json_resource.json"}, + {"CustomTypeAdder.add", "/adder_gcf_beta_event_json_resource.json"}, + }); + } + + @Parameter(0) + public String target; + + @Parameter(1) + public String eventFilePath; + + private static int lastSum = 0; + private static String lastEventType = ""; + + public static class JsonAdder { + + public void add(JsonElement data, Context context) { + lastSum = + data.getAsJsonObject().get("a").getAsInt() + data.getAsJsonObject().get("b").getAsInt(); + lastEventType = context.eventType(); + } + } + + public static class CustomTypeAdder { + + public static class Data { + + int a; + int b; + } + + public void add(Data data, Context context) { + lastSum = data.a + data.b; + lastEventType = context.eventType(); + } + } + + @Test + public void adder() throws Exception { + HttpServletRequest req = Mockito.mock(HttpServletRequest.class); + HttpServletResponse res = Mockito.mock(HttpServletResponse.class); + String fullTarget = "com.google.cloud.functions.invoker.BackgroundFunctionTest$" + target; + FunctionLoader loader = + new FunctionLoader<>(fullTarget, Optional.empty(), + new BackgroundFunctionSignatureMatcher()); + BackgroundCloudFunction function = loader.loadUserFunction(); + BackgroundFunctionExecutor executor = new BackgroundFunctionExecutor(function); + Mockito.when(req.getReader()) + .thenReturn( + new BufferedReader( + new InputStreamReader(this.getClass().getResourceAsStream(eventFilePath)))); + + executor.service(req, res); + + assertEquals(lastSum, 5); + assertEquals(lastEventType, "com.example.someevent.new"); + } +} diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/HttpFunctionTest.java b/invoker-core/src/test/java/com/google/cloud/functions/invoker/HttpFunctionTest.java new file mode 100644 index 00000000..4423dfec --- /dev/null +++ b/invoker-core/src/test/java/com/google/cloud/functions/invoker/HttpFunctionTest.java @@ -0,0 +1,57 @@ +package com.google.cloud.functions.invoker; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Optional; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.Test; +import org.mockito.Mockito; + +public class HttpFunctionTest { + + private static String lastResponse = ""; + + public static class HttpWriter { + + public void writeResponse(HttpServletRequest request, HttpServletResponse response) + throws IOException { + PrintWriter writer = response.getWriter(); + writer.write(request.getParameter("data")); + } + } + + @Test + public void adder() throws Exception { + HttpServletRequest req = Mockito.mock(HttpServletRequest.class); + HttpServletResponse res = Mockito.mock(HttpServletResponse.class); + String fullTarget = + "com.google.cloud.functions.invoker.HttpFunctionTest$HttpWriter.writeResponse"; + String requestData = "testData"; + FunctionLoader loader = + new FunctionLoader<>(fullTarget, Optional.empty(), new HttpFunctionSignatureMatcher()); + HttpCloudFunction function = loader.loadUserFunction(); + HttpFunctionExecutor executor = new HttpFunctionExecutor(function); + Mockito.when(req.getParameter("data")).thenReturn(requestData); + + Mockito.when(res.getWriter()).thenReturn(new MockPrintWriter("fooFile")); + + executor.service(req, res); + + assertEquals(lastResponse, requestData); + } + + private class MockPrintWriter extends java.io.PrintWriter { + + public MockPrintWriter(String s) throws FileNotFoundException { + super(s); + } + + public void write(String s) { + lastResponse = s; + } + } +} diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java b/invoker-core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java new file mode 100644 index 00000000..01c933fc --- /dev/null +++ b/invoker-core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java @@ -0,0 +1,220 @@ +package com.google.cloud.functions.invoker; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import com.google.auto.value.AutoValue; +import com.google.cloud.functions.invoker.runner.Invoker; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Files; +import com.google.common.io.Resources; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.net.ServerSocket; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; +import org.junit.BeforeClass; +import org.junit.Test; + +/** + * Integration test that starts up a web server running the Function Framework and sends HTTP + * requests to it. + */ +public class IntegrationTest { + + private static final String SERVER_READY_STRING = "Started ServerConnector"; + + private static int serverPort; + + /** + * Each test method will start up a server on the same port, make one or more HTTP requests to + * that port, then kill the server. So the port should be free when the next test method runs. + */ + @BeforeClass + public static void allocateServerPort() throws IOException { + ServerSocket serverSocket = new ServerSocket(0); + serverPort = serverSocket.getLocalPort(); + serverSocket.close(); + } + + /** + * Description of a test case. When we send an HTTP POST to the given {@link #url()} in the + * server, with the given {@link #requestText()} as the body of the POST, then we expect to get + * back the given {@link #expectedResponseText()} in the body of the response. + */ + @AutoValue + abstract static class TestCase { + + abstract String url(); + + abstract String requestText(); + + abstract String expectedResponseText(); + + static Builder builder() { + return new AutoValue_IntegrationTest_TestCase.Builder() + .setUrl("/") + .setRequestText("") + .setExpectedResponseText(""); + } + + @AutoValue.Builder + abstract static class Builder { + + abstract Builder setUrl(String x); + + abstract Builder setRequestText(String x); + + abstract Builder setExpectedResponseText(String x); + + abstract TestCase build(); + } + } + + @Test + public void helloWorld() throws Exception { + testHttpFunction("HelloWorld.helloWorld", + TestCase.builder().setExpectedResponseText("hello\n").build()); + } + + @Test + public void echo() throws Exception { + String testText = "hello\nworld\n"; + testHttpFunction("Echo.echo", + TestCase.builder().setRequestText(testText).setExpectedResponseText(testText).build()); + } + + @Test + public void echoUrl() throws Exception { + String[] testUrls = {"/", "/foo/bar", "/?foo=bar&baz=buh", "/foo?bar=baz"}; + TestCase[] testCases = Arrays.stream(testUrls) + .map(url -> TestCase.builder().setUrl(url).setExpectedResponseText(url + "\n").build()) + .toArray(TestCase[]::new); + testHttpFunction("EchoUrl.echoUrl", testCases); + } + + @Test + public void background() throws Exception { + URL resourceUrl = getClass().getResource("/adder_gcf_ga_event.json"); + assertThat(resourceUrl).isNotNull(); + File snoopFile = File.createTempFile("FunctionsIntegrationTest", ".txt"); + snoopFile.deleteOnExit(); + String originalJson = Resources.toString(resourceUrl, StandardCharsets.UTF_8); + JsonObject json = new JsonParser().parse(originalJson).getAsJsonObject(); + JsonObject jsonData = json.getAsJsonObject("data"); + jsonData.addProperty("targetFile", snoopFile.toString()); + testBackgroundFunction("BackgroundSnoop.snoop", + TestCase.builder().setRequestText(json.toString()).build()); + String snooped = Files.asCharSource(snoopFile, StandardCharsets.UTF_8).read(); + JsonObject snoopedJson = new JsonParser().parse(snooped).getAsJsonObject(); + assertThat(snoopedJson).isEqualTo(json); + } + + private void testHttpFunction(String classAndMethod, TestCase... testCases) throws Exception { + testFunction(SignatureType.HTTP, classAndMethod, testCases); + } + + private void testBackgroundFunction(String classAndMethod, TestCase... testCases) + throws Exception { + testFunction(SignatureType.BACKGROUND, classAndMethod, testCases); + } + + private void testFunction(SignatureType signatureType, String classAndMethod, + TestCase... testCases) throws Exception { + Process server = startServer(signatureType, classAndMethod); + try { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + for (TestCase testCase : testCases) { + String uri = "http://localhost:" + serverPort + testCase.url(); + Request request = httpClient.POST(uri); + request.header(HttpHeader.CONTENT_TYPE, "text/plain"); + request.content(new StringContentProvider(testCase.requestText())); + ContentResponse response = request.send(); + assertWithMessage("Response to %s is %s %s", uri, response.getStatus(), + response.getReason()) + .that(response.getStatus()).isEqualTo(HttpStatus.OK_200); + assertThat(response.getContentAsString()).isEqualTo(testCase.expectedResponseText()); + } + } finally { + server.destroy(); + server.waitFor(); + } + } + + private enum SignatureType { + HTTP("http"), + BACKGROUND("event"); + + private final String name; + + SignatureType(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + private Process startServer(SignatureType signatureType, String classAndMethod) + throws IOException, InterruptedException { + String fullMethodName = "com.google.cloud.functions.invoker.testfunctions." + classAndMethod; + File javaHome = new File(System.getProperty("java.home")); + assertThat(javaHome.exists()).isTrue(); + File javaBin = new File(javaHome, "bin"); + File javaCommand = new File(javaBin, "java"); + assertThat(javaCommand.exists()).isTrue(); + String myClassPath = System.getProperty("java.class.path"); + assertThat(myClassPath).isNotNull(); + String[] command = { + javaCommand.toString(), "-classpath", myClassPath, Invoker.class.getName(), + }; + ProcessBuilder processBuilder = new ProcessBuilder() + .command(command) + .redirectErrorStream(true); + Map environment = ImmutableMap.of( + "PORT", String.valueOf(serverPort), + "K_SERVICE", "test-function", + "FUNCTION_SIGNATURE_TYPE", signatureType.toString(), + "FUNCTION_TARGET", fullMethodName); + processBuilder.environment().putAll(environment); + Process serverProcess = processBuilder.start(); + CountDownLatch ready = new CountDownLatch(1); + new Thread(() -> monitorOutput(serverProcess.getInputStream(), ready)).start(); + ready.await(5, TimeUnit.SECONDS); + return serverProcess; + } + + private void monitorOutput(InputStream processOutput, CountDownLatch ready) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(processOutput))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.contains(SERVER_READY_STRING)) { + ready.countDown(); + } + System.out.println(line); + } + } catch (IOException e) { + e.printStackTrace(); + throw new UncheckedIOException(e); + } + } +} diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/http/HttpTest.java b/invoker-core/src/test/java/com/google/cloud/functions/invoker/http/HttpTest.java new file mode 100644 index 00000000..548d56b5 --- /dev/null +++ b/invoker-core/src/test/java/com/google/cloud/functions/invoker/http/HttpTest.java @@ -0,0 +1,492 @@ +package com.google.cloud.functions.invoker.http; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static org.junit.Assert.fail; + +import com.google.cloud.functions.HttpRequest; +import com.google.cloud.functions.HttpRequest.HttpPart; +import com.google.cloud.functions.HttpResponse; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import javax.servlet.MultipartConfigElement; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.BytesContentProvider; +import org.eclipse.jetty.client.util.MultiPartContentProvider; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpFields; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.junit.BeforeClass; +import org.junit.Test; + +public class HttpTest { + + private static final String TEST_BODY = + "In the reign of James the Second\n" + + "It was generally reckoned\n" + + "As a rather serious crime\n" + + "To marry two wives at a time.\n"; + + private static final byte[] RANDOM_BYTES = new byte[1024]; + static { + new Random().nextBytes(RANDOM_BYTES); + } + + private static int serverPort; + + /** + * Each test method will start up a server on the same port, make one or more HTTP requests to + * that port, then kill the server. So the port should be free when the next test method runs. + */ + @BeforeClass + public static void allocateServerPort() throws IOException { + ServerSocket serverSocket = new ServerSocket(0); + serverPort = serverSocket.getLocalPort(); + serverSocket.close(); + } + + /** + * Wrapper class that allows us to start a Jetty server with a single servlet for {@code /*} + * within a try-with-resources statement. The servlet will be configured to support multipart + * requests. + */ + private static class SimpleServer implements AutoCloseable { + private final Server server; + + SimpleServer(HttpServlet servlet) throws Exception { + this.server = new Server(serverPort); + ServletContextHandler context = new ServletContextHandler(); + context.setContextPath("/"); + server.setHandler(context); + ServletHolder servletHolder = new ServletHolder(servlet); + servletHolder.getRegistration().setMultipartConfig(new MultipartConfigElement("tiddly")); + context.addServlet(servletHolder, "/*"); + server.start(); + } + + @Override + public void close() throws Exception { + server.stop(); + } + } + + @FunctionalInterface + private interface HttpRequestTest { + void test(HttpRequest request) throws Exception; + } + + /** + * Tests methods on the {@link HttpRequest} object while the request is being serviced. We are + * not guaranteed that the underlying {@link HttpServletRequest} object will still be valid when + * the request completes, and in fact in Jetty it isn't. So we perform the checks in the context + * of the servlet, and report any exception back to the test method. + */ + @Test + public void httpRequestMethods() throws Exception { + AtomicReference testReference = new AtomicReference<>(); + AtomicReference exceptionReference = new AtomicReference<>(); + HttpRequestServlet testServlet = new HttpRequestServlet(testReference, exceptionReference); + try (SimpleServer server = new SimpleServer(testServlet)) { + httpRequestMethods(testReference, exceptionReference); + } + } + + private void httpRequestMethods( + AtomicReference testReference, AtomicReference exceptionReference) + throws Exception { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + String uri = "http://localhost:" + serverPort + "/foo/bar?baz=buh&baz=xxx&blim=blam&baz=what"; + HttpRequestTest[] tests = { + request -> assertThat(request.getMethod()).isEqualTo("POST"), + request -> assertThat(request.getMethod()).isEqualTo("POST"), + request -> assertThat(request.getUri()).isEqualTo(uri), + request -> assertThat(request.getPath()).isEqualTo("/foo/bar"), + request -> assertThat(request.getQuery()).hasValue("baz=buh&baz=xxx&blim=blam&baz=what"), + request -> { + Map> expectedQueryParameters = new TreeMap<>(); + expectedQueryParameters.put("baz", Arrays.asList("buh", "xxx", "what")); + expectedQueryParameters.put("blim", Arrays.asList("blam")); + assertThat(request.getQueryParameters()).isEqualTo(expectedQueryParameters); + }, + request -> assertThat(request.getFirstQueryParameter("baz")).hasValue("buh"), + request -> assertThat(request.getFirstQueryParameter("something")).isEmpty(), + request -> assertThat(request.getContentType().get()).ignoringCase() + .isEqualTo("text/plain; charset=utf-8"), + request -> assertThat(request.getContentLength()).isEqualTo(TEST_BODY.length()), + request -> assertThat(request.getCharacterEncoding()).isPresent(), + request -> assertThat(request.getCharacterEncoding().get()).ignoringCase() + .isEqualTo("utf-8"), + request -> { + try (BufferedReader reader = request.getReader()) { + validateReader(reader); + assertThat(request.getReader()).isSameInstanceAs(reader); + } + try { + request.getInputStream(); + fail("Did not get expected exception"); + } catch (IllegalStateException expected) { + } + }, + request -> { + try (InputStream inputStream = request.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + validateReader(reader); + assertThat(request.getInputStream()).isSameInstanceAs(inputStream); + } + }, + request -> { + Map> expectedHeaders = new TreeMap<>(); + expectedHeaders.put(HttpHeader.CONTENT_LENGTH.asString(), + Arrays.asList(String.valueOf(TEST_BODY.length()))); + expectedHeaders.put("foo", Arrays.asList("bar", "baz")); + assertThat(request.getHeaders()).containsAtLeastEntriesIn(expectedHeaders); + }, + request -> assertThat(request.getFirstHeader("foo")).hasValue("bar"), + request -> { + try { + request.getParts(); + fail("Did not get expected exception"); + } catch (IllegalStateException expected) { + } + } + }; + for (HttpRequestTest test : tests) { + testReference.set(test); + Request request = httpClient.POST(uri) + .header(HttpHeader.CONTENT_TYPE, "text/plain; charset=utf-8") + .header("foo", "bar") + .header("foo", "baz") + .content(new StringContentProvider(TEST_BODY)); + ContentResponse response = request.send(); + assertThat(response.getStatus()).isEqualTo(HttpStatus.OK_200); + throwIfNotNull(exceptionReference.get()); + } + } + + @Test + public void emptyRequest() throws Exception { + HttpClient httpClient = new HttpClient(); + httpClient.start(); + String uri = "http://localhost:" + serverPort; + HttpRequestTest test = request -> { + assertThat(request.getUri()).isEqualTo(uri + "/"); + assertThat(request.getPath()).isEqualTo("/"); + assertThat(request.getQuery()).isEmpty(); + assertThat(request.getQueryParameters()).isEmpty(); + assertThat(request.getContentType()).isEmpty(); + assertThat(request.getContentLength()).isEqualTo(0L); + }; + AtomicReference exceptionReference = new AtomicReference<>(); + AtomicReference testReference = new AtomicReference<>(test); + HttpRequestServlet testServlet = new HttpRequestServlet(testReference, exceptionReference); + try (SimpleServer server = new SimpleServer(testServlet)) { + ContentResponse response = httpClient.POST(uri).send(); + assertThat(response.getStatus()).isEqualTo(HttpStatus.OK_200); + throwIfNotNull(exceptionReference.get()); + } + } + + private void validateReader(BufferedReader reader) { + String text = reader.lines().collect(Collectors.joining("\n", "", "\n")); + assertThat(text).isEqualTo(TEST_BODY); + } + + @Test + public void multiPartRequest() throws Exception { + AtomicReference testReference = new AtomicReference<>(); + AtomicReference exceptionReference = new AtomicReference<>(); + HttpRequestServlet testServlet = new HttpRequestServlet(testReference, exceptionReference); + HttpClient httpClient = new HttpClient(); + httpClient.start(); + String uri = "http://localhost:" + serverPort + "/"; + MultiPartContentProvider multiPart = new MultiPartContentProvider(); + HttpFields textHttpFields = new HttpFields(); + textHttpFields.add("foo", "bar"); + multiPart.addFieldPart("text", new StringContentProvider(TEST_BODY), textHttpFields); + HttpFields bytesHttpFields = new HttpFields(); + bytesHttpFields.add("foo", "baz"); + bytesHttpFields.add("foo", "buh"); + assertThat(bytesHttpFields.getValuesList("foo")).containsExactly("baz", "buh"); + multiPart.addFilePart( + "binary", "/tmp/binary.x", new BytesContentProvider(RANDOM_BYTES), bytesHttpFields); + HttpRequestTest test = request -> { + // The Content-Type header will also have a boundary=something attribute. + assertThat(request.getContentType().get()).startsWith("multipart/form-data"); + assertThat(request.getParts().keySet()).containsExactly("text", "binary"); + HttpPart textPart = request.getParts().get("text"); + assertThat(textPart.getFileName()).isEmpty(); + assertThat(textPart.getContentLength()).isEqualTo(TEST_BODY.length()); + assertThat(textPart.getContentType().get()).startsWith("text/plain"); + assertThat(textPart.getCharacterEncoding()).isPresent(); + assertThat(textPart.getCharacterEncoding().get()).ignoringCase().isEqualTo("utf-8"); + assertThat(textPart.getHeaders()).containsAtLeast("foo", Arrays.asList("bar")); + assertThat(textPart.getFirstHeader("foo")).hasValue("bar"); + validateReader(textPart.getReader()); + HttpPart bytesPart = request.getParts().get("binary"); + assertThat(bytesPart.getFileName()).hasValue("/tmp/binary.x"); + assertThat(bytesPart.getContentLength()).isEqualTo(RANDOM_BYTES.length); + assertThat(bytesPart.getContentType()).hasValue("application/octet-stream"); + // We only see ["buh"] here, not ["baz", "buh"], apparently due to a Jetty bug. + // Repeated headers on multi-part content are not a big problem anyway. + List foos = bytesPart.getHeaders().get("foo"); + assertThat(foos).contains("buh"); + byte[] bytes = new byte[RANDOM_BYTES.length]; + try (InputStream inputStream = bytesPart.getInputStream()) { + assertThat(inputStream.read(bytes)).isEqualTo(bytes.length); + assertThat(inputStream.read()).isEqualTo(-1); + assertThat(bytes).isEqualTo(RANDOM_BYTES); + } + }; + try (SimpleServer server = new SimpleServer(testServlet)) { + testReference.set(test); + Request request = httpClient.POST(uri) + .header("foo", "oof") + .content(multiPart); + ContentResponse response = request.send(); + assertThat(response.getStatus()).isEqualTo(HttpStatus.OK_200); + throwIfNotNull(exceptionReference.get()); + } + } + + private static class HttpRequestServlet extends HttpServlet { + private final AtomicReference testReference; + private final AtomicReference exceptionReference; + + private HttpRequestServlet( + AtomicReference testReference, + AtomicReference exceptionReference) { + this.testReference = testReference; + this.exceptionReference = exceptionReference; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) { + try { + testReference.get().test(new HttpRequestImpl(req)); + } catch (Throwable t) { + exceptionReference.set(t); + } + } + } + + @FunctionalInterface + private interface HttpResponseTest { + void test(HttpResponse response) throws Exception; + } + + /** + * Tests interactions with the {@link HttpResponse} object while the request is still ongoing. + * For example, if we append a header then we should see that header in + * {@link HttpResponse#getHeaders()}. + */ + @Test + public void httpResponseSetAndGet() throws Exception { + AtomicReference testReference = new AtomicReference<>(); + AtomicReference exceptionReference = new AtomicReference<>(); + HttpResponseServlet testServlet = new HttpResponseServlet(testReference, exceptionReference); + try (SimpleServer server = new SimpleServer(testServlet)) { + httpResponseSetAndGet(testReference, exceptionReference); + } + } + + private void httpResponseSetAndGet( + AtomicReference testReference, + AtomicReference exceptionReference) throws Exception { + HttpResponseTest[] tests = { + response -> assertThat(response.getContentType()).isEmpty(), + response -> { + response.setContentType("text/plain; charset=utf-8"); + assertThat(response.getContentType().get()).matches("(?i)text/plain;\\s*charset=utf-8"); + }, + response -> { + response.appendHeader("Content-Type", "application/octet-stream"); + assertThat(response.getContentType()).hasValue("application/octet-stream"); + assertThat(response.getHeaders()) + .containsAtLeast("Content-Type", Arrays.asList("application/octet-stream")); + }, + response -> { + Map> initialHeaders = response.getHeaders(); + // The servlet spec says this should be empty, but actually we get a Date header here. + // So we just check that we can add our own headers. + response.appendHeader("foo", "bar"); + response.appendHeader("wibbly", "wobbly"); + response.appendHeader("foo", "baz"); + Map> updatedHeaders = new TreeMap<>(response.getHeaders()); + updatedHeaders.keySet().removeAll(initialHeaders.keySet()); + assertThat(updatedHeaders).containsExactly( + "foo", Arrays.asList("bar", "baz"), "wibbly", Arrays.asList("wobbly")); + }, + }; + for (HttpResponseTest test : tests) { + testReference.set(test); + HttpClient httpClient = new HttpClient(); + httpClient.start(); + String uri = "http://localhost:" + serverPort; + Request request = httpClient.POST(uri); + ContentResponse response = request.send(); + assertThat(response.getStatus()).isEqualTo(HttpStatus.OK_200); + throwIfNotNull(exceptionReference.get()); + } + } + + private static class HttpResponseServlet extends HttpServlet { + private final AtomicReference testReference; + private final AtomicReference exceptionReference; + + private HttpResponseServlet( + AtomicReference testReference, + AtomicReference exceptionReference) { + this.testReference = testReference; + this.exceptionReference = exceptionReference; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) { + try { + testReference.get().test(new HttpResponseImpl(resp)); + } catch (Throwable t) { + exceptionReference.set(t); + } + } + } + + @FunctionalInterface + private interface ResponseCheck { + void test(ContentResponse response); + } + + private static class ResponseTest { + final HttpResponseTest responseOperation; + final ResponseCheck responseCheck; + + private ResponseTest( + HttpResponseTest responseOperation, + ResponseCheck responseCheck) { + this.responseOperation = responseOperation; + this.responseCheck = responseCheck; + } + } + + private static ResponseTest responseTest( + HttpResponseTest responseOperation, ResponseCheck responseCheck) { + return new ResponseTest(responseOperation, responseCheck); + } + + /** + * Tests that operations on the {@link HttpResponse} have the appropriate effect on the HTTP + * response that ends up being sent. Here, for each check, we have two operations: the operation + * on the {@link HttpResponse}, which happens inside the servlet, and the operation to check the + * HTTP result, which happens in the client thread. + */ + @Test + public void httpResponseEffects() throws Exception { + AtomicReference testReference = new AtomicReference<>(); + AtomicReference exceptionReference = new AtomicReference<>(); + HttpResponseServlet testServlet = new HttpResponseServlet(testReference, exceptionReference); + try (SimpleServer server = new SimpleServer(testServlet)) { + httpResponseEffects(testReference, exceptionReference); + } + } + + private void httpResponseEffects( + AtomicReference testReference, + AtomicReference exceptionReference) throws Exception { + ResponseTest[] tests = { + responseTest( + response -> {}, + response -> assertThat(response.getStatus()).isEqualTo(HttpStatus.OK_200)), + responseTest( + response -> response.setStatusCode(HttpStatus.OK_200), + response -> assertThat(response.getStatus()).isEqualTo(HttpStatus.OK_200)), + responseTest( + response -> response.setStatusCode(HttpStatus.IM_A_TEAPOT_418), + response -> assertThat(response.getStatus()).isEqualTo(HttpStatus.IM_A_TEAPOT_418)), + responseTest( + response -> response.setStatusCode(HttpStatus.IM_A_TEAPOT_418, "Je suis une théière"), + response -> { + assertThat(response.getStatus()).isEqualTo(HttpStatus.IM_A_TEAPOT_418); + assertThat(response.getReason()).isEqualTo("Je suis une théière"); + }), + responseTest( + response -> response.setContentType("application/noddy"), + response -> assertThat(response.getMediaType()).isEqualTo("application/noddy")), + responseTest( + response -> { + response.appendHeader("foo", "bar"); + response.appendHeader("blim", "blam"); + response.appendHeader("foo", "baz"); + }, + response -> { + assertThat(response.getHeaders().getValuesList("foo")).containsExactly("bar", "baz"); + assertThat(response.getHeaders().getValuesList("blim")).containsExactly("blam"); + }), + responseTest( + response -> { + response.setContentType("text/plain"); + try (BufferedWriter writer = response.getWriter()) { + writer.write(TEST_BODY); + } + }, + response -> { + assertThat(response.getMediaType()).isEqualTo("text/plain"); + assertThat(response.getContentAsString()).isEqualTo(TEST_BODY); + }), + responseTest( + response -> { + response.setContentType("application/octet-stream"); + try (OutputStream outputStream = response.getOutputStream()) { + outputStream.write(RANDOM_BYTES); + } + }, + response -> { + assertThat(response.getMediaType()).isEqualTo("application/octet-stream"); + assertThat(response.getContent()).isEqualTo(RANDOM_BYTES); + }), + }; + for (ResponseTest test : tests) { + testReference.set(test.responseOperation); + HttpClient httpClient = new HttpClient(); + httpClient.start(); + String uri = "http://localhost:" + serverPort; + Request request = httpClient.POST(uri); + ContentResponse response = request.send(); + throwIfNotNull(exceptionReference.get()); + test.responseCheck.test(response); + } + } + + private static void throwIfNotNull(Throwable t) throws Exception { + if (t != null) { + if (t instanceof Error) { + throw (Error) t; + } else if (t instanceof Exception) { + throw (Exception) t; + } else { + // Some kind of mutant Throwable that is neither an Exception nor an Error. + throw new AssertionError(t); + } + } + } +} diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/BackgroundSnoop.java b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/BackgroundSnoop.java new file mode 100644 index 00000000..59d22636 --- /dev/null +++ b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/BackgroundSnoop.java @@ -0,0 +1,38 @@ +package com.google.cloud.functions.invoker.testfunctions; + +import com.google.cloud.functions.Context; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; + +/** + * Extract the targetFile property from the data of the JSON payload, and write to it a JSON + * encoding of this payload and the context. The JSON format is chosen to be identical to the + * EventFlow format that we currently use in GCF, and the file that we write should in fact be + * identical to the JSON payload that the Functions Framework received from the client in the test. + * This will need to be rewritten when we switch to CloudEvents. + */ +public class BackgroundSnoop { + public void snoop(JsonElement jsonElement, Context context) throws IOException { + String targetFile = jsonElement.getAsJsonObject().get("targetFile").getAsString(); + if (targetFile == null) { + throw new IllegalArgumentException("Expected targetFile in JSON payload"); + } + JsonObject resourceJson = new JsonParser().parse(context.resource()).getAsJsonObject(); + JsonObject contextJson = new JsonObject(); + contextJson.addProperty("eventId", context.eventId()); + contextJson.addProperty("timestamp", context.timestamp()); + contextJson.addProperty("eventType", context.eventType()); + contextJson.add("resource", resourceJson); + JsonObject contextAndPayloadJson = new JsonObject(); + contextAndPayloadJson.add("data", jsonElement); + contextAndPayloadJson.add("context", contextJson); + try (FileWriter fileWriter = new FileWriter(targetFile); + PrintWriter writer = new PrintWriter(fileWriter)) { + writer.println(contextAndPayloadJson); + } + } +} diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Echo.java b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Echo.java new file mode 100644 index 00000000..dac82632 --- /dev/null +++ b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Echo.java @@ -0,0 +1,15 @@ +package com.google.cloud.functions.invoker.testfunctions; + +import java.io.IOException; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class Echo { + public void echo(HttpServletRequest request, HttpServletResponse response) throws IOException { + String body = request.getReader().lines().collect(Collectors.joining("\n")) + "\n"; + response.setContentType("text/plain"); + response.getWriter().write(body); + response.getWriter().flush(); + } +} diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/EchoUrl.java b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/EchoUrl.java new file mode 100644 index 00000000..c708c80c --- /dev/null +++ b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/EchoUrl.java @@ -0,0 +1,15 @@ +package com.google.cloud.functions.invoker.testfunctions; + +import java.io.IOException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class EchoUrl { + public void echoUrl(HttpServletRequest request, HttpServletResponse response) throws IOException { + String url = request.getRequestURI(); + if (request.getQueryString() != null) { + url += "?" + request.getQueryString(); + } + response.getWriter().println(url); + } +} diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/HelloWorld.java b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/HelloWorld.java new file mode 100644 index 00000000..05c0c3c1 --- /dev/null +++ b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/HelloWorld.java @@ -0,0 +1,12 @@ +package com.google.cloud.functions.invoker.testfunctions; + +import java.io.IOException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class HelloWorld { + public void helloWorld(HttpServletRequest request, HttpServletResponse response) + throws IOException { + response.getWriter().println("hello"); + } +} diff --git a/invoker-core/src/test/resources/adder_gcf_beta_event.json b/invoker-core/src/test/resources/adder_gcf_beta_event.json new file mode 100644 index 00000000..77ce0059 --- /dev/null +++ b/invoker-core/src/test/resources/adder_gcf_beta_event.json @@ -0,0 +1,10 @@ +{ + "eventId": "B234-1234-1234", + "timestamp": "2018-04-05T17:31:00Z", + "eventType": "com.example.someevent.new", + "resource": "/mycontext", + "data": { + "a": 2, + "b": 3 + } +} \ No newline at end of file diff --git a/invoker-core/src/test/resources/adder_gcf_beta_event_json_resource.json b/invoker-core/src/test/resources/adder_gcf_beta_event_json_resource.json new file mode 100644 index 00000000..335b4174 --- /dev/null +++ b/invoker-core/src/test/resources/adder_gcf_beta_event_json_resource.json @@ -0,0 +1,14 @@ +{ + "eventId": "B234-1234-1234", + "timestamp": "2018-04-05T17:31:00Z", + "eventType": "com.example.someevent.new", + "resource": { + "service":"test-service", + "name":"test-name", + "type":"test-type" + }, + "data": { + "a": 2, + "b": 3 + } +} \ No newline at end of file diff --git a/invoker-core/src/test/resources/adder_gcf_ga_event.json b/invoker-core/src/test/resources/adder_gcf_ga_event.json new file mode 100644 index 00000000..4762f613 --- /dev/null +++ b/invoker-core/src/test/resources/adder_gcf_ga_event.json @@ -0,0 +1,16 @@ +{ + "data": { + "a": 2, + "b": 3 + }, + "context": { + "eventId": "B234-1234-1234", + "timestamp": "2018-04-05T17:31:00Z", + "eventType": "com.example.someevent.new", + "resource": { + "service":"test-service", + "name":"test-name", + "type":"test-type" + } + } +} \ No newline at end of file From 356c7f0ba5be6085a22a535f2a27b34f5732ab95 Mon Sep 17 00:00:00 2001 From: emcmanus Date: Fri, 25 Oct 2019 17:48:21 -0700 Subject: [PATCH 011/364] Import of Cloud Functions JVM from Git-on-Borg. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 991467fa996700743830c5fddc674bfcabab4b48 Fix location of imported files. by Éamonn McManus - f67364dd6c3c6f5c5badfaa8273fd9e183c60c77 Add a simple test of the converter. by Éamonn McManus - 50c2738e08b77695213143fb703b5c83ffad2e58 First step of switching to the new build system for java1... by Éamonn McManus PiperOrigin-RevId: 276793883 --- .../functions/invoker/runner/Invoker.java | 35 ++++++++----------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java index 95ae3bab..88e31ba2 100644 --- a/invoker-core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java @@ -9,6 +9,8 @@ import com.google.cloud.functions.invoker.HttpFunctionSignatureMatcher; import java.io.File; import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; import java.util.Optional; import java.util.logging.Level; import java.util.logging.LogManager; @@ -67,19 +69,20 @@ public static void main(String[] args) throws Exception { CommandLine line = parseCommandLineOptions(args); int port = - Integer.parseInt( - firstNonNull(line.getOptionValue("port"), System.getenv("PORT"), "8080")); + Arrays.asList(line.getOptionValue("port"), System.getenv("PORT")).stream() + .filter(Objects::nonNull) + .findFirst() + .map(Integer::parseInt) + .orElse(8080); String functionTarget = - firstNonNull( - line.getOptionValue("target"), - System.getenv("FUNCTION_TARGET"), - "TestFunction.function"); + Arrays.asList(line.getOptionValue("target"), System.getenv("FUNCTION_TARGET")).stream() + .filter(Objects::nonNull) + .findFirst() + .orElse("TestFunction.function"); Optional functionJarPath = - isLocalRun() - ? Optional.of( - firstNonNull(line.getOptionValue("jar"), System.getenv("FUNCTION_JAR"))) - : Optional.empty(); - + Arrays.asList(line.getOptionValue("jar"), System.getenv("FUNCTION_JAR")).stream() + .filter(Objects::nonNull) + .findFirst(); Invoker invoker = new Invoker( port, @@ -113,16 +116,6 @@ private static CommandLine parseCommandLineOptions(String[] args) { return null; } - @SafeVarargs - private static T firstNonNull(T... objects) { - for (T t : objects) { - if (t != null) { - return t; - } - } - return null; - } - private final Integer port; private final String functionTarget; private final String functionSignatureType; From 5917013042518beac2be1d59e0a81c9365a36f96 Mon Sep 17 00:00:00 2001 From: emcmanus Date: Sun, 27 Oct 2019 12:59:27 -0700 Subject: [PATCH 012/364] Import of Cloud Functions JVM from Git-on-Borg. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 07ad85ded380aff6076604e10472f44dd38746de Ensure unneeded dependencies are not included in the invo... by Éamonn McManus PiperOrigin-RevId: 276962766 --- invoker-core/pom.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/invoker-core/pom.xml b/invoker-core/pom.xml index 6843c19d..660c8c39 100644 --- a/invoker-core/pom.xml +++ b/invoker-core/pom.xml @@ -35,26 +35,25 @@ com.ryanharter.auto.value auto-value-gson 0.8.0 + provided com.ryanharter.auto.value auto-value-gson-annotations 0.8.0 - - - com.squareup - javapoet - 1.11.1 + provided com.google.auto.value auto-value 1.6.2 + provided com.google.auto.value auto-value-annotations 1.6.2 + provided org.eclipse.jetty @@ -107,6 +106,7 @@ org.eclipse.jetty jetty-client 9.4.11.v20180605 + test From 52ce9280dc6d5f896e306dd544c4f8e3c8ab78a7 Mon Sep 17 00:00:00 2001 From: emcmanus Date: Tue, 29 Oct 2019 16:31:29 -0700 Subject: [PATCH 013/364] Import of Cloud Functions JVM from Git-on-Borg. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 4d81df4ecc860e3ea5a538f981a271310e42f78c Handle the new Background Functions API. by Éamonn McManus - 75303c6cc6e646c68db8271029118809c469a804 Add BackgroundFunction to the GCF API. by Éamonn McManus - 51843c21eaafb371d3ede15c40e3ee2be4c9d990 Add support for new-style functions to the Invoker. by Éamonn McManus PiperOrigin-RevId: 277389762 --- functions-framework-api/pom.xml | 8 ++ .../cloud/functions/BackgroundFunction.java | 73 +++++++++++++++++ invoker-core/pom.xml | 4 +- .../functions/invoker/FunctionLoader.java | 32 +++----- .../NewBackgroundFunctionExecutor.java | 79 +++++++++++++++++++ .../invoker/NewHttpFunctionExecutor.java | 73 +++++++++++++++++ .../invoker/http/HttpResponseImpl.java | 10 ++- .../functions/invoker/runner/Invoker.java | 49 +++++++++--- .../invoker/BackgroundFunctionTest.java | 3 +- .../functions/invoker/HttpFunctionTest.java | 5 +- .../functions/invoker/IntegrationTest.java | 64 ++++++++++++--- .../testfunctions/NewBackgroundSnoop.java | 43 ++++++++++ .../invoker/testfunctions/NewEcho.java | 16 ++++ .../invoker/testfunctions/NewEchoUrl.java | 15 ++++ .../invoker/testfunctions/NewHelloWorld.java | 12 +++ 15 files changed, 434 insertions(+), 52 deletions(-) create mode 100644 functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java create mode 100644 invoker-core/src/main/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutor.java create mode 100644 invoker-core/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java create mode 100644 invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewBackgroundSnoop.java create mode 100644 invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewEcho.java create mode 100644 invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewEchoUrl.java create mode 100644 invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewHelloWorld.java diff --git a/functions-framework-api/pom.xml b/functions-framework-api/pom.xml index d3cdf30c..31daa6e8 100644 --- a/functions-framework-api/pom.xml +++ b/functions-framework-api/pom.xml @@ -179,4 +179,12 @@
+ + + com.google.code.gson + gson + 2.8.6 + jar + + diff --git a/functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java b/functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java new file mode 100644 index 00000000..7d4bd8aa --- /dev/null +++ b/functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java @@ -0,0 +1,73 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.cloud.functions; + +import com.google.gson.JsonElement; + +/** + * Represents a Cloud Function that is activated by an event. + * + *

Here is an example of an implementation that operates on the JSON payload of the event + * directly: + * + *

+ * public class Example implements BackgroundFunction {
+ *   private static final Logger logger = Logger.getLogger(Example.class.getName());
+ *
+ *   {@code @Override}
+ *   public void accept(JsonElement json, Context context) {
+ *     JsonElement messageId = json.getAsJsonObject().get("messageId");
+ *     String messageIdString = messageId.getAsJsonString();
+ *     logger.info("Got messageId " + messageIdString);
+ *   }
+ * }
+ * 
+ * + *

Here is an example of an implementation that deserializes the JSON payload into a Java + * object for simpler access: + * + *

+ * public class Example implements BackgroundFunction {
+ *   private static final Logger logger = Logger.getLogger(Example.class.getName());
+ *
+ *   {@code @Override}
+ *   public void accept(JsonElement json, Context context) {
+ *     PubSubMessage message = Gson.fromJson(json, PubSubMessage.class);
+ *     logger.info("Got messageId " + message.messageId);
+ *   }
+ * }
+ *
+ * // Where PubSubMessage is a user-defined class like this:
+ * public class PubSubMessage {
+ *   String data;
+ *   {@code Map} attributes;
+ *   String messageId;
+ *   String publishTime;
+ * }
+ * 
+ */ +@FunctionalInterface +public interface BackgroundFunction { + /** + * Called to service an incoming event. This interface is implemented by user code to + * provide the action for a given background function. + * (including any {@link Error}) then the HTTP response will have a 500 status code. + * + * @param json the payload of the event, as a parsed JSON object. + * @param context the context of the event. This is a set of values that every event has, + * separately from the payload, such as timestamp and event type. + */ + public void accept(JsonElement json, Context context); +} diff --git a/invoker-core/pom.xml b/invoker-core/pom.xml index 660c8c39..a456e9c4 100644 --- a/invoker-core/pom.xml +++ b/invoker-core/pom.xml @@ -14,7 +14,7 @@ com.google.cloud.functions functions-framework-api - 1.0.0-alpha-2-rc1 + 1.0.0-alpha-2-rc2 javax.servlet @@ -29,7 +29,7 @@ com.google.code.gson gson - 2.8.5 + 2.8.6 com.ryanharter.auto.value diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/FunctionLoader.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/FunctionLoader.java index 582f4936..4e657878 100644 --- a/invoker-core/src/main/java/com/google/cloud/functions/invoker/FunctionLoader.java +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/FunctionLoader.java @@ -1,26 +1,21 @@ package com.google.cloud.functions.invoker; -import java.io.File; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.Optional; - /** * Dynamically loads the user's function class and returns an instance of {@link CloudFunction}. */ public class FunctionLoader { private final String functionTarget; + private final ClassLoader classLoader; private final FunctionSignatureMatcher matcher; - private final Optional userJarFile; public FunctionLoader( String functionTarget, - Optional userJarFile, + ClassLoader classLoader, FunctionSignatureMatcher matcher) { this.functionTarget = functionTarget; + this.classLoader = classLoader; this.matcher = matcher; - this.userJarFile = userJarFile; } /** @@ -31,23 +26,18 @@ public FunctionLoader( public T loadUserFunction() throws Exception { int lastDotIndex = functionTarget.lastIndexOf("."); if (lastDotIndex == -1) { - throw new RuntimeException( - "Expected target of format .., but got " + functionTarget); + throw new ClassNotFoundException(functionTarget); } String targetClassName = functionTarget.substring(0, lastDotIndex); String targetMethodName = functionTarget.substring(lastDotIndex + 1); - - ClassLoader classLoader; - - if (userJarFile.isPresent()) { - classLoader = - new URLClassLoader( - new URL[]{userJarFile.get().toURI().toURL()}, - Thread.currentThread().getContextClassLoader()); - } else { - classLoader = Thread.currentThread().getContextClassLoader(); + Class targetClass; + try { + targetClass = classLoader.loadClass(targetClassName); + } catch (ClassNotFoundException e) { + throw new RuntimeException( + "Could not load either " + functionTarget + " (new form) or " + + targetClassName + " (old form)"); } - Class targetClass = classLoader.loadClass(targetClassName); Object targetInstance = targetClass.getDeclaredConstructor().newInstance(); return matcher.match(targetClass, targetInstance, targetMethodName, functionTarget); diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutor.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutor.java new file mode 100644 index 00000000..ed059d19 --- /dev/null +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutor.java @@ -0,0 +1,79 @@ +package com.google.cloud.functions.invoker; + +import com.google.cloud.functions.BackgroundFunction; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.TypeAdapter; +import java.io.BufferedReader; +import java.io.IOException; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** Executes the user's background function. */ +public class NewBackgroundFunctionExecutor extends HttpServlet { + private static final Logger logger = Logger.getLogger("com.google.cloud.functions.invoker"); + + private final BackgroundFunction function; + + private NewBackgroundFunctionExecutor(BackgroundFunction function) { + this.function = function; + } + + /** + * Make a {@link NewBackgroundFunctionExecutor} for the class named by the given {@code target}. + * If the class cannot be loaded, we currently assume that this is an old-style function + * (specified as package.Class.method instead of package.Class) and return + * {@code Optional.empty()}. + * + * @throws RuntimeException if we succeed in loading the class named by {@code target} but then + * either the class does not implement {@link HttpFunction} or we are unable to construct an + * instance using its no-arg constructor. + */ + public static Optional forTarget(String target) { + Class c; + try { + c = Class.forName(target); + } catch (ClassNotFoundException e) { + return Optional.empty(); + } + if (!BackgroundFunction.class.isAssignableFrom(c)) { + throw new RuntimeException( + "Class " + c.getName() + " does not implement " + BackgroundFunction.class.getName()); + } + Class functionClass = c.asSubclass(BackgroundFunction.class); + try { + BackgroundFunction function = functionClass.getConstructor().newInstance(); + return Optional.of(new NewBackgroundFunctionExecutor(function)); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Could not construct an instance of " + target + ": " + e, e); + } + } + + /** Executes the user's background function, can handle all HTTP type methods. */ + @Override + public void service(HttpServletRequest req, HttpServletResponse res) throws IOException { + BufferedReader body = req.getReader(); + + // A Type Adapter is required to set the type of the JsonObject because CloudFunctionsContext + // is abstract and Gson default behavior instantiates the type provided. + TypeAdapter typeAdapter = + CloudFunctionsContext.typeAdapter(new Gson()); + Gson gson = new GsonBuilder() + .registerTypeAdapter(CloudFunctionsContext.class, typeAdapter) + .registerTypeAdapter(Event.class, new Event.EventDeserializer()) + .create(); + + Event event = gson.fromJson(body, Event.class); + try { + function.accept(event.getData(), event.getContext()); + res.setStatus(HttpServletResponse.SC_OK); + } catch (Throwable t) { + res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + logger.log(Level.WARNING, "Failed to execute " + function.getClass().getName(), t); + } + } +} diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java new file mode 100644 index 00000000..499cce39 --- /dev/null +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java @@ -0,0 +1,73 @@ +package com.google.cloud.functions.invoker; + +import com.google.cloud.functions.HttpFunction; +import com.google.cloud.functions.invoker.http.HttpRequestImpl; +import com.google.cloud.functions.invoker.http.HttpResponseImpl; +import java.io.IOException; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** Executes the user's method. */ +public class NewHttpFunctionExecutor extends HttpServlet { + private static final Logger logger = Logger.getLogger("com.google.cloud.functions.invoker"); + + private final HttpFunction function; + + private NewHttpFunctionExecutor(HttpFunction function) { + this.function = function; + } + + /** + * Make a {@link NewHttpFunctionExecutor} for the class named by the given {@code target}. + * If the class cannot be loaded, we currently assume that this is an old-style function + * (specified as package.Class.method instead of package.Class) and return + * {@code Optional.empty()}. + * + * @throws RuntimeException if we succeed in loading the class named by {@code target} but then + * either the class does not implement {@link HttpFunction} or we are unable to construct an + * instance using its no-arg constructor. + */ + public static Optional forTarget(String target) { + Class c; + try { + c = Class.forName(target); + } catch (ClassNotFoundException e) { + return Optional.empty(); + } + if (!HttpFunction.class.isAssignableFrom(c)) { + throw new RuntimeException( + "Class " + c.getName() + " does not implement " + HttpFunction.class.getName()); + } + Class httpFunctionClass = c.asSubclass(HttpFunction.class); + try { + HttpFunction httpFunction = httpFunctionClass.getConstructor().newInstance(); + return Optional.of(new NewHttpFunctionExecutor(httpFunction)); + } catch (ReflectiveOperationException e) { + throw new RuntimeException("Could not construct an instance of " + target + ": " + e, e); + } + } + + /** Executes the user's method, can handle all HTTP type methods. */ + @Override + public void service(HttpServletRequest req, HttpServletResponse res) { + URLRequestWrapper wrapper = new URLRequestWrapper(req); + HttpRequestImpl reqImpl = new HttpRequestImpl(wrapper); + HttpResponseImpl respImpl = new HttpResponseImpl(res); + try { + function.service(reqImpl, respImpl); + } catch (Throwable t) { + res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + logger.log(Level.WARNING, "Failed to execute " + function.getClass().getName(), t); + } finally { + try { + respImpl.getWriter().flush(); + } catch (IOException e) { + // Too bad, can't flush. + } + } + } +} diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java index 93deb2b1..edfb9af9 100644 --- a/invoker-core/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java @@ -66,8 +66,13 @@ public OutputStream getOutputStream() throws IOException { return response.getOutputStream(); } + private BufferedWriter writer; + @Override - public BufferedWriter getWriter() throws IOException { + public synchronized BufferedWriter getWriter() throws IOException { + if (writer != null) { + return writer; + } // We could just wrap a BufferedWriter around the PrintWriter that the Servlet API gives us, // but this slightly clunky alternative potentially avoids two intermediate objects in the // writer chain. @@ -82,8 +87,9 @@ public BufferedWriter getWriter() throws IOException { } catch (ReflectiveOperationException e) { throw new IOException("Reflection failed", e); } - return (wrappedWriter instanceof BufferedWriter) + this.writer = (wrappedWriter instanceof BufferedWriter) ? (BufferedWriter) wrappedWriter : new BufferedWriter(wrappedWriter); + return this.writer; } } diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java index 88e31ba2..c6000699 100644 --- a/invoker-core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java @@ -7,14 +7,19 @@ import com.google.cloud.functions.invoker.HttpCloudFunction; import com.google.cloud.functions.invoker.HttpFunctionExecutor; import com.google.cloud.functions.invoker.HttpFunctionSignatureMatcher; +import com.google.cloud.functions.invoker.NewBackgroundFunctionExecutor; +import com.google.cloud.functions.invoker.NewHttpFunctionExecutor; import java.io.File; import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; import java.util.Arrays; import java.util.Objects; import java.util.Optional; import java.util.logging.Level; import java.util.logging.LogManager; import java.util.logging.Logger; +import javax.servlet.http.HttpServlet; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.DefaultParser; @@ -138,17 +143,43 @@ public void startServer() throws Exception { + functionJarFile.get().getAbsolutePath()); } + ClassLoader classLoader; + if (functionJarFile.isPresent()) { + classLoader = + new URLClassLoader( + new URL[]{functionJarFile.get().toURI().toURL()}, + Thread.currentThread().getContextClassLoader()); + } else { + classLoader = Thread.currentThread().getContextClassLoader(); + } + if ("http".equals(functionSignatureType)) { - FunctionLoader loader = - new FunctionLoader<>(functionTarget, functionJarFile, new HttpFunctionSignatureMatcher()); - HttpCloudFunction function = loader.loadUserFunction(); - context.addServlet(new ServletHolder(new HttpFunctionExecutor(function)), "/*"); + HttpServlet servlet; + Optional newExecutor = + NewHttpFunctionExecutor.forTarget(functionTarget); + if (newExecutor.isPresent()) { + servlet = newExecutor.get(); + } else { + FunctionLoader loader = + new FunctionLoader<>(functionTarget, classLoader, new HttpFunctionSignatureMatcher()); + HttpCloudFunction function = loader.loadUserFunction(); + servlet = new HttpFunctionExecutor(function); + } + context.addServlet(new ServletHolder(servlet), "/*"); } else if ("event".equals(functionSignatureType)) { - FunctionLoader loader = - new FunctionLoader<>( - functionTarget, functionJarFile, new BackgroundFunctionSignatureMatcher()); - BackgroundCloudFunction function = loader.loadUserFunction(); - context.addServlet(new ServletHolder(new BackgroundFunctionExecutor(function)), "/*"); + HttpServlet servlet; + Optional newExecutor = + NewBackgroundFunctionExecutor.forTarget(functionTarget); + if (newExecutor.isPresent()) { + servlet = newExecutor.get(); + } else { + FunctionLoader loader = + new FunctionLoader<>( + functionTarget, classLoader, new BackgroundFunctionSignatureMatcher()); + BackgroundCloudFunction function = loader.loadUserFunction(); + servlet = new BackgroundFunctionExecutor(function); + } + context.addServlet(new ServletHolder(servlet), "/*"); } else { throw new RuntimeException("Unknown function signature type: " + functionSignatureType); } diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/BackgroundFunctionTest.java b/invoker-core/src/test/java/com/google/cloud/functions/invoker/BackgroundFunctionTest.java index a717391a..b75cc538 100644 --- a/invoker-core/src/test/java/com/google/cloud/functions/invoker/BackgroundFunctionTest.java +++ b/invoker-core/src/test/java/com/google/cloud/functions/invoker/BackgroundFunctionTest.java @@ -8,7 +8,6 @@ import java.io.InputStreamReader; import java.util.Arrays; import java.util.Collection; -import java.util.Optional; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.junit.Test; @@ -71,7 +70,7 @@ public void adder() throws Exception { HttpServletResponse res = Mockito.mock(HttpServletResponse.class); String fullTarget = "com.google.cloud.functions.invoker.BackgroundFunctionTest$" + target; FunctionLoader loader = - new FunctionLoader<>(fullTarget, Optional.empty(), + new FunctionLoader<>(fullTarget, getClass().getClassLoader(), new BackgroundFunctionSignatureMatcher()); BackgroundCloudFunction function = loader.loadUserFunction(); BackgroundFunctionExecutor executor = new BackgroundFunctionExecutor(function); diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/HttpFunctionTest.java b/invoker-core/src/test/java/com/google/cloud/functions/invoker/HttpFunctionTest.java index 4423dfec..1f149ccf 100644 --- a/invoker-core/src/test/java/com/google/cloud/functions/invoker/HttpFunctionTest.java +++ b/invoker-core/src/test/java/com/google/cloud/functions/invoker/HttpFunctionTest.java @@ -5,7 +5,6 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintWriter; -import java.util.Optional; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.junit.Test; @@ -31,8 +30,8 @@ public void adder() throws Exception { String fullTarget = "com.google.cloud.functions.invoker.HttpFunctionTest$HttpWriter.writeResponse"; String requestData = "testData"; - FunctionLoader loader = - new FunctionLoader<>(fullTarget, Optional.empty(), new HttpFunctionSignatureMatcher()); + FunctionLoader loader = new FunctionLoader<>( + fullTarget, getClass().getClassLoader(), new HttpFunctionSignatureMatcher()); HttpCloudFunction function = loader.loadUserFunction(); HttpFunctionExecutor executor = new HttpFunctionExecutor(function); Mockito.when(req.getParameter("data")).thenReturn(requestData); diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java b/invoker-core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java index 01c933fc..67693e98 100644 --- a/invoker-core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java +++ b/invoker-core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java @@ -48,9 +48,9 @@ public class IntegrationTest { */ @BeforeClass public static void allocateServerPort() throws IOException { - ServerSocket serverSocket = new ServerSocket(0); - serverPort = serverSocket.getLocalPort(); - serverSocket.close(); + try (ServerSocket serverSocket = new ServerSocket(0)) { + serverPort = serverSocket.getLocalPort(); + } } /** @@ -93,6 +93,12 @@ public void helloWorld() throws Exception { TestCase.builder().setExpectedResponseText("hello\n").build()); } + @Test + public void newHelloWorld() throws Exception { + testHttpFunction("NewHelloWorld", + TestCase.builder().setExpectedResponseText("hello\n").build()); + } + @Test public void echo() throws Exception { String testText = "hello\nworld\n"; @@ -100,6 +106,13 @@ public void echo() throws Exception { TestCase.builder().setRequestText(testText).setExpectedResponseText(testText).build()); } + @Test + public void newEcho() throws Exception { + String testText = "hello\nworld\n"; + testHttpFunction("NewEcho", + TestCase.builder().setRequestText(testText).setExpectedResponseText(testText).build()); + } + @Test public void echoUrl() throws Exception { String[] testUrls = {"/", "/foo/bar", "/?foo=bar&baz=buh", "/foo?bar=baz"}; @@ -109,6 +122,15 @@ public void echoUrl() throws Exception { testHttpFunction("EchoUrl.echoUrl", testCases); } + @Test + public void newEchoUrl() throws Exception { + String[] testUrls = {"/", "/foo/bar", "/?foo=bar&baz=buh", "/foo?bar=baz"}; + TestCase[] testCases = Arrays.stream(testUrls) + .map(url -> TestCase.builder().setUrl(url).setExpectedResponseText(url + "\n").build()) + .toArray(TestCase[]::new); + testHttpFunction("NewEchoUrl", testCases); + } + @Test public void background() throws Exception { URL resourceUrl = getClass().getResource("/adder_gcf_ga_event.json"); @@ -126,8 +148,25 @@ public void background() throws Exception { assertThat(snoopedJson).isEqualTo(json); } - private void testHttpFunction(String classAndMethod, TestCase... testCases) throws Exception { - testFunction(SignatureType.HTTP, classAndMethod, testCases); + @Test + public void newBackground() throws Exception { + URL resourceUrl = getClass().getResource("/adder_gcf_ga_event.json"); + assertThat(resourceUrl).isNotNull(); + File snoopFile = File.createTempFile("FunctionsIntegrationTest", ".txt"); + snoopFile.deleteOnExit(); + String originalJson = Resources.toString(resourceUrl, StandardCharsets.UTF_8); + JsonObject json = new JsonParser().parse(originalJson).getAsJsonObject(); + JsonObject jsonData = json.getAsJsonObject("data"); + jsonData.addProperty("targetFile", snoopFile.toString()); + testBackgroundFunction("NewBackgroundSnoop", + TestCase.builder().setRequestText(json.toString()).build()); + String snooped = Files.asCharSource(snoopFile, StandardCharsets.UTF_8).read(); + JsonObject snoopedJson = new JsonParser().parse(snooped).getAsJsonObject(); + assertThat(snoopedJson).isEqualTo(json); + } + + private void testHttpFunction(String target, TestCase... testCases) throws Exception { + testFunction(SignatureType.HTTP, target, testCases); } private void testBackgroundFunction(String classAndMethod, TestCase... testCases) @@ -135,9 +174,9 @@ private void testBackgroundFunction(String classAndMethod, TestCase... testCases testFunction(SignatureType.BACKGROUND, classAndMethod, testCases); } - private void testFunction(SignatureType signatureType, String classAndMethod, - TestCase... testCases) throws Exception { - Process server = startServer(signatureType, classAndMethod); + private void testFunction( + SignatureType signatureType, String target, TestCase... testCases) throws Exception { + Process server = startServer(signatureType, target); try { HttpClient httpClient = new HttpClient(); httpClient.start(); @@ -174,9 +213,9 @@ public String toString() { } } - private Process startServer(SignatureType signatureType, String classAndMethod) + private Process startServer(SignatureType signatureType, String target) throws IOException, InterruptedException { - String fullMethodName = "com.google.cloud.functions.invoker.testfunctions." + classAndMethod; + String fullTarget = "com.google.cloud.functions.invoker.testfunctions." + target; File javaHome = new File(System.getProperty("java.home")); assertThat(javaHome.exists()).isTrue(); File javaBin = new File(javaHome, "bin"); @@ -190,11 +229,10 @@ private Process startServer(SignatureType signatureType, String classAndMethod) ProcessBuilder processBuilder = new ProcessBuilder() .command(command) .redirectErrorStream(true); - Map environment = ImmutableMap.of( - "PORT", String.valueOf(serverPort), + Map environment = ImmutableMap.of("PORT", String.valueOf(serverPort), "K_SERVICE", "test-function", "FUNCTION_SIGNATURE_TYPE", signatureType.toString(), - "FUNCTION_TARGET", fullMethodName); + "FUNCTION_TARGET", fullTarget); processBuilder.environment().putAll(environment); Process serverProcess = processBuilder.start(); CountDownLatch ready = new CountDownLatch(1); diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewBackgroundSnoop.java b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewBackgroundSnoop.java new file mode 100644 index 00000000..26be2b5a --- /dev/null +++ b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewBackgroundSnoop.java @@ -0,0 +1,43 @@ +package com.google.cloud.functions.invoker.testfunctions; + +import com.google.cloud.functions.BackgroundFunction; +import com.google.cloud.functions.Context; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.UncheckedIOException; + +/** + * Extract the targetFile property from the data of the JSON payload, and write to it a JSON + * encoding of this payload and the context. The JSON format is chosen to be identical to the + * EventFlow format that we currently use in GCF, and the file that we write should in fact be + * identical to the JSON payload that the Functions Framework received from the client in the test. + * This will need to be rewritten when we switch to CloudEvents. + */ +public class NewBackgroundSnoop implements BackgroundFunction { + @Override + public void accept(JsonElement jsonElement, Context context) { + String targetFile = jsonElement.getAsJsonObject().get("targetFile").getAsString(); + if (targetFile == null) { + throw new IllegalArgumentException("Expected targetFile in JSON payload"); + } + JsonObject resourceJson = new JsonParser().parse(context.resource()).getAsJsonObject(); + JsonObject contextJson = new JsonObject(); + contextJson.addProperty("eventId", context.eventId()); + contextJson.addProperty("timestamp", context.timestamp()); + contextJson.addProperty("eventType", context.eventType()); + contextJson.add("resource", resourceJson); + JsonObject contextAndPayloadJson = new JsonObject(); + contextAndPayloadJson.add("data", jsonElement); + contextAndPayloadJson.add("context", contextJson); + try (FileWriter fileWriter = new FileWriter(targetFile); + PrintWriter writer = new PrintWriter(fileWriter)) { + writer.println(contextAndPayloadJson); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewEcho.java b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewEcho.java new file mode 100644 index 00000000..3808877e --- /dev/null +++ b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewEcho.java @@ -0,0 +1,16 @@ +package com.google.cloud.functions.invoker.testfunctions; + +import com.google.cloud.functions.HttpFunction; +import com.google.cloud.functions.HttpRequest; +import com.google.cloud.functions.HttpResponse; +import java.util.stream.Collectors; + +public class NewEcho implements HttpFunction { + @Override + public void service(HttpRequest request, HttpResponse response) throws Exception { + String body = request.getReader().lines().collect(Collectors.joining("\n")) + "\n"; + response.setContentType("text/plain"); + response.getWriter().write(body); + response.getWriter().flush(); + } +} diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewEchoUrl.java b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewEchoUrl.java new file mode 100644 index 00000000..2df181f4 --- /dev/null +++ b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewEchoUrl.java @@ -0,0 +1,15 @@ +package com.google.cloud.functions.invoker.testfunctions; + +import com.google.cloud.functions.HttpFunction; +import com.google.cloud.functions.HttpRequest; +import com.google.cloud.functions.HttpResponse; + +public class NewEchoUrl implements HttpFunction { + @Override + public void service(HttpRequest request, HttpResponse response) throws Exception { + StringBuilder url = new StringBuilder(request.getPath()); + request.getQuery().ifPresent(q -> url.append("?").append(q)); + url.append("\n"); + response.getWriter().write(url.toString()); + } +} diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewHelloWorld.java b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewHelloWorld.java new file mode 100644 index 00000000..f1f9c21a --- /dev/null +++ b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewHelloWorld.java @@ -0,0 +1,12 @@ +package com.google.cloud.functions.invoker.testfunctions; + +import com.google.cloud.functions.HttpFunction; +import com.google.cloud.functions.HttpRequest; +import com.google.cloud.functions.HttpResponse; + +public class NewHelloWorld implements HttpFunction { + @Override + public void service(HttpRequest request, HttpResponse response) throws Exception { + response.getWriter().write("hello\n"); + } +} From 84a2f6d3469d7814f593e474e6d588605f77322a Mon Sep 17 00:00:00 2001 From: emcmanus Date: Fri, 8 Nov 2019 03:51:17 -0800 Subject: [PATCH 014/364] Import of Cloud Functions JVM from Git-on-Borg. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - a6111561e09808f60fd1e79b7d3f2035fcb047bc Change invoker to depend on API version 1.0.0-alpha-2-rc3. by Éamonn McManus - 7007628de5e20984babc8be8764325c2c2cad607 Revise background functions API. by Éamonn McManus PiperOrigin-RevId: 279283731 --- functions-framework-api/pom.xml | 10 +- .../cloud/functions/BackgroundFunction.java | 44 +++------ .../functions/RawBackgroundFunction.java | 74 +++++++++++++++ invoker-core/pom.xml | 2 +- .../NewBackgroundFunctionExecutor.java | 91 +++++++++++++++++-- .../functions/invoker/IntegrationTest.java | 37 ++++---- .../NewBackgroundFunctionExecutorTest.java | 61 +++++++++++++ .../testfunctions/NewBackgroundSnoop.java | 17 ++-- .../NewTypedBackgroundSnoop.java | 50 ++++++++++ 9 files changed, 312 insertions(+), 74 deletions(-) create mode 100644 functions-framework-api/src/main/java/com/google/cloud/functions/RawBackgroundFunction.java create mode 100644 invoker-core/src/test/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutorTest.java create mode 100644 invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewTypedBackgroundSnoop.java diff --git a/functions-framework-api/pom.xml b/functions-framework-api/pom.xml index 31daa6e8..36d7db1b 100644 --- a/functions-framework-api/pom.xml +++ b/functions-framework-api/pom.xml @@ -24,7 +24,7 @@ com.google.cloud.functions functions-framework-api - 1.0.0-alpha-2-rc2-SNAPSHOT + 1.0.0-alpha-2-rc3-SNAPSHOT UTF-8 @@ -179,12 +179,4 @@ - - - com.google.code.gson - gson - 2.8.6 - jar - - diff --git a/functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java b/functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java index 7d4bd8aa..bc7bf601 100644 --- a/functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java +++ b/functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java @@ -14,38 +14,22 @@ package com.google.cloud.functions; -import com.google.gson.JsonElement; - /** - * Represents a Cloud Function that is activated by an event. + * Represents a Cloud Function that is activated by an event and parsed into a user-supplied class. + * The payload of the event is a JSON object, which is deserialized into a user-defined class as + * described for + * Gson. * - *

Here is an example of an implementation that operates on the JSON payload of the event - * directly: + *

Here is an example of an implementation that accesses the {@code messageId} property from + * a payload that matches a user-defined {@code PubSubMessage} class: * *

- * public class Example implements BackgroundFunction {
+ * public class Example implements {@code BackgroundFunction} {
  *   private static final Logger logger = Logger.getLogger(Example.class.getName());
  *
  *   {@code @Override}
- *   public void accept(JsonElement json, Context context) {
- *     JsonElement messageId = json.getAsJsonObject().get("messageId");
- *     String messageIdString = messageId.getAsJsonString();
- *     logger.info("Got messageId " + messageIdString);
- *   }
- * }
- * 
- * - *

Here is an example of an implementation that deserializes the JSON payload into a Java - * object for simpler access: - * - *

- * public class Example implements BackgroundFunction {
- *   private static final Logger logger = Logger.getLogger(Example.class.getName());
- *
- *   {@code @Override}
- *   public void accept(JsonElement json, Context context) {
- *     PubSubMessage message = Gson.fromJson(json, PubSubMessage.class);
- *     logger.info("Got messageId " + message.messageId);
+ *   public void accept(PubSubMessage pubSubMessage, Context context) {
+ *     logger.info("Got messageId " + pubSubMessage.messageId);
  *   }
  * }
  *
@@ -57,17 +41,19 @@
  *   String publishTime;
  * }
  * 
+ * + * @param the class of payload objects that this function expects. */ @FunctionalInterface -public interface BackgroundFunction { +public interface BackgroundFunction { /** * Called to service an incoming event. This interface is implemented by user code to - * provide the action for a given background function. + * provide the action for a given background function. If this method throws any exception * (including any {@link Error}) then the HTTP response will have a 500 status code. * - * @param json the payload of the event, as a parsed JSON object. + * @param payload the payload of the event, deserialized from the original JSON string. * @param context the context of the event. This is a set of values that every event has, * separately from the payload, such as timestamp and event type. */ - public void accept(JsonElement json, Context context); + void accept(T payload, Context context); } diff --git a/functions-framework-api/src/main/java/com/google/cloud/functions/RawBackgroundFunction.java b/functions-framework-api/src/main/java/com/google/cloud/functions/RawBackgroundFunction.java new file mode 100644 index 00000000..472f7c4d --- /dev/null +++ b/functions-framework-api/src/main/java/com/google/cloud/functions/RawBackgroundFunction.java @@ -0,0 +1,74 @@ +// Copyright 2019 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.cloud.functions; + +/** + * Represents a Cloud Function that is activated by an event. The payload of the event is a JSON + * object, which can be parsed using a JSON package such as + * GSON. + * + *

Here is an example of an implementation that parses the JSON payload using Gson, to access its + * {@code messageId} property: + * + *

+ * public class Example implements RawBackgroundFunction {
+ *   private static final Logger logger = Logger.getLogger(Example.class.getName());
+ *
+ *   {@code @Override}
+ *   public void accept(String json, Context context) {
+ *     JsonObject jsonObject = new Gson().fromJson(json, JsonObject.class);
+ *     JsonElement messageId = jsonObject.get("messageId");
+ *     String messageIdString = messageId.getAsJsonString();
+ *     logger.info("Got messageId " + messageIdString);
+ *   }
+ * }
+ * 
+ * + *

Here is an example of an implementation that deserializes the JSON payload into a Java + * object for simpler access, again using Gson: + * + *

+ * public class Example implements RawBackgroundFunction {
+ *   private static final Logger logger = Logger.getLogger(Example.class.getName());
+ *
+ *   {@code @Override}
+ *   public void accept(String json, Context context) {
+ *     PubSubMessage message = new Gson().fromJson(json, PubSubMessage.class);
+ *     logger.info("Got messageId " + message.messageId);
+ *   }
+ * }
+ *
+ * // Where PubSubMessage is a user-defined class like this:
+ * public class PubSubMessage {
+ *   String data;
+ *   {@code Map} attributes;
+ *   String messageId;
+ *   String publishTime;
+ * }
+ * 
+ */ +@FunctionalInterface +public interface RawBackgroundFunction { + /** + * Called to service an incoming event. This interface is implemented by user code to + * provide the action for a given background function. If this method throws any exception + * (including any {@link Error}) then the HTTP response will have a 500 status code. + * + * @param json the payload of the event, as a JSON string. + * @param context the context of the event. This is a set of values that every event has, + * separately from the payload, such as timestamp and event type. + */ + void accept(String json, Context context); +} diff --git a/invoker-core/pom.xml b/invoker-core/pom.xml index a456e9c4..335f5c12 100644 --- a/invoker-core/pom.xml +++ b/invoker-core/pom.xml @@ -14,7 +14,7 @@ com.google.cloud.functions functions-framework-api - 1.0.0-alpha-2-rc2 + 1.0.0-alpha-2-rc3 javax.servlet diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutor.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutor.java index ed059d19..43b33456 100644 --- a/invoker-core/src/main/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutor.java +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutor.java @@ -1,11 +1,16 @@ package com.google.cloud.functions.invoker; import com.google.cloud.functions.BackgroundFunction; +import com.google.cloud.functions.Context; +import com.google.cloud.functions.RawBackgroundFunction; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; import com.google.gson.TypeAdapter; import java.io.BufferedReader; import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Arrays; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; @@ -17,9 +22,9 @@ public class NewBackgroundFunctionExecutor extends HttpServlet { private static final Logger logger = Logger.getLogger("com.google.cloud.functions.invoker"); - private final BackgroundFunction function; + private final RawBackgroundFunction function; - private NewBackgroundFunctionExecutor(BackgroundFunction function) { + private NewBackgroundFunctionExecutor(RawBackgroundFunction function) { this.function = function; } @@ -30,8 +35,8 @@ private NewBackgroundFunctionExecutor(BackgroundFunction function) { * {@code Optional.empty()}. * * @throws RuntimeException if we succeed in loading the class named by {@code target} but then - * either the class does not implement {@link HttpFunction} or we are unable to construct an - * instance using its no-arg constructor. + * either the class does not implement {@link RawBackgroundFunction} or we are unable to + * construct an instance using its no-arg constructor. */ public static Optional forTarget(String target) { Class c; @@ -40,17 +45,83 @@ public static Optional forTarget(String target) { } catch (ClassNotFoundException e) { return Optional.empty(); } - if (!BackgroundFunction.class.isAssignableFrom(c)) { + if (!BackgroundFunction.class.isAssignableFrom(c) + && !RawBackgroundFunction.class.isAssignableFrom(c)) { throw new RuntimeException( - "Class " + c.getName() + " does not implement " + BackgroundFunction.class.getName()); + "Class " + c.getName() + " implements neither " + BackgroundFunction.class + .getName() + " nor " + RawBackgroundFunction.class.getName()); } - Class functionClass = c.asSubclass(BackgroundFunction.class); + Object instance; try { - BackgroundFunction function = functionClass.getConstructor().newInstance(); - return Optional.of(new NewBackgroundFunctionExecutor(function)); + instance = c.getConstructor().newInstance(); } catch (ReflectiveOperationException e) { throw new RuntimeException("Could not construct an instance of " + target + ": " + e, e); } + RawBackgroundFunction function = + (instance instanceof RawBackgroundFunction) + ? (RawBackgroundFunction) instance + : asRaw((BackgroundFunction) instance); + return Optional.of(new NewBackgroundFunctionExecutor(function)); + } + + private static RawBackgroundFunction asRaw(BackgroundFunction backgroundFunction) { + Optional maybeTargetType = backgroundFunctionTypeArgument(backgroundFunction.getClass()); + if (!maybeTargetType.isPresent()) { + // This is probably because the user implemented just BackgroundFunction rather than + // BackgroundFunction. + throw new RuntimeException( + "Could not determine the payload type for BackgroundFunction of type " + + backgroundFunction.getClass().getName() + + "; must implement BackgroundFunction for some T"); + } + return new AsRaw(maybeTargetType.get(), backgroundFunction); + } + + /** + * Returns the {@code T} of a concrete class that implements + * {@link BackgroundFunction BackgroundFunction}. Returns an empty {@link Optional} if + * {@code T} can't be determined. + */ + static Optional backgroundFunctionTypeArgument( + Class functionClass) { + // If this is BackgroundFunction then the user must have implemented a method + // accept(Foo, Context), so we look for that method and return the type of its first argument. + // We must be careful because the compiler will also have added a synthetic method + // accept(Object, Context). + return Arrays.stream(functionClass.getMethods()) + .filter(m -> m.getName().equals("accept") && m.getParameterCount() == 2 + && m.getParameterTypes()[1] == Context.class + && m.getParameterTypes()[0] != Object.class) + .map(m -> m.getGenericParameterTypes()[0]) + .findFirst(); + } + + /** + * Wraps a typed {@link BackgroundFunction} as a {@link RawBackgroundFunction} that takes its + * input JSON string and deserializes it into the payload type of the {@link BackgroundFunction}/ + */ + private static class AsRaw implements RawBackgroundFunction { + private final Gson gson = new Gson(); + private final Type targetType; + private final BackgroundFunction backgroundFunction; + + private AsRaw(Type targetType, BackgroundFunction backgroundFunction) { + this.targetType = targetType; + this.backgroundFunction = backgroundFunction; + } + + @Override + public void accept(String json, Context context) { + T payload; + try { + payload = gson.fromJson(json, targetType); + } catch (JsonParseException e) { + logger.log(Level.WARNING, + "Could not convert payload to target type " + targetType.getTypeName(), e); + return; + } + backgroundFunction.accept(payload, context); + } } /** Executes the user's background function, can handle all HTTP type methods. */ @@ -69,7 +140,7 @@ public void service(HttpServletRequest req, HttpServletResponse res) throws IOEx Event event = gson.fromJson(body, Event.class); try { - function.accept(event.getData(), event.getContext()); + function.accept(gson.toJson(event.getData()), event.getContext()); res.setStatus(HttpServletResponse.SC_OK); } catch (Throwable t) { res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java b/invoker-core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java index 67693e98..31fc3bb9 100644 --- a/invoker-core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java +++ b/invoker-core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java @@ -8,8 +8,8 @@ import com.google.common.collect.ImmutableMap; import com.google.common.io.Files; import com.google.common.io.Resources; +import com.google.gson.Gson; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import java.io.BufferedReader; import java.io.File; import java.io.IOException; @@ -133,35 +133,38 @@ public void newEchoUrl() throws Exception { @Test public void background() throws Exception { - URL resourceUrl = getClass().getResource("/adder_gcf_ga_event.json"); - assertThat(resourceUrl).isNotNull(); - File snoopFile = File.createTempFile("FunctionsIntegrationTest", ".txt"); - snoopFile.deleteOnExit(); - String originalJson = Resources.toString(resourceUrl, StandardCharsets.UTF_8); - JsonObject json = new JsonParser().parse(originalJson).getAsJsonObject(); - JsonObject jsonData = json.getAsJsonObject("data"); - jsonData.addProperty("targetFile", snoopFile.toString()); - testBackgroundFunction("BackgroundSnoop.snoop", - TestCase.builder().setRequestText(json.toString()).build()); - String snooped = Files.asCharSource(snoopFile, StandardCharsets.UTF_8).read(); - JsonObject snoopedJson = new JsonParser().parse(snooped).getAsJsonObject(); - assertThat(snoopedJson).isEqualTo(json); + backgroundTest("BackgroundSnoop.snoop"); } @Test public void newBackground() throws Exception { + backgroundTest("NewBackgroundSnoop"); + } + + @Test + public void newTypedBackground() throws Exception { + backgroundTest("NewTypedBackgroundSnoop"); + } + + // In these tests, we test a number of different functions that express the same functionality + // in different ways. Each function is invoked with a complete HTTP body that looks like a real + // event. We start with a fixed body and insert into its JSON an extra property that tells the + // function where to write what it received. We have to do this since background functions, by + // design, don't return a value. + private void backgroundTest(String functionTarget) throws Exception { + Gson gson = new Gson(); URL resourceUrl = getClass().getResource("/adder_gcf_ga_event.json"); assertThat(resourceUrl).isNotNull(); File snoopFile = File.createTempFile("FunctionsIntegrationTest", ".txt"); snoopFile.deleteOnExit(); String originalJson = Resources.toString(resourceUrl, StandardCharsets.UTF_8); - JsonObject json = new JsonParser().parse(originalJson).getAsJsonObject(); + JsonObject json = gson.fromJson(originalJson, JsonObject.class); JsonObject jsonData = json.getAsJsonObject("data"); jsonData.addProperty("targetFile", snoopFile.toString()); - testBackgroundFunction("NewBackgroundSnoop", + testBackgroundFunction(functionTarget, TestCase.builder().setRequestText(json.toString()).build()); String snooped = Files.asCharSource(snoopFile, StandardCharsets.UTF_8).read(); - JsonObject snoopedJson = new JsonParser().parse(snooped).getAsJsonObject(); + JsonObject snoopedJson = gson.fromJson(snooped, JsonObject.class); assertThat(snoopedJson).isEqualTo(json); } diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutorTest.java b/invoker-core/src/test/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutorTest.java new file mode 100644 index 00000000..59a3a171 --- /dev/null +++ b/invoker-core/src/test/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutorTest.java @@ -0,0 +1,61 @@ +package com.google.cloud.functions.invoker; + +import static com.google.cloud.functions.invoker.NewBackgroundFunctionExecutor.backgroundFunctionTypeArgument; +import static com.google.common.truth.Truth8.assertThat; + +import com.google.cloud.functions.BackgroundFunction; +import com.google.cloud.functions.Context; +import java.util.Map; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class NewBackgroundFunctionExecutorTest { + private static class PubSubMessage { + String data; + Map attributes; + String messageId; + String publishTime; + } + + private static class PubSubFunction implements BackgroundFunction { + @Override public void accept(PubSubMessage payload, Context context) {} + } + + @Test + public void backgroundFunctionTypeArgument_simple() { + assertThat(backgroundFunctionTypeArgument(PubSubFunction.class)).hasValue(PubSubMessage.class); + } + + private abstract static class Parent implements BackgroundFunction {} + + private static class Child extends Parent { + @Override public void accept(PubSubMessage payload, Context context) {} + } + + @Test + public void backgroundFunctionTypeArgument_superclass() { + assertThat(backgroundFunctionTypeArgument(Child.class)).hasValue(PubSubMessage.class); + } + + private interface GenericParent extends BackgroundFunction {} + + private static class GenericChild implements GenericParent { + @Override public void accept(PubSubMessage payload, Context context) {} + } + + @Test + public void backgroundFunctionTypeArgument_genericInterface() { + assertThat(backgroundFunctionTypeArgument(GenericChild.class)).hasValue(PubSubMessage.class); + } + + private static class ForgotTypeParameter implements BackgroundFunction { + @Override public void accept(Object payload, Context context) {} + } + + @Test + public void backgroundFunctionTypeArgument_raw() { + assertThat(backgroundFunctionTypeArgument(ForgotTypeParameter.class)).isEmpty(); + } +} diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewBackgroundSnoop.java b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewBackgroundSnoop.java index 26be2b5a..e20123fa 100644 --- a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewBackgroundSnoop.java +++ b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewBackgroundSnoop.java @@ -1,10 +1,9 @@ package com.google.cloud.functions.invoker.testfunctions; -import com.google.cloud.functions.BackgroundFunction; import com.google.cloud.functions.Context; -import com.google.gson.JsonElement; +import com.google.cloud.functions.RawBackgroundFunction; +import com.google.gson.Gson; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; @@ -17,21 +16,23 @@ * identical to the JSON payload that the Functions Framework received from the client in the test. * This will need to be rewritten when we switch to CloudEvents. */ -public class NewBackgroundSnoop implements BackgroundFunction { +public class NewBackgroundSnoop implements RawBackgroundFunction { @Override - public void accept(JsonElement jsonElement, Context context) { - String targetFile = jsonElement.getAsJsonObject().get("targetFile").getAsString(); + public void accept(String json, Context context) { + Gson gson = new Gson(); + JsonObject jsonObject = gson.fromJson(json, JsonObject.class); + String targetFile = jsonObject.get("targetFile").getAsString(); if (targetFile == null) { throw new IllegalArgumentException("Expected targetFile in JSON payload"); } - JsonObject resourceJson = new JsonParser().parse(context.resource()).getAsJsonObject(); + JsonObject resourceJson = gson.fromJson(context.resource(), JsonObject.class); JsonObject contextJson = new JsonObject(); contextJson.addProperty("eventId", context.eventId()); contextJson.addProperty("timestamp", context.timestamp()); contextJson.addProperty("eventType", context.eventType()); contextJson.add("resource", resourceJson); JsonObject contextAndPayloadJson = new JsonObject(); - contextAndPayloadJson.add("data", jsonElement); + contextAndPayloadJson.add("data", jsonObject); contextAndPayloadJson.add("context", contextJson); try (FileWriter fileWriter = new FileWriter(targetFile); PrintWriter writer = new PrintWriter(fileWriter)) { diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewTypedBackgroundSnoop.java b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewTypedBackgroundSnoop.java new file mode 100644 index 00000000..14ca67d1 --- /dev/null +++ b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewTypedBackgroundSnoop.java @@ -0,0 +1,50 @@ +package com.google.cloud.functions.invoker.testfunctions; + +import com.google.cloud.functions.BackgroundFunction; +import com.google.cloud.functions.Context; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.UncheckedIOException; + +/** + * Extract the targetFile property from the data of the JSON payload, and write to it a JSON + * encoding of this payload and the context. The JSON format is chosen to be identical to the + * EventFlow format that we currently use in GCF, and the file that we write should in fact be + * identical to the JSON payload that the Functions Framework received from the client in the test. + * This will need to be rewritten when we switch to CloudEvents. + */ +public class NewTypedBackgroundSnoop + implements BackgroundFunction { + static class Payload { + int a; + int b; + String targetFile; + } + + @Override + public void accept(Payload payload, Context context) { + Gson gson = new Gson(); + String targetFile = payload.targetFile; + if (targetFile == null) { + throw new IllegalArgumentException("Expected targetFile in JSON payload"); + } + JsonObject resourceJson = gson.fromJson(context.resource(), JsonObject.class); + JsonObject contextJson = new JsonObject(); + contextJson.addProperty("eventId", context.eventId()); + contextJson.addProperty("timestamp", context.timestamp()); + contextJson.addProperty("eventType", context.eventType()); + contextJson.add("resource", resourceJson); + JsonObject contextAndPayloadJson = new JsonObject(); + contextAndPayloadJson.add("data", gson.toJsonTree(payload)); + contextAndPayloadJson.add("context", contextJson); + try (FileWriter fileWriter = new FileWriter(targetFile); + PrintWriter writer = new PrintWriter(fileWriter)) { + writer.println(contextAndPayloadJson); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} From ee214a4d8b5e19be24105b7298452f6f4c50da61 Mon Sep 17 00:00:00 2001 From: emcmanus Date: Tue, 12 Nov 2019 15:59:22 -0800 Subject: [PATCH 015/364] Import of Cloud Functions JVM from Git-on-Borg. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - c1328f640791a360d414deccb47f873a76dee171 Merge "Improve coverage of packageless and nested functio... by Éamonn McManus - 52bae0b45618d9f3bd19b8f97acbcbb5690293ad Clean up the invoker-runner pom.xml. by Éamonn McManus - 8c9223dc6ddba8e639f23f85dfe9e607c703fc33 Explicitly set source level in example. by Éamonn McManus - f573a69eb1588502330c8775f0dd9a11364e556b Update example to reflect the new API. by Éamonn McManus PiperOrigin-RevId: 280075940 --- .../invoker/NewHttpFunctionExecutor.java | 18 +++++++-- .../src/test/java/PackagelessHelloWorld.java | 13 +++++++ .../functions/invoker/IntegrationTest.java | 38 +++++++++++++------ .../invoker/testfunctions/Nested.java | 18 +++++++++ 4 files changed, 72 insertions(+), 15 deletions(-) create mode 100644 invoker-core/src/test/java/PackagelessHelloWorld.java create mode 100644 invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Nested.java diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java index 499cce39..dc897950 100644 --- a/invoker-core/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java @@ -33,10 +33,20 @@ private NewHttpFunctionExecutor(HttpFunction function) { */ public static Optional forTarget(String target) { Class c; - try { - c = Class.forName(target); - } catch (ClassNotFoundException e) { - return Optional.empty(); + while (true) { + try { + c = Class.forName(target); + break; + } catch (ClassNotFoundException e) { + // This might be a nested class like com.example.Foo.Bar. That will actually appear as + // com.example.Foo$Bar as far as Class.forName is concerned. So we try to replace every dot + // from the last to the first with a $ in the hope of finding a class we can load. + int lastDot = target.lastIndexOf('.'); + if (lastDot < 0) { + return Optional.empty(); + } + target = target.substring(0, lastDot) + '$' + target.substring(lastDot + 1); + } } if (!HttpFunction.class.isAssignableFrom(c)) { throw new RuntimeException( diff --git a/invoker-core/src/test/java/PackagelessHelloWorld.java b/invoker-core/src/test/java/PackagelessHelloWorld.java new file mode 100644 index 00000000..5a41bc5f --- /dev/null +++ b/invoker-core/src/test/java/PackagelessHelloWorld.java @@ -0,0 +1,13 @@ +// A function in the default package. + +import com.google.cloud.functions.HttpFunction; +import com.google.cloud.functions.HttpRequest; +import com.google.cloud.functions.HttpResponse; + +public class PackagelessHelloWorld implements HttpFunction { + @Override + public void service(HttpRequest request, HttpResponse response) throws Exception { + response.setContentType("text/plain; charset=utf-8"); + response.getWriter().write("hello, world\n"); + } +} diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java b/invoker-core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java index 31fc3bb9..aa2441eb 100644 --- a/invoker-core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java +++ b/invoker-core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java @@ -87,29 +87,33 @@ abstract static class Builder { } } + private static String fullTarget(String nameWithoutPackage) { + return "com.google.cloud.functions.invoker.testfunctions." + nameWithoutPackage; + } + @Test public void helloWorld() throws Exception { - testHttpFunction("HelloWorld.helloWorld", + testHttpFunction(fullTarget("HelloWorld.helloWorld"), TestCase.builder().setExpectedResponseText("hello\n").build()); } @Test public void newHelloWorld() throws Exception { - testHttpFunction("NewHelloWorld", + testHttpFunction(fullTarget("NewHelloWorld"), TestCase.builder().setExpectedResponseText("hello\n").build()); } @Test public void echo() throws Exception { String testText = "hello\nworld\n"; - testHttpFunction("Echo.echo", + testHttpFunction(fullTarget("Echo.echo"), TestCase.builder().setRequestText(testText).setExpectedResponseText(testText).build()); } @Test public void newEcho() throws Exception { String testText = "hello\nworld\n"; - testHttpFunction("NewEcho", + testHttpFunction(fullTarget("NewEcho"), TestCase.builder().setRequestText(testText).setExpectedResponseText(testText).build()); } @@ -119,7 +123,7 @@ public void echoUrl() throws Exception { TestCase[] testCases = Arrays.stream(testUrls) .map(url -> TestCase.builder().setUrl(url).setExpectedResponseText(url + "\n").build()) .toArray(TestCase[]::new); - testHttpFunction("EchoUrl.echoUrl", testCases); + testHttpFunction(fullTarget("EchoUrl.echoUrl"), testCases); } @Test @@ -128,22 +132,35 @@ public void newEchoUrl() throws Exception { TestCase[] testCases = Arrays.stream(testUrls) .map(url -> TestCase.builder().setUrl(url).setExpectedResponseText(url + "\n").build()) .toArray(TestCase[]::new); - testHttpFunction("NewEchoUrl", testCases); + testHttpFunction(fullTarget("NewEchoUrl"), testCases); } @Test public void background() throws Exception { - backgroundTest("BackgroundSnoop.snoop"); + backgroundTest(fullTarget("BackgroundSnoop.snoop")); } @Test public void newBackground() throws Exception { - backgroundTest("NewBackgroundSnoop"); + backgroundTest(fullTarget("NewBackgroundSnoop")); } @Test public void newTypedBackground() throws Exception { - backgroundTest("NewTypedBackgroundSnoop"); + backgroundTest(fullTarget("NewTypedBackgroundSnoop")); + } + + @Test + public void nested() throws Exception { + String testText = "sic transit gloria mundi"; + testHttpFunction(fullTarget("Nested.Echo"), + TestCase.builder().setRequestText(testText).setExpectedResponseText(testText).build()); + } + + @Test + public void packageless() throws Exception { + testHttpFunction("PackagelessHelloWorld", + TestCase.builder().setExpectedResponseText("hello, world\n").build()); } // In these tests, we test a number of different functions that express the same functionality @@ -218,7 +235,6 @@ public String toString() { private Process startServer(SignatureType signatureType, String target) throws IOException, InterruptedException { - String fullTarget = "com.google.cloud.functions.invoker.testfunctions." + target; File javaHome = new File(System.getProperty("java.home")); assertThat(javaHome.exists()).isTrue(); File javaBin = new File(javaHome, "bin"); @@ -235,7 +251,7 @@ private Process startServer(SignatureType signatureType, String target) Map environment = ImmutableMap.of("PORT", String.valueOf(serverPort), "K_SERVICE", "test-function", "FUNCTION_SIGNATURE_TYPE", signatureType.toString(), - "FUNCTION_TARGET", fullTarget); + "FUNCTION_TARGET", target); processBuilder.environment().putAll(environment); Process serverProcess = processBuilder.start(); CountDownLatch ready = new CountDownLatch(1); diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Nested.java b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Nested.java new file mode 100644 index 00000000..62710891 --- /dev/null +++ b/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Nested.java @@ -0,0 +1,18 @@ +package com.google.cloud.functions.invoker.testfunctions; + +import com.google.cloud.functions.HttpFunction; +import com.google.cloud.functions.HttpRequest; +import com.google.cloud.functions.HttpResponse; +import java.util.stream.Collectors; + +public class Nested { + public static class Echo implements HttpFunction { + @Override + public void service(HttpRequest request, HttpResponse response) throws Exception { + String body = request.getReader().lines().collect(Collectors.joining("\n")); + response.setContentType("text/plain"); + response.getWriter().write(body); + response.getWriter().flush(); + } + } +} From d8417f3e8346fbc704ef09044986026c774b8126 Mon Sep 17 00:00:00 2001 From: emcmanus Date: Thu, 19 Dec 2019 08:32:15 -0800 Subject: [PATCH 016/364] Import of Cloud Functions JVM from Git-on-Borg. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - f1db675d9af38369700dc3434ef500749c6fe6aa Build with JDK11 not JDK8. by Éamonn McManus - 4588713b3c7c168da51eb88dc0c1dc659cc55216 Change how exceptions are logged. by Éamonn McManus PiperOrigin-RevId: 286397821 --- .../cloud/functions/invoker/NewHttpFunctionExecutor.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java b/invoker-core/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java index dc897950..02855474 100644 --- a/invoker-core/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java +++ b/invoker-core/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java @@ -70,8 +70,12 @@ public void service(HttpServletRequest req, HttpServletResponse res) { try { function.service(reqImpl, respImpl); } catch (Throwable t) { + // TODO(b/146510646): this should be logged properly as an exception, but that currently + // causes integration tests to fail. + // logger.log(Level.WARNING, "Failed to execute " + function.getClass().getName(), t); + logger.log(Level.WARNING, "Failed to execute {0}", function.getClass().getName()); + t.printStackTrace(); res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - logger.log(Level.WARNING, "Failed to execute " + function.getClass().getName(), t); } finally { try { respImpl.getWriter().flush(); From 3f1fc81c87830d07b6df899406e6f8c31e5bbc19 Mon Sep 17 00:00:00 2001 From: emcmanus Date: Fri, 3 Jan 2020 15:35:34 -0800 Subject: [PATCH 017/364] Import of Cloud Functions JVM from Git-on-Borg. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fd7e4c100b9bf98a8e437204417a6218695d498e Change Servlet API version to 3.1.0. by Éamonn McManus - 1c014f29c9cbf2a99f6f736a0e27ae9284de229b Move the GCF invoker into a separate Maven module by Éamonn McManus PiperOrigin-RevId: 288060394 --- invoker-core/core/pom.xml | 170 ++++++++++++++++++ .../invoker/BackgroundCloudFunction.java | 0 .../invoker/BackgroundFunctionExecutor.java | 0 .../BackgroundFunctionSignatureMatcher.java | 0 .../functions/invoker/CloudFunction.java | 0 .../invoker/CloudFunctionsContext.java | 0 .../google/cloud/functions/invoker/Event.java | 0 .../functions/invoker/FunctionLoader.java | 0 .../invoker/FunctionSignatureMatcher.java | 0 .../functions/invoker/HttpCloudFunction.java | 0 .../invoker/HttpFunctionExecutor.java | 0 .../invoker/HttpFunctionSignatureMatcher.java | 0 .../NewBackgroundFunctionExecutor.java | 0 .../invoker/NewHttpFunctionExecutor.java | 0 .../functions/invoker/URLRequestWrapper.java | 0 .../invoker/http/HttpRequestImpl.java | 0 .../invoker/http/HttpResponseImpl.java | 0 .../functions/invoker/runner/Invoker.java | 0 .../src/main/resources/logging.properties | 0 .../src/test/java/PackagelessHelloWorld.java | 0 .../invoker/BackgroundFunctionTest.java | 0 .../functions/invoker/HttpFunctionTest.java | 0 .../functions/invoker/IntegrationTest.java | 0 .../NewBackgroundFunctionExecutorTest.java | 0 .../functions/invoker/http/HttpTest.java | 0 .../testfunctions/BackgroundSnoop.java | 0 .../functions/invoker/testfunctions/Echo.java | 0 .../invoker/testfunctions/EchoUrl.java | 0 .../invoker/testfunctions/HelloWorld.java | 0 .../invoker/testfunctions/Nested.java | 0 .../testfunctions/NewBackgroundSnoop.java | 0 .../invoker/testfunctions/NewEcho.java | 0 .../invoker/testfunctions/NewEchoUrl.java | 0 .../invoker/testfunctions/NewHelloWorld.java | 0 .../NewTypedBackgroundSnoop.java | 0 .../test/resources/adder_gcf_beta_event.json | 0 .../adder_gcf_beta_event_json_resource.json | 0 .../test/resources/adder_gcf_ga_event.json | 0 invoker-core/pom.xml | 167 +++-------------- 39 files changed, 190 insertions(+), 147 deletions(-) create mode 100644 invoker-core/core/pom.xml rename invoker-core/{ => core}/src/main/java/com/google/cloud/functions/invoker/BackgroundCloudFunction.java (100%) rename invoker-core/{ => core}/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java (100%) rename invoker-core/{ => core}/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionSignatureMatcher.java (100%) rename invoker-core/{ => core}/src/main/java/com/google/cloud/functions/invoker/CloudFunction.java (100%) rename invoker-core/{ => core}/src/main/java/com/google/cloud/functions/invoker/CloudFunctionsContext.java (100%) rename invoker-core/{ => core}/src/main/java/com/google/cloud/functions/invoker/Event.java (100%) rename invoker-core/{ => core}/src/main/java/com/google/cloud/functions/invoker/FunctionLoader.java (100%) rename invoker-core/{ => core}/src/main/java/com/google/cloud/functions/invoker/FunctionSignatureMatcher.java (100%) rename invoker-core/{ => core}/src/main/java/com/google/cloud/functions/invoker/HttpCloudFunction.java (100%) rename invoker-core/{ => core}/src/main/java/com/google/cloud/functions/invoker/HttpFunctionExecutor.java (100%) rename invoker-core/{ => core}/src/main/java/com/google/cloud/functions/invoker/HttpFunctionSignatureMatcher.java (100%) rename invoker-core/{ => core}/src/main/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutor.java (100%) rename invoker-core/{ => core}/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java (100%) rename invoker-core/{ => core}/src/main/java/com/google/cloud/functions/invoker/URLRequestWrapper.java (100%) rename invoker-core/{ => core}/src/main/java/com/google/cloud/functions/invoker/http/HttpRequestImpl.java (100%) rename invoker-core/{ => core}/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java (100%) rename invoker-core/{ => core}/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java (100%) rename invoker-core/{ => core}/src/main/resources/logging.properties (100%) rename invoker-core/{ => core}/src/test/java/PackagelessHelloWorld.java (100%) rename invoker-core/{ => core}/src/test/java/com/google/cloud/functions/invoker/BackgroundFunctionTest.java (100%) rename invoker-core/{ => core}/src/test/java/com/google/cloud/functions/invoker/HttpFunctionTest.java (100%) rename invoker-core/{ => core}/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java (100%) rename invoker-core/{ => core}/src/test/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutorTest.java (100%) rename invoker-core/{ => core}/src/test/java/com/google/cloud/functions/invoker/http/HttpTest.java (100%) rename invoker-core/{ => core}/src/test/java/com/google/cloud/functions/invoker/testfunctions/BackgroundSnoop.java (100%) rename invoker-core/{ => core}/src/test/java/com/google/cloud/functions/invoker/testfunctions/Echo.java (100%) rename invoker-core/{ => core}/src/test/java/com/google/cloud/functions/invoker/testfunctions/EchoUrl.java (100%) rename invoker-core/{ => core}/src/test/java/com/google/cloud/functions/invoker/testfunctions/HelloWorld.java (100%) rename invoker-core/{ => core}/src/test/java/com/google/cloud/functions/invoker/testfunctions/Nested.java (100%) rename invoker-core/{ => core}/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewBackgroundSnoop.java (100%) rename invoker-core/{ => core}/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewEcho.java (100%) rename invoker-core/{ => core}/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewEchoUrl.java (100%) rename invoker-core/{ => core}/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewHelloWorld.java (100%) rename invoker-core/{ => core}/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewTypedBackgroundSnoop.java (100%) rename invoker-core/{ => core}/src/test/resources/adder_gcf_beta_event.json (100%) rename invoker-core/{ => core}/src/test/resources/adder_gcf_beta_event_json_resource.json (100%) rename invoker-core/{ => core}/src/test/resources/adder_gcf_ga_event.json (100%) diff --git a/invoker-core/core/pom.xml b/invoker-core/core/pom.xml new file mode 100644 index 00000000..837449c3 --- /dev/null +++ b/invoker-core/core/pom.xml @@ -0,0 +1,170 @@ + + 4.0.0 + + + com.google.cloud.functions.invoker + java-function-invoker-core-parent + 1.0.0-alpha-1 + + + com.google.cloud.functions.invoker + java-function-invoker-core + 1.0.0-alpha-1 + GCF Java Invoker + + Application that invokes a GCF Java function. This application is a + complete HTTP server that interprets incoming HTTP requests appropriately + and forwards them to the function code. + + + + UTF-8 + 3.8.0 + 5.3.2 + + + + + com.google.cloud.functions + functions-framework-api + + + javax.servlet + javax.servlet-api + 3.1.0 + + + javax.annotation + javax.annotation-api + 1.3.2 + + + com.google.code.gson + gson + 2.8.6 + + + com.ryanharter.auto.value + auto-value-gson + 0.8.0 + provided + + + com.ryanharter.auto.value + auto-value-gson-annotations + 0.8.0 + provided + + + com.google.auto.value + auto-value + 1.6.2 + provided + + + com.google.auto.value + auto-value-annotations + 1.6.2 + provided + + + org.eclipse.jetty + jetty-servlet + 9.4.11.v20180605 + + + org.eclipse.jetty + jetty-server + 9.4.11.v20180605 + + + commons-cli + commons-cli + 1.4 + + + + + org.mockito + mockito-core + 2.21.0 + test + + + org.mockito + mockito-junit-jupiter + 2.23.0 + test + + + junit + junit + 4.12 + test + + + com.google.truth + truth + 1.0 + test + + + com.google.truth.extensions + truth-java8-extension + 1.0 + test + + + org.eclipse.jetty + jetty-client + 9.4.11.v20180605 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + 1.8 + 1.8 + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.1 + + + package + + shade + + + + + com + shaded.com.google.cloud.functions + + com.google.cloud.functions.** + com.google.gson.** + + + + + + + com.google.cloud.functions.invoker.runner.Invoker + + + + + + + + + diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/BackgroundCloudFunction.java b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/BackgroundCloudFunction.java similarity index 100% rename from invoker-core/src/main/java/com/google/cloud/functions/invoker/BackgroundCloudFunction.java rename to invoker-core/core/src/main/java/com/google/cloud/functions/invoker/BackgroundCloudFunction.java diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java similarity index 100% rename from invoker-core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java rename to invoker-core/core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutor.java diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionSignatureMatcher.java b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionSignatureMatcher.java similarity index 100% rename from invoker-core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionSignatureMatcher.java rename to invoker-core/core/src/main/java/com/google/cloud/functions/invoker/BackgroundFunctionSignatureMatcher.java diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/CloudFunction.java b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/CloudFunction.java similarity index 100% rename from invoker-core/src/main/java/com/google/cloud/functions/invoker/CloudFunction.java rename to invoker-core/core/src/main/java/com/google/cloud/functions/invoker/CloudFunction.java diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/CloudFunctionsContext.java b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/CloudFunctionsContext.java similarity index 100% rename from invoker-core/src/main/java/com/google/cloud/functions/invoker/CloudFunctionsContext.java rename to invoker-core/core/src/main/java/com/google/cloud/functions/invoker/CloudFunctionsContext.java diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/Event.java b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/Event.java similarity index 100% rename from invoker-core/src/main/java/com/google/cloud/functions/invoker/Event.java rename to invoker-core/core/src/main/java/com/google/cloud/functions/invoker/Event.java diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/FunctionLoader.java b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/FunctionLoader.java similarity index 100% rename from invoker-core/src/main/java/com/google/cloud/functions/invoker/FunctionLoader.java rename to invoker-core/core/src/main/java/com/google/cloud/functions/invoker/FunctionLoader.java diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/FunctionSignatureMatcher.java b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/FunctionSignatureMatcher.java similarity index 100% rename from invoker-core/src/main/java/com/google/cloud/functions/invoker/FunctionSignatureMatcher.java rename to invoker-core/core/src/main/java/com/google/cloud/functions/invoker/FunctionSignatureMatcher.java diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/HttpCloudFunction.java b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/HttpCloudFunction.java similarity index 100% rename from invoker-core/src/main/java/com/google/cloud/functions/invoker/HttpCloudFunction.java rename to invoker-core/core/src/main/java/com/google/cloud/functions/invoker/HttpCloudFunction.java diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionExecutor.java b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionExecutor.java similarity index 100% rename from invoker-core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionExecutor.java rename to invoker-core/core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionExecutor.java diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionSignatureMatcher.java b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionSignatureMatcher.java similarity index 100% rename from invoker-core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionSignatureMatcher.java rename to invoker-core/core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionSignatureMatcher.java diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutor.java b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutor.java similarity index 100% rename from invoker-core/src/main/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutor.java rename to invoker-core/core/src/main/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutor.java diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java similarity index 100% rename from invoker-core/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java rename to invoker-core/core/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/URLRequestWrapper.java b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/URLRequestWrapper.java similarity index 100% rename from invoker-core/src/main/java/com/google/cloud/functions/invoker/URLRequestWrapper.java rename to invoker-core/core/src/main/java/com/google/cloud/functions/invoker/URLRequestWrapper.java diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/http/HttpRequestImpl.java b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/http/HttpRequestImpl.java similarity index 100% rename from invoker-core/src/main/java/com/google/cloud/functions/invoker/http/HttpRequestImpl.java rename to invoker-core/core/src/main/java/com/google/cloud/functions/invoker/http/HttpRequestImpl.java diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java similarity index 100% rename from invoker-core/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java rename to invoker-core/core/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java diff --git a/invoker-core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java similarity index 100% rename from invoker-core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java rename to invoker-core/core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java diff --git a/invoker-core/src/main/resources/logging.properties b/invoker-core/core/src/main/resources/logging.properties similarity index 100% rename from invoker-core/src/main/resources/logging.properties rename to invoker-core/core/src/main/resources/logging.properties diff --git a/invoker-core/src/test/java/PackagelessHelloWorld.java b/invoker-core/core/src/test/java/PackagelessHelloWorld.java similarity index 100% rename from invoker-core/src/test/java/PackagelessHelloWorld.java rename to invoker-core/core/src/test/java/PackagelessHelloWorld.java diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/BackgroundFunctionTest.java b/invoker-core/core/src/test/java/com/google/cloud/functions/invoker/BackgroundFunctionTest.java similarity index 100% rename from invoker-core/src/test/java/com/google/cloud/functions/invoker/BackgroundFunctionTest.java rename to invoker-core/core/src/test/java/com/google/cloud/functions/invoker/BackgroundFunctionTest.java diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/HttpFunctionTest.java b/invoker-core/core/src/test/java/com/google/cloud/functions/invoker/HttpFunctionTest.java similarity index 100% rename from invoker-core/src/test/java/com/google/cloud/functions/invoker/HttpFunctionTest.java rename to invoker-core/core/src/test/java/com/google/cloud/functions/invoker/HttpFunctionTest.java diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java b/invoker-core/core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java similarity index 100% rename from invoker-core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java rename to invoker-core/core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutorTest.java b/invoker-core/core/src/test/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutorTest.java similarity index 100% rename from invoker-core/src/test/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutorTest.java rename to invoker-core/core/src/test/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutorTest.java diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/http/HttpTest.java b/invoker-core/core/src/test/java/com/google/cloud/functions/invoker/http/HttpTest.java similarity index 100% rename from invoker-core/src/test/java/com/google/cloud/functions/invoker/http/HttpTest.java rename to invoker-core/core/src/test/java/com/google/cloud/functions/invoker/http/HttpTest.java diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/BackgroundSnoop.java b/invoker-core/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/BackgroundSnoop.java similarity index 100% rename from invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/BackgroundSnoop.java rename to invoker-core/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/BackgroundSnoop.java diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Echo.java b/invoker-core/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Echo.java similarity index 100% rename from invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Echo.java rename to invoker-core/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Echo.java diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/EchoUrl.java b/invoker-core/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/EchoUrl.java similarity index 100% rename from invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/EchoUrl.java rename to invoker-core/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/EchoUrl.java diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/HelloWorld.java b/invoker-core/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/HelloWorld.java similarity index 100% rename from invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/HelloWorld.java rename to invoker-core/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/HelloWorld.java diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Nested.java b/invoker-core/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Nested.java similarity index 100% rename from invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Nested.java rename to invoker-core/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Nested.java diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewBackgroundSnoop.java b/invoker-core/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewBackgroundSnoop.java similarity index 100% rename from invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewBackgroundSnoop.java rename to invoker-core/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewBackgroundSnoop.java diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewEcho.java b/invoker-core/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewEcho.java similarity index 100% rename from invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewEcho.java rename to invoker-core/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewEcho.java diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewEchoUrl.java b/invoker-core/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewEchoUrl.java similarity index 100% rename from invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewEchoUrl.java rename to invoker-core/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewEchoUrl.java diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewHelloWorld.java b/invoker-core/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewHelloWorld.java similarity index 100% rename from invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewHelloWorld.java rename to invoker-core/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewHelloWorld.java diff --git a/invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewTypedBackgroundSnoop.java b/invoker-core/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewTypedBackgroundSnoop.java similarity index 100% rename from invoker-core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewTypedBackgroundSnoop.java rename to invoker-core/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/NewTypedBackgroundSnoop.java diff --git a/invoker-core/src/test/resources/adder_gcf_beta_event.json b/invoker-core/core/src/test/resources/adder_gcf_beta_event.json similarity index 100% rename from invoker-core/src/test/resources/adder_gcf_beta_event.json rename to invoker-core/core/src/test/resources/adder_gcf_beta_event.json diff --git a/invoker-core/src/test/resources/adder_gcf_beta_event_json_resource.json b/invoker-core/core/src/test/resources/adder_gcf_beta_event_json_resource.json similarity index 100% rename from invoker-core/src/test/resources/adder_gcf_beta_event_json_resource.json rename to invoker-core/core/src/test/resources/adder_gcf_beta_event_json_resource.json diff --git a/invoker-core/src/test/resources/adder_gcf_ga_event.json b/invoker-core/core/src/test/resources/adder_gcf_ga_event.json similarity index 100% rename from invoker-core/src/test/resources/adder_gcf_ga_event.json rename to invoker-core/core/src/test/resources/adder_gcf_ga_event.json diff --git a/invoker-core/pom.xml b/invoker-core/pom.xml index 335f5c12..9ce731d7 100644 --- a/invoker-core/pom.xml +++ b/invoker-core/pom.xml @@ -1,158 +1,31 @@ 4.0.0 com.google.cloud.functions.invoker - java-function-invoker-core + java-function-invoker-core-parent 1.0.0-alpha-1 + pom + GCF Java Invoker Parent + + Parent POM for the GCF Java Invoker. The project is structured like this so + that we can have modules that build jar files for use in tests. + + + + core + UTF-8 3.8.0 - 5.3.2 - - - com.google.cloud.functions - functions-framework-api - 1.0.0-alpha-2-rc3 - - - javax.servlet - javax.servlet-api - 4.0.1 - - - javax.annotation - javax.annotation-api - 1.3.2 - - - com.google.code.gson - gson - 2.8.6 - - - com.ryanharter.auto.value - auto-value-gson - 0.8.0 - provided - - - com.ryanharter.auto.value - auto-value-gson-annotations - 0.8.0 - provided - - - com.google.auto.value - auto-value - 1.6.2 - provided - - - com.google.auto.value - auto-value-annotations - 1.6.2 - provided - - - org.eclipse.jetty - jetty-servlet - 9.4.11.v20180605 - - - org.eclipse.jetty - jetty-server - 9.4.11.v20180605 - - - commons-cli - commons-cli - 1.4 - - - - - org.mockito - mockito-core - 2.21.0 - test - - - org.mockito - mockito-junit-jupiter - 2.23.0 - test - - - junit - junit - 4.12 - test - - - com.google.truth - truth - 1.0 - test - - - com.google.truth.extensions - truth-java8-extension - 1.0 - test - - - org.eclipse.jetty - jetty-client - 9.4.11.v20180605 - test - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - ${maven-compiler-plugin.version} - - 1.8 - 1.8 - - - - - org.apache.maven.plugins - maven-shade-plugin - 3.2.1 - - - package - - shade - - - - - com - shaded.com.google.cloud.functions - - com.google.cloud.functions.** - com.google.gson.** - - - - - - - com.google.cloud.functions.invoker.runner.Invoker - - - - - - - - + + + + com.google.cloud.functions + functions-framework-api + 1.0.0-alpha-2-rc3 + + + From 599030933b17fec79f9ec9694cc79d5685436fbb Mon Sep 17 00:00:00 2001 From: emcmanus Date: Tue, 7 Jan 2020 07:53:38 -0800 Subject: [PATCH 018/364] Import of Cloud Functions JVM from Git-on-Borg. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - b56a91a933b6f438b06ca65fe70b31105fa15fbc Hide the runtime from functions loaded via -jar. by Éamonn McManus PiperOrigin-RevId: 288496834 --- invoker-core/core/pom.xml | 18 ++-- .../NewBackgroundFunctionExecutor.java | 5 +- .../invoker/NewHttpFunctionExecutor.java | 4 +- .../functions/invoker/runner/Invoker.java | 57 ++++++++++-- .../functions/invoker/IntegrationTest.java | 92 ++++++++++++++++--- invoker-core/functionjar/pom.xml | 84 +++++++++++++++++ .../java/com/example/functionjar/Main.java | 0 .../com/example/functionjar/Background.java | 30 ++++++ .../java/com/example/functionjar/Checker.java | 20 ++++ .../com/example/functionjar/Foreground.java | 20 ++++ invoker-core/pom.xml | 5 +- 11 files changed, 300 insertions(+), 35 deletions(-) create mode 100644 invoker-core/functionjar/pom.xml create mode 100644 invoker-core/functionjar/src/main/java/com/example/functionjar/Main.java create mode 100644 invoker-core/functionjar/src/test/java/com/example/functionjar/Background.java create mode 100644 invoker-core/functionjar/src/test/java/com/example/functionjar/Checker.java create mode 100644 invoker-core/functionjar/src/test/java/com/example/functionjar/Foreground.java diff --git a/invoker-core/core/pom.xml b/invoker-core/core/pom.xml index 837449c3..32474395 100644 --- a/invoker-core/core/pom.xml +++ b/invoker-core/core/pom.xml @@ -19,7 +19,6 @@ UTF-8 - 3.8.0 5.3.2 @@ -84,6 +83,13 @@ + + com.google.cloud.functions.invoker + java-function-invoker-core-functionjar + 1.0.0-alpha-1 + test-jar + test + org.mockito mockito-core @@ -124,18 +130,8 @@ - - org.apache.maven.plugins - maven-compiler-plugin - ${maven-compiler-plugin.version} - - 1.8 - 1.8 - - - org.apache.maven.plugins maven-shade-plugin 3.2.1 diff --git a/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutor.java b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutor.java index 43b33456..dba98924 100644 --- a/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutor.java +++ b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/NewBackgroundFunctionExecutor.java @@ -38,10 +38,11 @@ private NewBackgroundFunctionExecutor(RawBackgroundFunction function) { * either the class does not implement {@link RawBackgroundFunction} or we are unable to * construct an instance using its no-arg constructor. */ - public static Optional forTarget(String target) { + public static Optional forTarget( + String target, ClassLoader loader) { Class c; try { - c = Class.forName(target); + c = loader.loadClass(target); } catch (ClassNotFoundException e) { return Optional.empty(); } diff --git a/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java index 02855474..59561f7d 100644 --- a/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java +++ b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/NewHttpFunctionExecutor.java @@ -31,11 +31,11 @@ private NewHttpFunctionExecutor(HttpFunction function) { * either the class does not implement {@link HttpFunction} or we are unable to construct an * instance using its no-arg constructor. */ - public static Optional forTarget(String target) { + public static Optional forTarget(String target, ClassLoader loader) { Class c; while (true) { try { - c = Class.forName(target); + c = loader.loadClass(target); break; } catch (ClassNotFoundException e) { // This might be a nested class like com.example.Foo.Bar. That will actually appear as diff --git a/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java index c6000699..a79949da 100644 --- a/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java +++ b/invoker-core/core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java @@ -11,6 +11,7 @@ import com.google.cloud.functions.invoker.NewHttpFunctionExecutor; import java.io.File; import java.io.IOException; +import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.util.Arrays; @@ -139,24 +140,23 @@ public void startServer() throws Exception { : Optional.empty(); if (functionJarFile.isPresent() && !functionJarFile.get().exists()) { throw new IllegalArgumentException( - "functionJarPath points to an non-existing file: " + "functionJarPath points to an non-existent file: " + functionJarFile.get().getAbsolutePath()); } + ClassLoader runtimeLoader = getClass().getClassLoader(); ClassLoader classLoader; if (functionJarFile.isPresent()) { - classLoader = - new URLClassLoader( - new URL[]{functionJarFile.get().toURI().toURL()}, - Thread.currentThread().getContextClassLoader()); + ClassLoader parent = new OnlyApiClassLoader(runtimeLoader); + classLoader = new URLClassLoader(new URL[]{functionJarFile.get().toURI().toURL()}, parent); } else { - classLoader = Thread.currentThread().getContextClassLoader(); + classLoader = runtimeLoader; } if ("http".equals(functionSignatureType)) { HttpServlet servlet; Optional newExecutor = - NewHttpFunctionExecutor.forTarget(functionTarget); + NewHttpFunctionExecutor.forTarget(functionTarget, classLoader); if (newExecutor.isPresent()) { servlet = newExecutor.get(); } else { @@ -169,7 +169,7 @@ public void startServer() throws Exception { } else if ("event".equals(functionSignatureType)) { HttpServlet servlet; Optional newExecutor = - NewBackgroundFunctionExecutor.forTarget(functionTarget); + NewBackgroundFunctionExecutor.forTarget(functionTarget, classLoader); if (newExecutor.isPresent()) { servlet = newExecutor.get(); } else { @@ -196,4 +196,45 @@ private void logServerInfo() { logger.log(Level.INFO, "URL: http://localhost:{0,number,#}/", port); } } + + /** + * A loader that only loads GCF API classes. Those are classes whose package is exactly + * {@code com.google.cloud.functions}. The package can't be a subpackage, such as + * {@code com.google.cloud.functions.invoker}. + * + *

This loader allows us to load the classes from a user function, without making the + * runtime classes visible to them. We will make this loader the parent of the + * {@link URLClassLoader} that loads the user code in order to filter out those runtime classes. + * + *

The reason we do need to share the API classes between the runtime and the user function is + * so that the runtime can instantiate the function class and cast it to + * {@link com.google.cloud.functions.HttpFunction} or whatever. + */ + private static class OnlyApiClassLoader extends ClassLoader { + private final ClassLoader runtimeClassLoader; + + OnlyApiClassLoader(ClassLoader runtimeClassLoader) { + super(getSystemOrBootstrapClassLoader()); + this.runtimeClassLoader = runtimeClassLoader; + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + String prefix = "com.google.cloud.functions."; + if (name.startsWith(prefix) && Character.isUpperCase(name.charAt(prefix.length()))) { + return runtimeClassLoader.loadClass(name); + } + return super.findClass(name); // should throw ClassNotFoundException + } + + private static ClassLoader getSystemOrBootstrapClassLoader() { + try { + // We're still building against the Java 8 API, so we have to use reflection for now. + Method getPlatformClassLoader = ClassLoader.class.getMethod("getPlatformClassLoader"); + return (ClassLoader) getPlatformClassLoader.invoke(null); + } catch (ReflectiveOperationException e) { + return null; + } + } + } } diff --git a/invoker-core/core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java b/invoker-core/core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java index aa2441eb..6c9153f7 100644 --- a/invoker-core/core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java +++ b/invoker-core/core/src/test/java/com/google/cloud/functions/invoker/IntegrationTest.java @@ -2,11 +2,13 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static java.util.stream.Collectors.toList; import com.google.auto.value.AutoValue; import com.google.cloud.functions.invoker.runner.Invoker; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.io.Files; +import com.google.common.collect.Iterables; import com.google.common.io.Resources; import com.google.gson.Gson; import com.google.gson.JsonObject; @@ -19,10 +21,15 @@ import java.net.ServerSocket; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; @@ -163,6 +170,53 @@ public void packageless() throws Exception { TestCase.builder().setExpectedResponseText("hello, world\n").build()); } + /** Any runtime class that user code shouldn't be able to see. */ + private static final Class INTERNAL_CLASS = CloudFunction.class; + + private String functionJarString() throws IOException { + Path functionJarTargetDir = Paths.get("../functionjar/target"); + Pattern functionJarPattern = Pattern.compile("java-function-invoker-core-functionjar-.*\\.jar"); + List functionJars = Files.list(functionJarTargetDir) + .map(path -> path.getFileName().toString()) + .filter(s -> functionJarPattern.matcher(s).matches()) + .map(s -> functionJarTargetDir.resolve(s)) + .collect(toList()); + assertWithMessage("Number of jars in %s matching %s", functionJarTargetDir, functionJarPattern) + .that(functionJars).hasSize(1); + return Iterables.getOnlyElement(functionJars).toString(); + } + + /** + * Tests that if we launch an HTTP function with {@code -jar}, then the function code cannot + * see the classes from the runtime. This is allows us to avoid conflicts between versions of + * libraries that we use in the runtime and different versions of the same libraries that the + * function might use. + */ + @Test + public void jarOptionHttp() throws Exception { + testHttpFunction("com.example.functionjar.Foreground", + ImmutableList.of("-jar", functionJarString()), + TestCase.builder() + .setUrl("/?class=" + INTERNAL_CLASS.getName()) + .setExpectedResponseText("OK") + .build()); + } + + /** Like {@link #jarOptionHttp} but for background functions. */ + @Test + public void jarOptionBackground() throws Exception { + Gson gson = new Gson(); + URL resourceUrl = getClass().getResource("/adder_gcf_ga_event.json"); + assertThat(resourceUrl).isNotNull(); + String originalJson = Resources.toString(resourceUrl, StandardCharsets.UTF_8); + JsonObject json = gson.fromJson(originalJson, JsonObject.class); + JsonObject jsonData = json.getAsJsonObject("data"); + jsonData.addProperty("class", INTERNAL_CLASS.getName()); + testBackgroundFunction("com.example.functionjar.Background", + ImmutableList.of("-jar", functionJarString()), + TestCase.builder().setRequestText(json.toString()).build()); + } + // In these tests, we test a number of different functions that express the same functionality // in different ways. Each function is invoked with a complete HTTP body that looks like a real // event. We start with a fixed body and insert into its JSON an extra property that tells the @@ -180,23 +234,36 @@ private void backgroundTest(String functionTarget) throws Exception { jsonData.addProperty("targetFile", snoopFile.toString()); testBackgroundFunction(functionTarget, TestCase.builder().setRequestText(json.toString()).build()); - String snooped = Files.asCharSource(snoopFile, StandardCharsets.UTF_8).read(); + String snooped = new String(Files.readAllBytes(snoopFile.toPath()), StandardCharsets.UTF_8); JsonObject snoopedJson = gson.fromJson(snooped, JsonObject.class); assertThat(snoopedJson).isEqualTo(json); } private void testHttpFunction(String target, TestCase... testCases) throws Exception { - testFunction(SignatureType.HTTP, target, testCases); + testHttpFunction(target, ImmutableList.of(), testCases); + } + + private void testHttpFunction( + String target, ImmutableList extraArgs, TestCase... testCases) throws Exception { + testFunction(SignatureType.HTTP, target, extraArgs, testCases); } private void testBackgroundFunction(String classAndMethod, TestCase... testCases) throws Exception { - testFunction(SignatureType.BACKGROUND, classAndMethod, testCases); + testBackgroundFunction(classAndMethod, ImmutableList.of(), testCases); + } + private void testBackgroundFunction( + String classAndMethod, ImmutableList extraArgs, TestCase... testCases) + throws Exception { + testFunction(SignatureType.BACKGROUND, classAndMethod, extraArgs, testCases); } private void testFunction( - SignatureType signatureType, String target, TestCase... testCases) throws Exception { - Process server = startServer(signatureType, target); + SignatureType signatureType, + String target, + ImmutableList extraArgs, + TestCase... testCases) throws Exception { + Process server = startServer(signatureType, target, extraArgs); try { HttpClient httpClient = new HttpClient(); httpClient.start(); @@ -233,7 +300,8 @@ public String toString() { } } - private Process startServer(SignatureType signatureType, String target) + private Process startServer( + SignatureType signatureType, String target, ImmutableList extraArgs) throws IOException, InterruptedException { File javaHome = new File(System.getProperty("java.home")); assertThat(javaHome.exists()).isTrue(); @@ -242,9 +310,10 @@ private Process startServer(SignatureType signatureType, String target) assertThat(javaCommand.exists()).isTrue(); String myClassPath = System.getProperty("java.class.path"); assertThat(myClassPath).isNotNull(); - String[] command = { - javaCommand.toString(), "-classpath", myClassPath, Invoker.class.getName(), - }; + ImmutableList command = ImmutableList.builder() + .add(javaCommand.toString(), "-classpath", myClassPath, Invoker.class.getName()) + .addAll(extraArgs) + .build(); ProcessBuilder processBuilder = new ProcessBuilder() .command(command) .redirectErrorStream(true); @@ -256,7 +325,8 @@ private Process startServer(SignatureType signatureType, String target) Process serverProcess = processBuilder.start(); CountDownLatch ready = new CountDownLatch(1); new Thread(() -> monitorOutput(serverProcess.getInputStream(), ready)).start(); - ready.await(5, TimeUnit.SECONDS); + boolean serverReady = ready.await(5, TimeUnit.SECONDS); + assertWithMessage("Waiting for server to be ready").that(serverReady).isTrue(); return serverProcess; } diff --git a/invoker-core/functionjar/pom.xml b/invoker-core/functionjar/pom.xml new file mode 100644 index 00000000..32ff71da --- /dev/null +++ b/invoker-core/functionjar/pom.xml @@ -0,0 +1,84 @@ + + 4.0.0 + + + com.google.cloud.functions.invoker + java-function-invoker-core-parent + 1.0.0-alpha-1 + + + com.google.cloud.functions.invoker + java-function-invoker-core-functionjar + 1.0.0-alpha-1 + Example GCF Function Jar + + An example of a GCF function packaged into a jar. We use this in tests. + + + + + com.google.cloud.functions + functions-framework-api + + + + com.google.escapevelocity + escapevelocity + 0.9.1 + + + com.google.guava + guava + 28.1-jre + + + com.google.code.gson + gson + 2.8.6 + + + + + + + maven-jar-plugin + 3.1.2 + + + + true + dependency + + + + + + + test-jar + + test-compile + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + test-compile + + copy-dependencies + + + + + + + diff --git a/invoker-core/functionjar/src/main/java/com/example/functionjar/Main.java b/invoker-core/functionjar/src/main/java/com/example/functionjar/Main.java new file mode 100644 index 00000000..e69de29b diff --git a/invoker-core/functionjar/src/test/java/com/example/functionjar/Background.java b/invoker-core/functionjar/src/test/java/com/example/functionjar/Background.java new file mode 100644 index 00000000..1e0d9154 --- /dev/null +++ b/invoker-core/functionjar/src/test/java/com/example/functionjar/Background.java @@ -0,0 +1,30 @@ +package com.example.functionjar; + +import com.google.cloud.functions.Context; +import com.google.cloud.functions.RawBackgroundFunction; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +/** + * @author emcmanus@google.com (Éamonn McManus) + */ +public class Background implements RawBackgroundFunction { + @Override + public void accept(String json, Context context) { + try { + test(json); + } catch (Throwable e) { + e.printStackTrace(); + throw e; + } + } + + private void test(String jsonString) { + Gson gson = new Gson(); + JsonObject json = gson.fromJson(jsonString, JsonObject.class); + JsonPrimitive jsonRuntimeClassName = json.getAsJsonPrimitive("class"); + String runtimeClassName = jsonRuntimeClassName.getAsString(); + new Checker().serviceOrAssert(runtimeClassName); + } +} diff --git a/invoker-core/functionjar/src/test/java/com/example/functionjar/Checker.java b/invoker-core/functionjar/src/test/java/com/example/functionjar/Checker.java new file mode 100644 index 00000000..8a0c1c3a --- /dev/null +++ b/invoker-core/functionjar/src/test/java/com/example/functionjar/Checker.java @@ -0,0 +1,20 @@ +package com.example.functionjar; + +import com.google.escapevelocity.Template; + +class Checker { + void serviceOrAssert(String runtimeClassName) { + ClassLoader myLoader = getClass().getClassLoader(); + Class