diff --git a/functions/v2/ocr/ocr-process-image/pom.xml b/functions/v2/ocr/ocr-process-image/pom.xml new file mode 100644 index 00000000000..3a3254751ed --- /dev/null +++ b/functions/v2/ocr/ocr-process-image/pom.xml @@ -0,0 +1,141 @@ + + + + + + 4.0.0 + + com.example.cloud.functions + functions-ocr-process-image + + + com.google.cloud.samples + shared-configuration + 1.2.0 + + + + 11 + 11 + UTF-8 + + + + + + com.google.cloud + libraries-bom + 25.4.0 + pom + import + + + + + + + com.google.cloud.functions + functions-framework-api + 1.0.4 + provided + + + io.cloudevents + cloudevents-core + 2.3.0 + + + org.projectlombok + lombok + 1.18.24 + + + com.google.cloud + google-cloud-vision + + + com.google.cloud + google-cloud-translate + + + com.google.cloud + google-cloud-pubsub + + + com.google.code.gson + gson + 2.9.1 + + + + + + junit + junit + 4.13.2 + test + + + com.google.truth + truth + 1.1.3 + test + + + com.google.guava + guava-testlib + 31.1-jre + test + + + + + + + + com.google.cloud.functions + function-maven-plugin + 0.10.0 + + + functions.OcrProcessImage + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M7 + + ${skipTests} + sponge_log + false + + + + + diff --git a/functions/v2/ocr/ocr-process-image/src/main/java/functions/OcrProcessImage.java b/functions/v2/ocr/ocr-process-image/src/main/java/functions/OcrProcessImage.java new file mode 100644 index 00000000000..e0fd90e7c99 --- /dev/null +++ b/functions/v2/ocr/ocr-process-image/src/main/java/functions/OcrProcessImage.java @@ -0,0 +1,189 @@ +/* + * Copyright 2022 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_ocr_process] + +import com.google.cloud.functions.CloudEventsFunction; +import com.google.cloud.pubsub.v1.Publisher; +import com.google.cloud.translate.v3.DetectLanguageRequest; +import com.google.cloud.translate.v3.DetectLanguageResponse; +import com.google.cloud.translate.v3.LocationName; +import com.google.cloud.translate.v3.TranslationServiceClient; +import com.google.cloud.vision.v1.AnnotateImageRequest; +import com.google.cloud.vision.v1.AnnotateImageResponse; +import com.google.cloud.vision.v1.Feature; +import com.google.cloud.vision.v1.Image; +import com.google.cloud.vision.v1.ImageAnnotatorClient; +import com.google.cloud.vision.v1.ImageSource; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.protobuf.ByteString; +import com.google.pubsub.v1.ProjectTopicName; +import com.google.pubsub.v1.PubsubMessage; +import functions.eventpojos.StorageObjectData; +import io.cloudevents.CloudEvent; +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.logging.Logger; + +// [END functions_ocr_process] + +// [START functions_ocr_setup] +public class OcrProcessImage implements CloudEventsFunction { + // TODO set these environment variables + private static final String PROJECT_ID = System.getenv("GCP_PROJECT"); + private static final String TRANSLATE_TOPIC_NAME = System.getenv("TRANSLATE_TOPIC"); + private static final String[] TO_LANGS = System.getenv("TO_LANG").split(","); + + private static final Logger logger = Logger.getLogger(OcrProcessImage.class.getName()); + private static final String LOCATION_NAME = LocationName.of(PROJECT_ID, "global").toString(); + private Publisher publisher; + + public OcrProcessImage() throws IOException { + publisher = Publisher.newBuilder(ProjectTopicName.of(PROJECT_ID, TRANSLATE_TOPIC_NAME)).build(); + } + + // Create custom deserializer to handle timestamps in event data + class DateDeserializer implements JsonDeserializer { + @Override + public OffsetDateTime deserialize( + JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + return OffsetDateTime.parse(json.getAsString()); + } + } + + Gson gson = + new GsonBuilder().registerTypeAdapter(OffsetDateTime.class, new DateDeserializer()).create(); + // [END functions_ocr_setup] + + // [START functions_ocr_process] + @Override + public void accept(CloudEvent event) { + // Unmarshal data from CloudEvent + StorageObjectData gcsEvent = + gson.fromJson( + new String(event.getData().toBytes(), StandardCharsets.UTF_8), StorageObjectData.class); + String bucket = gcsEvent.getBucket(); + if (bucket == null) { + throw new IllegalArgumentException("Missing bucket parameter"); + } + String filename = gcsEvent.getName(); + if (filename == null) { + throw new IllegalArgumentException("Missing name parameter"); + } + + detectText(bucket, filename); + } + // [END functions_ocr_process] + + // [START functions_ocr_detect] + private void detectText(String bucket, String filename) { + logger.info("Looking for text in image " + filename); + + List visionRequests = new ArrayList<>(); + String gcsPath = String.format("gs://%s/%s", bucket, filename); + + ImageSource imgSource = ImageSource.newBuilder().setGcsImageUri(gcsPath).build(); + Image img = Image.newBuilder().setSource(imgSource).build(); + + Feature textFeature = Feature.newBuilder().setType(Feature.Type.TEXT_DETECTION).build(); + AnnotateImageRequest visionRequest = + AnnotateImageRequest.newBuilder().addFeatures(textFeature).setImage(img).build(); + visionRequests.add(visionRequest); + + // Detect text in an image using the Cloud Vision API + AnnotateImageResponse visionResponse; + try (ImageAnnotatorClient client = ImageAnnotatorClient.create()) { + visionResponse = client.batchAnnotateImages(visionRequests).getResponses(0); + if (visionResponse == null || !visionResponse.hasFullTextAnnotation()) { + logger.info(String.format("Image %s contains no text", filename)); + return; + } + + if (visionResponse.hasError()) { + // Log error + logger.log( + Level.SEVERE, "Error in vision API call: " + visionResponse.getError().getMessage()); + return; + } + } catch (IOException e) { + // Log error (since IOException cannot be thrown by a Cloud Function) + logger.log(Level.SEVERE, "Error detecting text: " + e.getMessage(), e); + return; + } + + String text = visionResponse.getFullTextAnnotation().getText(); + logger.info("Extracted text from image: " + text); + + // Detect language using the Cloud Translation API + DetectLanguageRequest languageRequest = + DetectLanguageRequest.newBuilder() + .setParent(LOCATION_NAME) + .setMimeType("text/plain") + .setContent(text) + .build(); + DetectLanguageResponse languageResponse; + try (TranslationServiceClient client = TranslationServiceClient.create()) { + languageResponse = client.detectLanguage(languageRequest); + } catch (IOException e) { + // Log error (since IOException cannot be thrown by a function) + logger.log(Level.SEVERE, "Error detecting language: " + e.getMessage(), e); + return; + } + + if (languageResponse.getLanguagesCount() == 0) { + logger.info("No languages were detected for text: " + text); + return; + } + + String languageCode = languageResponse.getLanguages(0).getLanguageCode(); + logger.info(String.format("Detected language %s for file %s", languageCode, filename)); + + // Send a Pub/Sub translation request for every language we're going to translate to + for (String targetLanguage : TO_LANGS) { + logger.info("Sending translation request for language " + targetLanguage); + OcrTranslateApiMessage message = new OcrTranslateApiMessage(text, filename, targetLanguage); + ByteString byteStr = ByteString.copyFrom(message.toPubsubData()); + PubsubMessage pubsubApiMessage = PubsubMessage.newBuilder().setData(byteStr).build(); + try { + publisher.publish(pubsubApiMessage).get(); + } catch (InterruptedException | ExecutionException e) { + // Log error + logger.log(Level.SEVERE, "Error publishing translation request: " + e.getMessage(), e); + return; + } + } + } + // [END functions_ocr_detect] + + // [START functions_ocr_process] + // [START functions_ocr_setup] +} +// [END functions_ocr_setup] +// [END functions_ocr_process] diff --git a/functions/v2/ocr/ocr-process-image/src/main/java/functions/OcrTranslateApiMessage.java b/functions/v2/ocr/ocr-process-image/src/main/java/functions/OcrTranslateApiMessage.java new file mode 100644 index 00000000000..cd880b9a3c7 --- /dev/null +++ b/functions/v2/ocr/ocr-process-image/src/main/java/functions/OcrTranslateApiMessage.java @@ -0,0 +1,74 @@ +/* + * Copyright 2022 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_ocr_translate_pojo] + +import com.google.gson.Gson; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; + +// Object for storing OCR translation requests +public class OcrTranslateApiMessage { + private static final Gson gson = new Gson(); + + private String text; + private String filename; + private String lang; + + public OcrTranslateApiMessage(String text, String filename, String lang) { + if (text == null) { + throw new IllegalArgumentException("Missing text parameter"); + } + if (filename == null) { + throw new IllegalArgumentException("Missing filename parameter"); + } + if (lang == null) { + throw new IllegalArgumentException("Missing lang parameter"); + } + + this.text = text; + this.filename = filename; + this.lang = lang; + } + + public String getText() { + return text; + } + + public String getFilename() { + return filename; + } + + public String getLang() { + return lang; + } + + public static OcrTranslateApiMessage fromPubsubData(byte[] data) { + String jsonStr = new String(Base64.getDecoder().decode(data), StandardCharsets.UTF_8); + Map jsonMap = gson.fromJson(jsonStr, Map.class); + + return new OcrTranslateApiMessage( + jsonMap.get("text"), jsonMap.get("filename"), jsonMap.get("lang")); + } + + public byte[] toPubsubData() { + return gson.toJson(this).getBytes(StandardCharsets.UTF_8); + } +} +// [END functions_ocr_translate_pojo] diff --git a/functions/v2/ocr/ocr-process-image/src/main/java/functions/eventpojos/CustomerEncryption.java b/functions/v2/ocr/ocr-process-image/src/main/java/functions/eventpojos/CustomerEncryption.java new file mode 100644 index 00000000000..3d8bdf8ba23 --- /dev/null +++ b/functions/v2/ocr/ocr-process-image/src/main/java/functions/eventpojos/CustomerEncryption.java @@ -0,0 +1,25 @@ +/* + * Copyright 2022 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.eventpojos; + +// Metadata of customer-supplied encryption key for a Cloud Storage object +// https://cloud.google.com/storage/docs/json_api/v1/objects +@lombok.Data +public class CustomerEncryption { + private String encryptionAlgorithm; + private String keySha256; +} diff --git a/functions/v2/ocr/ocr-process-image/src/main/java/functions/eventpojos/StorageObjectData.java b/functions/v2/ocr/ocr-process-image/src/main/java/functions/eventpojos/StorageObjectData.java new file mode 100644 index 00000000000..24c3971a3a6 --- /dev/null +++ b/functions/v2/ocr/ocr-process-image/src/main/java/functions/eventpojos/StorageObjectData.java @@ -0,0 +1,55 @@ +/* + * Copyright 2022 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.eventpojos; + +import java.time.OffsetDateTime; +import java.util.Map; + +// Represents an object within Cloud Storage +// https://cloud.google.com/storage/docs/json_api/v1/objects +@lombok.Data +public class StorageObjectData { + private String bucket; + private String cacheControl; + private Long componentCount; + private String contentDisposition; + private String contentEncoding; + private String contentLanguage; + private String contentType; + private String crc32C; + private CustomerEncryption customerEncryption; + private String etag; + private Boolean eventBasedHold; + private Long generation; + private String id; + private String kind; + private String kmsKeyName; + private String md5Hash; + private String mediaLink; + private Map metadata; + private Long metageneration; + private String name; + private OffsetDateTime retentionExpirationTime; + private String selfLink; + private Long size; + private String storageClass; + private Boolean temporaryHold; + private OffsetDateTime timeCreated; + private OffsetDateTime timeDeleted; + private OffsetDateTime timeStorageClassUpdated; + private OffsetDateTime updated; +} diff --git a/functions/v2/ocr/ocr-process-image/src/test/java/functions/OcrProcessImageTest.java b/functions/v2/ocr/ocr-process-image/src/test/java/functions/OcrProcessImageTest.java new file mode 100644 index 00000000000..1d88b96987b --- /dev/null +++ b/functions/v2/ocr/ocr-process-image/src/test/java/functions/OcrProcessImageTest.java @@ -0,0 +1,114 @@ +/* + * Copyright 2022 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.common.testing.TestLogHandler; +import com.google.common.truth.Truth; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import functions.eventpojos.StorageObjectData; +import io.cloudevents.CloudEvent; +import io.cloudevents.core.builder.CloudEventBuilder; +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.logging.LogRecord; +import java.util.logging.Logger; +import org.junit.After; +import org.junit.BeforeClass; +import org.junit.Test; + +public class OcrProcessImageTest { + private static String FUNCTIONS_BUCKET = "nodejs-docs-samples-tests"; + + private static final Logger logger = Logger.getLogger(OcrProcessImage.class.getName()); + + private static final TestLogHandler LOG_HANDLER = new TestLogHandler(); + + // Create custom serializer to handle timestamps in event data + class DateSerializer implements JsonSerializer { + @Override + public JsonElement serialize( + OffsetDateTime time, Type typeOfSrc, JsonSerializationContext context) + throws JsonParseException { + return new JsonPrimitive(time.toString()); + } + } + + private final Gson gson = + new GsonBuilder().registerTypeAdapter(OffsetDateTime.class, new DateSerializer()).create(); + + private static OcrProcessImage sampleUnderTest; + + @BeforeClass + public static void setUpClass() throws IOException { + Truth.assertThat(System.getenv("GCP_PROJECT")); + Truth.assertThat(System.getenv("TO_LANG")); + Truth.assertThat(System.getenv("TRANSLATE_TOPIC")); + sampleUnderTest = new OcrProcessImage(); + logger.addHandler(LOG_HANDLER); + } + + @After + public void afterTest() { + LOG_HANDLER.clear(); + } + + @Test(expected = IllegalArgumentException.class) + public void functionsOcrProcess_shouldValidateParams() throws IOException, URISyntaxException { + StorageObjectData data = new StorageObjectData(); + CloudEvent event = + CloudEventBuilder.v1() + .withId("000") + .withType("google.cloud.storage.object.v1.finalized") + .withSource(new URI("curl-command")) + .withData("application/json", gson.toJson(data).getBytes()) + .build(); + + sampleUnderTest.accept(event); + } + + @Test + public void functionsOcrProcess_shouldDetectText() throws IOException, URISyntaxException { + StorageObjectData data = new StorageObjectData(); + data.setBucket(FUNCTIONS_BUCKET); + data.setName("wakeupcat.jpg"); + CloudEvent event = + CloudEventBuilder.v1() + .withId("000") + .withType("google.cloud.storage.object.v1.finalized") + .withSource(new URI("curl-command")) + .withData("application/json", gson.toJson(data).getBytes()) + .build(); + + sampleUnderTest.accept(event); + + List logs = LOG_HANDLER.getStoredLogRecords(); + Truth.assertThat(logs.get(1).getMessage()) + .contains("Extracted text from image: Wake up human!"); + Truth.assertThat(logs.get(2).getMessage()) + .contains("Detected language en for file wakeupcat.jpg"); + } +} diff --git a/functions/v2/ocr/ocr-save-result/pom.xml b/functions/v2/ocr/ocr-save-result/pom.xml new file mode 100644 index 00000000000..7c0b311bc86 --- /dev/null +++ b/functions/v2/ocr/ocr-save-result/pom.xml @@ -0,0 +1,131 @@ + + + + + + 4.0.0 + + com.example.cloud.functions + functions-ocr-save-result + + + com.google.cloud.samples + shared-configuration + 1.2.0 + + + + 11 + 11 + UTF-8 + + + + + + com.google.cloud + libraries-bom + 26.1.3 + pom + import + + + + + + + com.google.cloud.functions + functions-framework-api + 1.0.4 + provided + + + com.google.cloud + google-cloud-storage + + + io.cloudevents + cloudevents-core + 2.4.0 + + + org.projectlombok + lombok + 1.18.24 + + + com.google.code.gson + gson + 2.9.1 + + + + junit + junit + 4.13.2 + test + + + com.google.truth + truth + 1.1.3 + test + + + com.google.guava + guava-testlib + 31.1-jre + test + + + + + + + + com.google.cloud.functions + function-maven-plugin + 0.10.0 + + + functions.OcrSaveResult + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M7 + + ${skipTests} + sponge_log + false + + + + + diff --git a/functions/v2/ocr/ocr-save-result/src/main/java/functions/OcrSaveResult.java b/functions/v2/ocr/ocr-save-result/src/main/java/functions/OcrSaveResult.java new file mode 100644 index 00000000000..4f2fae681a6 --- /dev/null +++ b/functions/v2/ocr/ocr-save-result/src/main/java/functions/OcrSaveResult.java @@ -0,0 +1,82 @@ +/* + * Copyright 2022 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_ocr_save] + +import com.google.cloud.functions.CloudEventsFunction; +import com.google.cloud.storage.BlobId; +import com.google.cloud.storage.BlobInfo; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageOptions; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import functions.eventpojos.MessagePublishedData; +import io.cloudevents.CloudEvent; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.util.logging.Logger; + +public class OcrSaveResult implements CloudEventsFunction { + // TODO set this environment variable + private static final String RESULT_BUCKET = System.getenv("RESULT_BUCKET"); + + private static final Storage STORAGE = StorageOptions.getDefaultInstance().getService(); + private static final Logger logger = Logger.getLogger(OcrSaveResult.class.getName()); + + // Configure Gson with custom deserializer to handle timestamps in event data + class DateDeserializer implements JsonDeserializer { + @Override + public OffsetDateTime deserialize( + JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + return OffsetDateTime.parse(json.getAsString()); + } + } + + Gson gson = + new GsonBuilder().registerTypeAdapter(OffsetDateTime.class, new DateDeserializer()).create(); + + @Override + public void accept(CloudEvent event) { + // Unmarshal data from CloudEvent + MessagePublishedData data = + gson.fromJson( + new String(event.getData().toBytes(), StandardCharsets.UTF_8), + MessagePublishedData.class); + OcrTranslateApiMessage ocrMessage = + OcrTranslateApiMessage.fromPubsubData( + data.getMessage().getData().getBytes(StandardCharsets.UTF_8)); + + logger.info("Received request to save file " + ocrMessage.getFilename()); + + String newFileName = + String.format("%s_to_%s.txt", ocrMessage.getFilename(), ocrMessage.getLang()); + + // Save file to RESULT_BUCKET with name newFileName + logger.info(String.format("Saving result to %s in bucket %s", newFileName, RESULT_BUCKET)); + BlobInfo blobInfo = BlobInfo.newBuilder(BlobId.of(RESULT_BUCKET, newFileName)).build(); + STORAGE.create(blobInfo, ocrMessage.getText().getBytes(StandardCharsets.UTF_8)); + logger.info("File saved"); + } +} +// [END functions_ocr_save] diff --git a/functions/v2/ocr/ocr-save-result/src/main/java/functions/OcrTranslateApiMessage.java b/functions/v2/ocr/ocr-save-result/src/main/java/functions/OcrTranslateApiMessage.java new file mode 100644 index 00000000000..cd880b9a3c7 --- /dev/null +++ b/functions/v2/ocr/ocr-save-result/src/main/java/functions/OcrTranslateApiMessage.java @@ -0,0 +1,74 @@ +/* + * Copyright 2022 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_ocr_translate_pojo] + +import com.google.gson.Gson; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; + +// Object for storing OCR translation requests +public class OcrTranslateApiMessage { + private static final Gson gson = new Gson(); + + private String text; + private String filename; + private String lang; + + public OcrTranslateApiMessage(String text, String filename, String lang) { + if (text == null) { + throw new IllegalArgumentException("Missing text parameter"); + } + if (filename == null) { + throw new IllegalArgumentException("Missing filename parameter"); + } + if (lang == null) { + throw new IllegalArgumentException("Missing lang parameter"); + } + + this.text = text; + this.filename = filename; + this.lang = lang; + } + + public String getText() { + return text; + } + + public String getFilename() { + return filename; + } + + public String getLang() { + return lang; + } + + public static OcrTranslateApiMessage fromPubsubData(byte[] data) { + String jsonStr = new String(Base64.getDecoder().decode(data), StandardCharsets.UTF_8); + Map jsonMap = gson.fromJson(jsonStr, Map.class); + + return new OcrTranslateApiMessage( + jsonMap.get("text"), jsonMap.get("filename"), jsonMap.get("lang")); + } + + public byte[] toPubsubData() { + return gson.toJson(this).getBytes(StandardCharsets.UTF_8); + } +} +// [END functions_ocr_translate_pojo] diff --git a/functions/v2/ocr/ocr-save-result/src/main/java/functions/eventpojos/Message.java b/functions/v2/ocr/ocr-save-result/src/main/java/functions/eventpojos/Message.java new file mode 100644 index 00000000000..1bc64926807 --- /dev/null +++ b/functions/v2/ocr/ocr-save-result/src/main/java/functions/eventpojos/Message.java @@ -0,0 +1,31 @@ +/* + * Copyright 2022 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.eventpojos; + +import java.time.OffsetDateTime; +import java.util.Map; + +// Represents a PubSub message +// https://cloud.google.com/pubsub/docs/reference/rest/v1/PubsubMessage +@lombok.Data +public class Message { + private Map attributes; + private String data; + private String messageId; + private String orderingKey; + private OffsetDateTime publishTime; +} diff --git a/functions/v2/ocr/ocr-save-result/src/main/java/functions/eventpojos/MessagePublishedData.java b/functions/v2/ocr/ocr-save-result/src/main/java/functions/eventpojos/MessagePublishedData.java new file mode 100644 index 00000000000..bc5030f29b7 --- /dev/null +++ b/functions/v2/ocr/ocr-save-result/src/main/java/functions/eventpojos/MessagePublishedData.java @@ -0,0 +1,24 @@ +/* + * Copyright 2022 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.eventpojos; + +// The event data when a message is published to a topic. +@lombok.Data +public class MessagePublishedData { + private Message message; + private String subscription; +} diff --git a/functions/v2/ocr/ocr-save-result/src/test/java/functions/OcrSaveResultTest.java b/functions/v2/ocr/ocr-save-result/src/test/java/functions/OcrSaveResultTest.java new file mode 100644 index 00000000000..7a25275c2c0 --- /dev/null +++ b/functions/v2/ocr/ocr-save-result/src/test/java/functions/OcrSaveResultTest.java @@ -0,0 +1,150 @@ +/* + * Copyright 2022 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 com.google.cloud.storage.BlobInfo; +import com.google.cloud.storage.Storage; +import com.google.cloud.storage.StorageOptions; +import com.google.common.testing.TestLogHandler; +import com.google.common.truth.Truth; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import functions.eventpojos.Message; +import functions.eventpojos.MessagePublishedData; +import io.cloudevents.CloudEvent; +import io.cloudevents.core.builder.CloudEventBuilder; +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.OffsetDateTime; +import java.util.Base64; +import java.util.List; +import java.util.UUID; +import java.util.logging.LogRecord; +import java.util.logging.Logger; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +public class OcrSaveResultTest { + private static String RESULT_BUCKET = System.getenv("RESULT_BUCKET"); + + private static final Logger logger = Logger.getLogger(OcrSaveResult.class.getName()); + + private static final TestLogHandler LOG_HANDLER = new TestLogHandler(); + + // Create custom serializer to handle timestamps in event data + class DateSerializer implements JsonSerializer { + @Override + public JsonElement serialize( + OffsetDateTime time, Type typeOfSrc, JsonSerializationContext context) + throws JsonParseException { + return new JsonPrimitive(time.toString()); + } + } + + private final Gson gson = + new GsonBuilder().registerTypeAdapter(OffsetDateTime.class, new DateSerializer()).create(); + + private static final Storage STORAGE = StorageOptions.getDefaultInstance().getService(); + private static final String RANDOM_STRING = UUID.randomUUID().toString(); + + @BeforeClass + public static void setUpClass() { + assertThat(RESULT_BUCKET).isNotNull(); + logger.addHandler(LOG_HANDLER); + } + + @After + public void afterTest() { + LOG_HANDLER.clear(); + } + + @AfterClass + public static void tearDownClass() { + String deletedFilename = String.format("test-%s.jpg_to_es.txt", RANDOM_STRING); + STORAGE.delete(RESULT_BUCKET, deletedFilename); + } + + @Test(expected = IllegalArgumentException.class) + public void functionsOcrSave_shouldValidateParams() throws IOException, URISyntaxException { + MessagePublishedData data = new MessagePublishedData(); + Message message = new Message(); + message.setData(new String(Base64.getEncoder().encode("{}".getBytes()))); + data.setMessage(message); + + CloudEvent event = + CloudEventBuilder.v1() + .withId("000") + .withType("google.cloud.pubsub.topic.v1.messagePublished") + .withSource(new URI("curl-command")) + .withData("application/json", gson.toJson(data).getBytes()) + .build(); + + new OcrSaveResult().accept(event); + } + + @Test + public void functionsOcrSave_shouldPublishTranslatedText() + throws IOException, URISyntaxException { + String text = "Wake up human!"; + String filename = String.format("test-%s.jpg", RANDOM_STRING); + String lang = "es"; + + JsonObject dataJson = new JsonObject(); + dataJson.addProperty("text", text); + dataJson.addProperty("filename", filename); + dataJson.addProperty("lang", lang); + + MessagePublishedData data = new MessagePublishedData(); + Message message = new Message(); + message.setData(new String(Base64.getEncoder().encode(gson.toJson(dataJson).getBytes()))); + data.setMessage(message); + CloudEvent event = + CloudEventBuilder.v1() + .withId("000") + .withType("google.cloud.pubsub.topic.v1.messagePublished") + .withSource(new URI("curl-command")) + .withData("application/json", gson.toJson(data).getBytes()) + .build(); + + new OcrSaveResult().accept(event); + + String resultFilename = filename + "_to_es.txt"; + + // Check log messages + List logs = LOG_HANDLER.getStoredLogRecords(); + String expectedMessage = + String.format("Saving result to %s in bucket %s", resultFilename, RESULT_BUCKET); + Truth.assertThat(LOG_HANDLER.getStoredLogRecords().get(1).getMessage()) + .isEqualTo(expectedMessage); + + // Check that file was written + BlobInfo resultBlob = STORAGE.get(RESULT_BUCKET, resultFilename); + assertThat(resultBlob).isNotNull(); + } +} diff --git a/functions/v2/ocr/ocr-translate-text/pom.xml b/functions/v2/ocr/ocr-translate-text/pom.xml new file mode 100644 index 00000000000..8cc2aeda734 --- /dev/null +++ b/functions/v2/ocr/ocr-translate-text/pom.xml @@ -0,0 +1,130 @@ + + + + + + 4.0.0 + + com.example.cloud.functions + functions-ocr-translate-text + + + com.google.cloud.samples + shared-configuration + 1.2.0 + + + + 11 + 11 + UTF-8 + + + + + + com.google.cloud + libraries-bom + 25.4.0 + pom + import + + + + + + + com.google.cloud.functions + functions-framework-api + 1.0.4 + provided + + + com.google.cloud + google-cloud-translate + + + com.google.cloud + google-cloud-pubsub + + + io.cloudevents + cloudevents-core + 2.3.0 + + + org.projectlombok + lombok + 1.18.24 + + + + junit + junit + 4.13.2 + test + + + com.google.truth + truth + 1.1.3 + test + + + com.google.guava + guava-testlib + 31.1-jre + test + + + + + + + + com.google.cloud.functions + function-maven-plugin + 0.10.0 + + + functions.OcrTranslateText + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M7 + + ${skipTests} + sponge_log + false + + + + + diff --git a/functions/v2/ocr/ocr-translate-text/src/main/java/functions/OcrTranslateApiMessage.java b/functions/v2/ocr/ocr-translate-text/src/main/java/functions/OcrTranslateApiMessage.java new file mode 100644 index 00000000000..cd880b9a3c7 --- /dev/null +++ b/functions/v2/ocr/ocr-translate-text/src/main/java/functions/OcrTranslateApiMessage.java @@ -0,0 +1,74 @@ +/* + * Copyright 2022 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_ocr_translate_pojo] + +import com.google.gson.Gson; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; + +// Object for storing OCR translation requests +public class OcrTranslateApiMessage { + private static final Gson gson = new Gson(); + + private String text; + private String filename; + private String lang; + + public OcrTranslateApiMessage(String text, String filename, String lang) { + if (text == null) { + throw new IllegalArgumentException("Missing text parameter"); + } + if (filename == null) { + throw new IllegalArgumentException("Missing filename parameter"); + } + if (lang == null) { + throw new IllegalArgumentException("Missing lang parameter"); + } + + this.text = text; + this.filename = filename; + this.lang = lang; + } + + public String getText() { + return text; + } + + public String getFilename() { + return filename; + } + + public String getLang() { + return lang; + } + + public static OcrTranslateApiMessage fromPubsubData(byte[] data) { + String jsonStr = new String(Base64.getDecoder().decode(data), StandardCharsets.UTF_8); + Map jsonMap = gson.fromJson(jsonStr, Map.class); + + return new OcrTranslateApiMessage( + jsonMap.get("text"), jsonMap.get("filename"), jsonMap.get("lang")); + } + + public byte[] toPubsubData() { + return gson.toJson(this).getBytes(StandardCharsets.UTF_8); + } +} +// [END functions_ocr_translate_pojo] diff --git a/functions/v2/ocr/ocr-translate-text/src/main/java/functions/OcrTranslateText.java b/functions/v2/ocr/ocr-translate-text/src/main/java/functions/OcrTranslateText.java new file mode 100644 index 00000000000..dafec796657 --- /dev/null +++ b/functions/v2/ocr/ocr-translate-text/src/main/java/functions/OcrTranslateText.java @@ -0,0 +1,137 @@ +/* + * Copyright 2022 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_ocr_translate] + +import com.google.cloud.functions.CloudEventsFunction; +import com.google.cloud.pubsub.v1.Publisher; +import com.google.cloud.translate.v3.LocationName; +import com.google.cloud.translate.v3.TranslateTextRequest; +import com.google.cloud.translate.v3.TranslateTextResponse; +import com.google.cloud.translate.v3.TranslationServiceClient; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.protobuf.ByteString; +import com.google.pubsub.v1.ProjectTopicName; +import com.google.pubsub.v1.PubsubMessage; +import functions.eventpojos.MessagePublishedData; +import io.cloudevents.CloudEvent; +import java.io.IOException; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.time.OffsetDateTime; +import java.util.concurrent.ExecutionException; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class OcrTranslateText implements CloudEventsFunction { + private static final Logger logger = Logger.getLogger(OcrTranslateText.class.getName()); + + // TODO set these environment variables + private static final String PROJECT_ID = getenv("GCP_PROJECT"); + private static final String RESULTS_TOPIC_NAME = getenv("RESULT_TOPIC"); + private static final String LOCATION_NAME = LocationName.of(PROJECT_ID, "global").toString(); + + private Publisher publisher; + + public OcrTranslateText() throws IOException { + publisher = Publisher.newBuilder(ProjectTopicName.of(PROJECT_ID, RESULTS_TOPIC_NAME)).build(); + } + + // Create custom deserializer to handle timestamps in event data + class DateDeserializer implements JsonDeserializer { + @Override + public OffsetDateTime deserialize( + JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + return OffsetDateTime.parse(json.getAsString()); + } + } + + Gson gson = + new GsonBuilder().registerTypeAdapter(OffsetDateTime.class, new DateDeserializer()).create(); + + @Override + public void accept(CloudEvent event) throws InterruptedException, IOException { + MessagePublishedData data = + gson.fromJson( + new String(event.getData().toBytes(), StandardCharsets.UTF_8), + MessagePublishedData.class); + OcrTranslateApiMessage ocrMessage = + OcrTranslateApiMessage.fromPubsubData( + data.getMessage().getData().getBytes(StandardCharsets.UTF_8)); + + String targetLang = ocrMessage.getLang(); + logger.info("Translating text into " + targetLang); + + // Translate text to target language + String text = ocrMessage.getText(); + TranslateTextRequest request = + TranslateTextRequest.newBuilder() + .setParent(LOCATION_NAME) + .setMimeType("text/plain") + .setTargetLanguageCode(targetLang) + .addContents(text) + .build(); + + TranslateTextResponse response; + try (TranslationServiceClient client = TranslationServiceClient.create()) { + response = client.translateText(request); + } catch (IOException e) { + // Log error (since IOException cannot be thrown by a function) + logger.log(Level.SEVERE, "Error translating text: " + e.getMessage(), e); + return; + } + if (response.getTranslationsCount() == 0) { + return; + } + + String translatedText = response.getTranslations(0).getTranslatedText(); + logger.info("Translated text: " + translatedText); + + // Send translated text to (subsequent) Pub/Sub topic + String filename = ocrMessage.getFilename(); + OcrTranslateApiMessage translateMessage = + new OcrTranslateApiMessage(translatedText, filename, targetLang); + try { + ByteString byteStr = ByteString.copyFrom(translateMessage.toPubsubData()); + PubsubMessage pubsubApiMessage = PubsubMessage.newBuilder().setData(byteStr).build(); + publisher.publish(pubsubApiMessage).get(); + logger.info("Text translated to " + targetLang); + } catch (InterruptedException | ExecutionException e) { + // Log error (since these exception types cannot be thrown by a function) + logger.log(Level.SEVERE, "Error publishing translation save request: " + e.getMessage(), e); + } + } + + // Avoid ungraceful deployment failures due to unset environment variables. + // If you get this warning you should redeploy with the variable set. + private static String getenv(String name) { + String value = System.getenv(name); + if (value == null) { + logger.warning("Environment variable " + name + " was not set"); + value = "MISSING"; + } + return value; + } +} +// [END functions_ocr_translate] diff --git a/functions/v2/ocr/ocr-translate-text/src/main/java/functions/eventpojos/Message.java b/functions/v2/ocr/ocr-translate-text/src/main/java/functions/eventpojos/Message.java new file mode 100644 index 00000000000..1bc64926807 --- /dev/null +++ b/functions/v2/ocr/ocr-translate-text/src/main/java/functions/eventpojos/Message.java @@ -0,0 +1,31 @@ +/* + * Copyright 2022 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.eventpojos; + +import java.time.OffsetDateTime; +import java.util.Map; + +// Represents a PubSub message +// https://cloud.google.com/pubsub/docs/reference/rest/v1/PubsubMessage +@lombok.Data +public class Message { + private Map attributes; + private String data; + private String messageId; + private String orderingKey; + private OffsetDateTime publishTime; +} diff --git a/functions/v2/ocr/ocr-translate-text/src/main/java/functions/eventpojos/MessagePublishedData.java b/functions/v2/ocr/ocr-translate-text/src/main/java/functions/eventpojos/MessagePublishedData.java new file mode 100644 index 00000000000..bc5030f29b7 --- /dev/null +++ b/functions/v2/ocr/ocr-translate-text/src/main/java/functions/eventpojos/MessagePublishedData.java @@ -0,0 +1,24 @@ +/* + * Copyright 2022 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.eventpojos; + +// The event data when a message is published to a topic. +@lombok.Data +public class MessagePublishedData { + private Message message; + private String subscription; +} diff --git a/functions/v2/ocr/ocr-translate-text/src/test/java/functions/OcrTranslateTextTest.java b/functions/v2/ocr/ocr-translate-text/src/test/java/functions/OcrTranslateTextTest.java new file mode 100644 index 00000000000..63c8eae334f --- /dev/null +++ b/functions/v2/ocr/ocr-translate-text/src/test/java/functions/OcrTranslateTextTest.java @@ -0,0 +1,130 @@ +/* + * Copyright 2022 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 com.google.common.testing.TestLogHandler; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import functions.eventpojos.Message; +import functions.eventpojos.MessagePublishedData; +import io.cloudevents.CloudEvent; +import io.cloudevents.core.builder.CloudEventBuilder; +import java.io.IOException; +import java.lang.reflect.Type; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.OffsetDateTime; +import java.util.Base64; +import java.util.List; +import java.util.logging.LogRecord; +import java.util.logging.Logger; +import org.junit.After; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class OcrTranslateTextTest { + private static final Logger logger = Logger.getLogger(OcrTranslateText.class.getName()); + + private static final TestLogHandler LOG_HANDLER = new TestLogHandler(); + + // Create custom serializer to handle timestamps in event data + class DateSerializer implements JsonSerializer { + @Override + public JsonElement serialize( + OffsetDateTime time, Type typeOfSrc, JsonSerializationContext context) + throws JsonParseException { + return new JsonPrimitive(time.toString()); + } + } + + private final Gson gson = + new GsonBuilder().registerTypeAdapter(OffsetDateTime.class, new DateSerializer()).create(); + + private static OcrTranslateText sampleUnderTest; + + @BeforeClass + public static void setUpClass() throws IOException { + assertThat(System.getenv("RESULT_TOPIC")).isNotNull(); + sampleUnderTest = new OcrTranslateText(); + logger.addHandler(LOG_HANDLER); + } + + @After + public void afterTest() { + LOG_HANDLER.clear(); + } + + @Test(expected = IllegalArgumentException.class) + public void functionsOcrTranslate_shouldValidateParams() + throws IOException, URISyntaxException, InterruptedException { + MessagePublishedData data = new MessagePublishedData(); + Message message = new Message(); + message.setData(new String(Base64.getEncoder().encode("{}".getBytes()))); + data.setMessage(message); + + CloudEvent event = + CloudEventBuilder.v1() + .withId("000") + .withType("google.cloud.pubsub.topic.v1.messagePublished") + .withSource(new URI("curl-command")) + .withData("application/json", gson.toJson(data).getBytes()) + .build(); + sampleUnderTest.accept(event); + } + + @Test + public void functionsOcrTranslate_shouldTranslateText() + throws IOException, URISyntaxException, InterruptedException { + String text = "Wake up human!"; + String filename = "wakeupcat.jpg"; + String lang = "es"; + + JsonObject dataJson = new JsonObject(); + dataJson.addProperty("text", text); + dataJson.addProperty("filename", filename); + dataJson.addProperty("lang", lang); + + MessagePublishedData data = new MessagePublishedData(); + Message message = new Message(); + message.setData(new String(Base64.getEncoder().encode(gson.toJson(dataJson).getBytes()))); + data.setMessage(message); + CloudEvent event = + CloudEventBuilder.v1() + .withId("000") + .withType("google.cloud.pubsub.topic.v1.messagePublished") + .withSource(new URI("curl-command")) + .withData("application/json", gson.toJson(data).getBytes()) + .build(); + + sampleUnderTest.accept(event); + + List logs = LOG_HANDLER.getStoredLogRecords(); + assertThat(logs.get(1).getMessage()).contains("¡Despierta humano!"); + assertThat(logs.get(2).getMessage()).isEqualTo("Text translated to es"); + } +}