diff --git a/functions/http/http-form-data/pom.xml b/functions/http/http-form-data/pom.xml
new file mode 100644
index 00000000000..e016859058e
--- /dev/null
+++ b/functions/http/http-form-data/pom.xml
@@ -0,0 +1,178 @@
+
+
+
+
+
+ 4.0.0
+
+ com.example.cloud.functions
+ functions-http-form-data
+
+
+ com.google.cloud.samples
+ shared-configuration
+ 1.0.17
+
+
+
+ 2.0.7
+ 11
+ 11
+ UTF-8
+
+
+
+
+
+
+
+
+ skip_tests_on_gcf
+
+
+ env.NEW_BUILD
+
+
+
+ true
+
+
+
+
+
+
+ com.google.code.gson
+ gson
+ 2.8.6
+
+
+ javax.servlet
+ javax.servlet-api
+ 4.0.1
+ provided
+
+
+ com.google.cloud.functions
+ functions-framework-api
+ 1.0.1
+
+
+
+
+ com.google.truth
+ truth
+ 1.0.1
+ test
+
+
+ org.powermock
+ powermock-core
+ ${powermock.version}
+ test
+
+
+ org.powermock
+ powermock-module-junit4
+ ${powermock.version}
+ test
+
+
+ org.powermock
+ powermock-api-mockito2
+ ${powermock.version}
+ test
+
+
+ com.google.guava
+ guava-testlib
+ 29.0-jre
+ test
+
+
+
+ junit
+ junit
+ 4.13
+ test
+
+
+ com.google.guava
+ guava-testlib
+ 29.0-jre
+ compile
+
+
+
+
+
+
+
+ com.google.cloud.functions
+ function-maven-plugin
+ 0.9.2
+
+ functions.HttpFormData
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.0.0-M4
+
+ ${skipTests}
+ sponge_log
+ false
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ compile
+ compile
+
+ compile
+
+
+
+ testCompile
+ test-compile
+
+ testCompile
+
+
+
+
+
+ .google/
+
+
+
+
+
+
diff --git a/functions/http/http-form-data/src/main/java/functions/HttpFormData.java b/functions/http/http-form-data/src/main/java/functions/HttpFormData.java
new file mode 100644
index 00000000000..cb965d0a7c2
--- /dev/null
+++ b/functions/http/http-form-data/src/main/java/functions/HttpFormData.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2020 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 functions;
+
+// [START functions_http_form_data]
+
+import com.google.cloud.functions.HttpFunction;
+import com.google.cloud.functions.HttpRequest;
+import com.google.cloud.functions.HttpResponse;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.logging.Logger;
+import javax.servlet.annotation.MultipartConfig;
+
+@MultipartConfig
+public class HttpFormData implements HttpFunction {
+ private static final Logger LOGGER = Logger.getLogger(HttpFormData.class.getName());
+
+ @Override
+ public void service(HttpRequest request, HttpResponse response)
+ throws IOException {
+
+ if (!"POST".equals(request.getMethod())) {
+ response.setStatusCode(HttpURLConnection.HTTP_BAD_METHOD);
+ return;
+ }
+
+ // This code will process each file uploaded.
+ String tempDirectory = System.getProperty("java.io.tmpdir");
+ for (HttpRequest.HttpPart httpPart : request.getParts().values()) {
+ String filename = httpPart.getFileName().orElse(null);
+ if (filename == null) {
+ continue;
+ }
+
+ LOGGER.info("Processed file: " + filename);
+
+ // Note: GCF's temp directory is an in-memory file system
+ // Thus, any files in it must fit in the instance's memory.
+ Path filePath = Paths.get(tempDirectory, filename).toAbsolutePath();
+
+ // Note: files saved to a GCF instance itself may not persist across executions.
+ // Persistent files should be stored elsewhere, e.g. a Cloud Storage bucket.
+ Files.copy(httpPart.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
+
+ // TODO(developer): process saved files here
+ Files.delete(filePath);
+ }
+
+ // This code will process other form fields.
+ for (String fieldName : request.getQueryParameters().keySet()) {
+ String firstFieldValue = request.getFirstQueryParameter(fieldName).get();
+
+ // TODO(developer): process field values here
+ LOGGER.info(String.format(
+ "Processed field: %s (value: %s)", fieldName, firstFieldValue));
+ }
+ }
+}
+// [END functions_http_form_data]
\ No newline at end of file
diff --git a/functions/http/http-form-data/src/test/java/functions/HttpFormDataTest.java b/functions/http/http-form-data/src/test/java/functions/HttpFormDataTest.java
new file mode 100644
index 00000000000..b08cf6f11e8
--- /dev/null
+++ b/functions/http/http-form-data/src/test/java/functions/HttpFormDataTest.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2020 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 functions;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.powermock.api.mockito.PowerMockito.mock;
+import static org.powermock.api.mockito.PowerMockito.when;
+
+import com.google.cloud.functions.HttpRequest;
+import com.google.cloud.functions.HttpResponse;
+import com.google.common.testing.TestLogHandler;
+import java.io.BufferedWriter;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringWriter;
+import java.net.HttpURLConnection;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.logging.Logger;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.powermock.api.mockito.PowerMockito;
+
+@RunWith(JUnit4.class)
+public class HttpFormDataTest {
+ @Mock private HttpRequest request;
+ @Mock private HttpResponse response;
+
+ private BufferedWriter writerOut;
+ private StringWriter responseOut;
+
+ private static final Logger LOGGER = Logger.getLogger(HttpFormData.class.getName());
+ private static final TestLogHandler logHandler = new TestLogHandler();
+
+ @BeforeClass
+ public static void setUp() {
+ LOGGER.addHandler(logHandler);
+ }
+
+ @Before
+ public void beforeTest() throws IOException {
+ Mockito.mockitoSession().initMocks(this);
+
+ request = mock(HttpRequest.class);
+ response = mock(HttpResponse.class);
+
+ responseOut = new StringWriter();
+ writerOut = new BufferedWriter(responseOut);
+ PowerMockito.when(response.getWriter()).thenReturn(writerOut);
+
+ logHandler.clear();
+ }
+
+ @Test
+ public void functionsHttpMethod_shouldErrorOnGet() throws IOException {
+ when(request.getMethod()).thenReturn("GET");
+
+ new HttpFormData().service(request, response);
+
+ writerOut.flush();
+ verify(response, times(1)).setStatusCode(HttpURLConnection.HTTP_BAD_METHOD);
+ }
+
+ @Test
+ public void functionsHttpFormData_shouldSaveFiles() throws IOException {
+ when(request.getMethod()).thenReturn("POST");
+
+ ArrayList partsList = new ArrayList<>();
+
+ InputStream stream = new ByteArrayInputStream("foo text%n".getBytes(StandardCharsets.UTF_8));
+
+ MockHttpPart mockHttpPart = new MockHttpPart();
+ mockHttpPart.setFileName("foo.txt");
+ mockHttpPart.setInputStream(stream);
+ partsList.add(mockHttpPart);
+
+ HashMap httpParts = new HashMap<>();
+ httpParts.put("mock", mockHttpPart);
+ when(request.getParts()).thenReturn(httpParts);
+
+ new HttpFormData().service(request, response);
+
+ assertThat(logHandler.getStoredLogRecords().get(0).getMessage()).isEqualTo(
+ "Processed file: foo.txt");
+ }
+
+ @Test
+ public void functionsHttpFormData_shouldProcessFields() throws IOException {
+ when(request.getMethod()).thenReturn("POST");
+ when(request.getParts()).thenReturn(new HashMap<>());
+
+ HashMap> queryParams = new HashMap<>();
+ queryParams.put("foo", Arrays.asList(new String[]{"bar"}));
+
+ when(request.getQueryParameters()).thenReturn(queryParams);
+ when(request.getFirstQueryParameter("foo")).thenReturn(Optional.of("bar"));
+
+ new HttpFormData().service(request, response);
+
+ assertThat(logHandler.getStoredLogRecords().get(0).getMessage()).isEqualTo(
+ "Processed field: foo (value: bar)");
+ }
+}
\ No newline at end of file
diff --git a/functions/http/http-form-data/src/test/java/functions/MockHttpPart.java b/functions/http/http-form-data/src/test/java/functions/MockHttpPart.java
new file mode 100644
index 00000000000..c3aa5fa36f9
--- /dev/null
+++ b/functions/http/http-form-data/src/test/java/functions/MockHttpPart.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2020 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 functions;
+
+import com.google.cloud.functions.HttpRequest;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+public class MockHttpPart implements HttpRequest.HttpPart {
+ private Optional fileName;
+ private InputStream inputStream = InputStream.nullInputStream();
+
+ public void setFileName(String name) {
+ fileName = Optional.of(name);
+ }
+
+ public void setInputStream(InputStream stream) {
+ inputStream = stream;
+ }
+
+ @Override
+ public Optional getFileName() {
+ return fileName;
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return inputStream;
+ }
+
+ // Auto-stubbed methods below
+ @Override
+ public Optional getContentType() {
+ return Optional.empty();
+ }
+
+ @Override
+ public long getContentLength() {
+ return 0;
+ }
+
+ @Override
+ public Optional getCharacterEncoding() {
+ return Optional.empty();
+ }
+
+ @Override
+ public BufferedReader getReader() throws IOException {
+ return null;
+ }
+
+ @Override
+ public Map> getHeaders() {
+ return null;
+ }
+}
\ No newline at end of file