Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: Define strongly typed function interface #186

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ jobs:
# queries: security-extended,security-and-quality


# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
- name: Autobuild
uses: github/codeql-action/autobuild@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3
with:
working-directory: ${{ matrix.working-directory }}


- name: Build
run: |
(cd functions-framework-api/ && mvn install)
(cd invoker/ && mvn clean install)
(cd function-maven-plugin && mvn install)

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@29b1f65c5e92e24fe6b6647da1eaabe529cec70f # v2.3.3
Expand Down
2 changes: 1 addition & 1 deletion function-maven-plugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
<dependency>
<groupId>com.google.cloud.functions.invoker</groupId>
<artifactId>java-function-invoker</artifactId>
<version>1.2.1</version>
<version>1.2.3-SNAPSHOT</version>
</dependency>

<dependency>
Expand Down
2 changes: 1 addition & 1 deletion functions-framework-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

<groupId>com.google.cloud.functions</groupId>
<artifactId>functions-framework-api</artifactId>
<version>1.0.5-SNAPSHOT</version>
<version>1.0.6-SNAPSHOT</version>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// 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.lang.reflect.Type;

/**
* Represents a Cloud Function with a strongly typed interface that is activated by an HTTP request.
*/
@FunctionalInterface
public interface TypedFunction<RequestT, ResponseT> {
/**
* Called to service an incoming HTTP request. This interface is implemented by user code to
* provide the action for a given HTTP function. If this method throws any exception (including
* any {@link Error}) then the HTTP response will have a 500 status code.
*
* @param arg the payload of the event, deserialized from the original JSON string.
* @return invocation result or null to indicate the body of the response should be empty.
* @throws Exception to produce a 500 status code in the HTTP response.
*/
public ResponseT apply(RequestT arg) throws Exception;

/**
* Called to get the the format object that handles request decoding and response encoding. If
* null is returned a default JSON format is used.
*
* @return the {@link WireFormat} to use for serialization
*/
public default WireFormat getWireFormat() {
return null;
}

/**
* Describes how to deserialize request object and serialize response objects for an HTTP
* invocation.
*/
public interface WireFormat {
/** Serialize is expected to encode the object to the provided HttpResponse. */
void serialize(Object object, HttpResponse response) throws Exception;

/**
* Deserialize is expected to read an object of {@code Type} from the HttpRequest. The Type is
* determined through reflection on the user's function.
*/
Object deserialize(HttpRequest request, Type type) throws Exception;
}
}
4 changes: 2 additions & 2 deletions invoker/conformance/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
<parent>
<artifactId>java-function-invoker-parent</artifactId>
<groupId>com.google.cloud.functions.invoker</groupId>
<version>1.2.2-SNAPSHOT</version>
<version>1.2.3-SNAPSHOT</version>
</parent>

<groupId>com.google.cloud.functions.invoker</groupId>
<artifactId>conformance</artifactId>
<version>1.2.2-SNAPSHOT</version>
<version>1.2.3-SNAPSHOT</version>

<name>GCF Confromance Tests</name>
<description>
Expand Down
7 changes: 4 additions & 3 deletions invoker/core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
<parent>
<groupId>com.google.cloud.functions.invoker</groupId>
<artifactId>java-function-invoker-parent</artifactId>
<version>1.2.2-SNAPSHOT</version>
<version>1.2.3-SNAPSHOT</version>
</parent>

<groupId>com.google.cloud.functions.invoker</groupId>
<artifactId>java-function-invoker</artifactId>
<version>1.2.2-SNAPSHOT</version>
<version>1.2.3-SNAPSHOT</version>
<name>GCF Java Invoker</name>
<description>
Application that invokes a GCF Java function. This application is a
Expand Down Expand Up @@ -44,6 +44,7 @@
<dependency>
<groupId>com.google.cloud.functions</groupId>
<artifactId>functions-framework-api</artifactId>
<version>1.0.6-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
Expand Down Expand Up @@ -114,7 +115,7 @@
<dependency>
<groupId>com.google.cloud.functions.invoker</groupId>
<artifactId>java-function-invoker-testfunction</artifactId>
<version>1.2.2-SNAPSHOT</version>
<version>1.2.3-SNAPSHOT</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
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.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServlet;
Expand Down Expand Up @@ -72,18 +71,7 @@ public void service(HttpServletRequest req, HttpServletResponse res) {
res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
} finally {
Thread.currentThread().setContextClassLoader(oldContextLoader);
try {
// We can't use HttpServletResponse.flushBuffer() because we wrap the PrintWriter
// returned by HttpServletResponse in our own BufferedWriter to match our API.
// So we have to flush whichever of getWriter() or getOutputStream() works.
try {
respImpl.getOutputStream().flush();
} catch (IllegalStateException e) {
respImpl.getWriter().flush();
}
} catch (IOException e) {
// Too bad, can't flush.
}
respImpl.flush();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package com.google.cloud.functions.invoker;

import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import com.google.cloud.functions.TypedFunction;
import com.google.cloud.functions.TypedFunction.WireFormat;
import com.google.cloud.functions.invoker.http.HttpRequestImpl;
import com.google.cloud.functions.invoker.http.HttpResponseImpl;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.lang.reflect.Type;
import java.util.Arrays;
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;

public class TypedFunctionExecutor extends HttpServlet {
private static final String APPLY_METHOD = "apply";
private static final Logger logger = Logger.getLogger("com.google.cloud.functions.invoker");

private final Type argType;
private final TypedFunction<Object, Object> function;
private final WireFormat format;

private TypedFunctionExecutor(
Type argType, TypedFunction<Object, Object> func, WireFormat format) {
this.argType = argType;
this.function = func;
this.format = format;
}

public static TypedFunctionExecutor forClass(Class<?> functionClass) {
if (!TypedFunction.class.isAssignableFrom(functionClass)) {
throw new RuntimeException(
"Class "
+ functionClass.getName()
+ " does not implement "
+ TypedFunction.class.getName());
}
@SuppressWarnings("unchecked")
Class<? extends TypedFunction<?, ?>> typedFunctionClass =
(Class<? extends TypedFunction<?, ?>>) functionClass.asSubclass(TypedFunction.class);

Optional<Type> argType = handlerTypeArgument(typedFunctionClass);
if (argType.isEmpty()) {
throw new RuntimeException(
"Class "
+ typedFunctionClass.getName()
+ " does not implement "
+ TypedFunction.class.getName());
}

TypedFunction<?, ?> typedFunction;
try {
typedFunction = typedFunctionClass.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(
"Class "
+ typedFunctionClass.getName()
+ " must declare a valid default constructor to be usable as a strongly typed"
+ " function. Could not use constructor: "
+ e.toString());
}

WireFormat format = typedFunction.getWireFormat();
if (format == null) {
format = LazyDefaultFormatHolder.defaultFormat;
}

@SuppressWarnings("unchecked")
TypedFunctionExecutor executor =
new TypedFunctionExecutor(
argType.orElseThrow(), (TypedFunction<Object, Object>) typedFunction, format);
return executor;
}

/**
* Returns the {@code ReqT} of a concrete class that implements {@link TypedFunction
* TypedFunction<ReqT, RespT>}. Returns an empty {@link Optional} if {@code ReqT} can't be
* determined.
*/
static Optional<Type> handlerTypeArgument(Class<? extends TypedFunction<?, ?>> functionClass) {
return Arrays.stream(functionClass.getMethods())
.filter(method -> method.getName().equals(APPLY_METHOD) && method.getParameterCount() == 1)
.map(method -> method.getGenericParameterTypes()[0])
.filter(type -> type != Object.class)
.findFirst();
}

/** Executes the user's method, can handle all HTTP type methods. */
@Override
public void service(HttpServletRequest req, HttpServletResponse res) {
HttpRequestImpl reqImpl = new HttpRequestImpl(req);
HttpResponseImpl resImpl = new HttpResponseImpl(res);
ClassLoader oldContextClassLoader = Thread.currentThread().getContextClassLoader();

try {
Thread.currentThread().setContextClassLoader(function.getClass().getClassLoader());
handleRequest(reqImpl, resImpl);
} finally {
Thread.currentThread().setContextClassLoader(oldContextClassLoader);
resImpl.flush();
}
}

private void handleRequest(HttpRequest req, HttpResponse res) {
Object reqObj;
try {
reqObj = format.deserialize(req, argType);
} catch (Throwable t) {
logger.log(Level.SEVERE, "Failed to parse request for " + function.getClass().getName(), t);
res.setStatusCode(HttpServletResponse.SC_BAD_REQUEST);
return;
}

Object resObj;
try {
resObj = function.apply(reqObj);
} catch (Throwable t) {
logger.log(Level.SEVERE, "Failed to execute " + function.getClass().getName(), t);
res.setStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return;
}

try {
format.serialize(resObj, res);
} catch (Throwable t) {
logger.log(
Level.SEVERE, "Failed to serialize response for " + function.getClass().getName(), t);
res.setStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
return;
}
}

private static class LazyDefaultFormatHolder {
static final WireFormat defaultFormat = new GsonWireFormat();
}

private static class GsonWireFormat implements TypedFunction.WireFormat {
private final Gson gson = new GsonBuilder().create();

@Override
public void serialize(Object object, HttpResponse response) throws Exception {
if (object == null) {
response.setStatusCode(HttpServletResponse.SC_NO_CONTENT);
return;
}
try (BufferedWriter bodyWriter = response.getWriter()) {
gson.toJson(object, bodyWriter);
}
}

@Override
public Object deserialize(HttpRequest request, Type type) throws Exception {
try (BufferedReader bodyReader = request.getReader()) {
return gson.fromJson(bodyReader, type);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,22 +86,32 @@ public OutputStream getOutputStream() throws IOException {
@Override
public synchronized BufferedWriter getWriter() throws IOException {
if (writer == null) {
// Unfortunately this means that we get two intermediate objects between the
// object we return
// and the underlying Writer that response.getWriter() wraps. We could try
// accessing the
// PrintWriter.out field via reflection, but that sort of access to non-public
// fields of
// platform classes is now frowned on and may draw warnings or even fail in
// subsequent
// versions.
// We could instead wrap the OutputStream, but that would require us to deduce
// the appropriate
// Charset, using logic like this:
// Unfortunately this means that we get two intermediate objects between the object we return
// and the underlying Writer that response.getWriter() wraps. We could try accessing the
// PrintWriter.out field via reflection, but that sort of access to non-public fields of
// platform classes is now frowned on and may draw warnings or even fail in subsequent
// versions. We could instead wrap the OutputStream, but that would require us to deduce the
// appropriate Charset, using logic like this:
// https://github.com/eclipse/jetty.project/blob/923ec38adf/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java#L731
// We may end up doing that if performance is an issue.
writer = new BufferedWriter(response.getWriter());
}
return writer;
}

public void flush() {
try {
// We can't use HttpServletResponse.flushBuffer() because we wrap the
// PrintWriter returned by HttpServletResponse in our own BufferedWriter
// to match our API. So we have to flush whichever of getWriter() or
// getOutputStream() works.
try {
getOutputStream().flush();
} catch (IllegalStateException e) {
getWriter().flush();
}
} catch (IOException e) {
// Too bad, can't flush.
}
}
}
Loading