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