Skip to content
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

feat(doc-handling): Some investigations to use documents in the REST … #2976

Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import io.camunda.zeebe.spring.client.jobhandling.CommandExceptionHandlingStrategy;
import io.camunda.zeebe.spring.client.jobhandling.JobWorkerManager;
import io.camunda.zeebe.spring.client.metrics.MetricsRecorder;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -44,7 +45,7 @@ public OutboundConnectorFactory outboundConnectorFactory() {
}

@Bean
public DocumentFactory documentFactory() {
public DocumentFactory documentFactory() throws IOException {
johnBgood marked this conversation as resolved.
Show resolved Hide resolved
return new DocumentFactoryImpl(InMemoryDocumentStore.INSTANCE);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ public class Base64OperationExecutor implements DocumentOperationExecutor {

@Override
public boolean matches(DocumentOperation operationReference) {
return false;
return "base64".equalsIgnoreCase(operationReference.name());
}

@Override
public Object execute(DocumentOperation operationReference, Document document) {
return null;
return document.asBase64();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,8 @@ public InputStream getDocumentContent(CamundaDocumentReference reference) {
public void deleteDocument(CamundaDocumentReference reference) {
documents.remove(reference.documentId());
}

public void clear() {
documents.clear();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@
import io.camunda.connector.api.error.ConnectorException;
import io.camunda.connector.api.json.ConnectorsObjectMapperSupplier;
import io.camunda.connector.http.base.model.HttpCommonRequest;
import io.camunda.connector.http.base.utils.DocumentHelper;
import io.camunda.document.Document;
import io.camunda.document.DocumentMetadata;
import java.io.BufferedInputStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import org.apache.hc.client5.http.entity.mime.HttpMultipartMode;
Expand Down Expand Up @@ -89,6 +94,9 @@ private Optional<ContentType> tryGetContentType(HttpCommonRequest request) {

private HttpEntity createStringEntity(HttpCommonRequest request) {
Object body = request.getBody();
if (body instanceof Map map) {
body = new DocumentHelper().createDocuments(map, Document::asByteArray);
}
Optional<ContentType> contentType = tryGetContentType(request);
try {
return body instanceof String s
Expand Down Expand Up @@ -119,9 +127,23 @@ private HttpEntity createMultiPartEntity(Map<?, ?> body, ContentType contentType
builder.setMode(HttpMultipartMode.LEGACY);
Optional.ofNullable(contentType.getParameter("boundary")).ifPresent(builder::setBoundary);
for (Map.Entry<?, ?> entry : body.entrySet()) {
builder.addTextBody(
String.valueOf(entry.getKey()), String.valueOf(entry.getValue()), MULTIPART_FORM_DATA);
if (Objects.requireNonNull(entry.getValue()) instanceof Document document) {
streamDocumentContent(entry, document, builder);
} else {
builder.addTextBody(
String.valueOf(entry.getKey()), String.valueOf(entry.getValue()), MULTIPART_FORM_DATA);
}
}
return builder.build();
}

private void streamDocumentContent(
Map.Entry<?, ?> entry, Document document, MultipartEntityBuilder builder) {
DocumentMetadata metadata = document.metadata();
builder.addBinaryBody(
String.valueOf(entry.getKey()),
new BufferedInputStream(document.asInputStream()),
Dismissed Show dismissed Hide dismissed
ContentType.DEFAULT_BINARY,
metadata.getFileName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
* under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright
* ownership. Camunda licenses this file to you under the Apache License,
* Version 2.0; 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 io.camunda.connector.http.base.utils;

import io.camunda.document.CamundaDocument;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;

public class DocumentHelper {
johnBgood marked this conversation as resolved.
Show resolved Hide resolved

/**
* Traverse the {@link Map} recursively and create all Documents found in the map.
*
* @param input the input map
* @param transformer the transformer to apply to each document (e.g. convert to Base64 etc)
*/
public Object createDocuments(Object input, Function<CamundaDocument, Object> transformer) {
johnBgood marked this conversation as resolved.
Show resolved Hide resolved
return switch (input) {
case Map<?, ?> map -> map.entrySet().stream()
.map(
(Map.Entry<?, ?> e) ->
new AbstractMap.SimpleEntry<>(
e.getKey(), createDocuments(e.getValue(), transformer)))
.collect(
Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue));

case Collection list -> list.stream().map(o -> createDocuments(o, transformer)).toList();
case CamundaDocument doc -> transformer.apply(doc);
default -> input;
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@
import io.camunda.connector.http.base.model.auth.BasicAuthentication;
import io.camunda.connector.http.base.model.auth.BearerAuthentication;
import io.camunda.connector.http.base.model.auth.OAuthAuthentication;
import io.camunda.document.CamundaDocument;
import io.camunda.document.DocumentMetadata;
import io.camunda.document.store.CamundaDocumentStore;
import io.camunda.document.store.DocumentCreationRequest;
import io.camunda.document.store.InMemoryDocumentStore;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
Expand All @@ -88,11 +93,54 @@ public class CustomApacheHttpClientTest {

private final CustomApacheHttpClient customApacheHttpClient = CustomApacheHttpClient.getDefault();
private final ObjectMapper objectMapper = ConnectorsObjectMapperSupplier.DEFAULT_MAPPER;
private final CamundaDocumentStore store = InMemoryDocumentStore.INSTANCE;

private String getHostAndPort(WireMockRuntimeInfo wmRuntimeInfo) {
return "http://localhost:" + wmRuntimeInfo.getHttpPort();
}

@Nested
class DocumentUploadTests {

@Test
public void shouldReturn201_whenUploadDocument(WireMockRuntimeInfo wmRuntimeInfo) {
stubFor(post("/path").withMultipartRequestBody(aMultipart()).willReturn(created()));
var ref =
store.createDocument(
DocumentCreationRequest.from("The content of this file".getBytes())
.metadata(new DocumentMetadata(Map.of(DocumentMetadata.FILE_NAME, "file.txt")))
.build());
HttpCommonRequest request = new HttpCommonRequest();
request.setMethod(HttpMethod.POST);
request.setHeaders(Map.of("Content-Type", ContentType.MULTIPART_FORM_DATA.getMimeType()));
request.setUrl(getHostAndPort(wmRuntimeInfo) + "/path");
request.setBody(
Map.of(
"otherField",
"otherValue",
"document",
new CamundaDocument(ref.metadata(), ref, store)));
HttpCommonResult result = customApacheHttpClient.execute(request);
assertThat(result).isNotNull();
assertThat(result.status()).isEqualTo(201);

verify(
postRequestedFor(urlEqualTo("/path"))
.withHeader(
"Content-Type", and(containing("multipart/form-data"), containing("boundary=")))
.withRequestBodyPart(
new MultipartValuePatternBuilder()
.withName("otherField")
.withBody(equalTo("otherValue"))
.build())
.withRequestBodyPart(
new MultipartValuePatternBuilder()
.withName("document")
.withBody(equalTo("The content of this file"))
.build()));
}
}

@Nested
class ProxyTests {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH
* under one or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information regarding copyright
* ownership. Camunda licenses this file to you under the Apache License,
* Version 2.0; 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 io.camunda.connector.http.base.utils;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import io.camunda.document.CamundaDocument;
import io.camunda.document.DocumentMetadata;
import io.camunda.document.reference.CamundaDocumentReferenceImpl;
import io.camunda.document.store.InMemoryDocumentStore;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

public class DocumentHelperTest {

@AfterEach
public void tearDown() {
InMemoryDocumentStore.INSTANCE.clear();
}

@Test
public void shouldCreateDocuments_whenMapInput() {
// given
DocumentHelper documentHelper = new DocumentHelper();
CamundaDocument document =
new CamundaDocument(
new DocumentMetadata(Map.of()),
new CamundaDocumentReferenceImpl("store", "id1", new DocumentMetadata(Map.of())),
InMemoryDocumentStore.INSTANCE);
Map<String, Object> input =
Map.of("body", Map.of("content", Arrays.asList(document, document, document)));
Function<CamundaDocument, Object> transformer = mock(Function.class);
when(transformer.apply(document)).thenReturn("transformed".getBytes(StandardCharsets.UTF_8));

// when
Object res = documentHelper.createDocuments(input, transformer);

// then
assertThat(res).isInstanceOf(Map.class);
verify(transformer, times(3)).apply(document);
assertThat(((Map<?, ?>) res).get("body")).isInstanceOf(Map.class);
assertThat(((Map<?, ?>) ((Map<?, ?>) res).get("body")).get("content")).isInstanceOf(List.class);
assertThat((List<byte[]>) ((Map<?, ?>) ((Map<?, ?>) res).get("body")).get("content"))
.containsAll(
Arrays.asList(
"transformed".getBytes(StandardCharsets.UTF_8),
"transformed".getBytes(StandardCharsets.UTF_8),
"transformed".getBytes(StandardCharsets.UTF_8)));
}

@Test
public void shouldCreateDocuments_whenListInput() {
// given
DocumentHelper documentHelper = new DocumentHelper();
CamundaDocument document =
new CamundaDocument(
new DocumentMetadata(Map.of()),
new CamundaDocumentReferenceImpl("store", "id1", new DocumentMetadata(Map.of())),
InMemoryDocumentStore.INSTANCE);
List<Object> input = Arrays.asList(document, document, document);
Function<CamundaDocument, Object> transformer = mock(Function.class);
when(transformer.apply(document)).thenReturn("transformed".getBytes(StandardCharsets.UTF_8));

// when
Object res = documentHelper.createDocuments(input, transformer);

// then
assertThat(res).isInstanceOf(List.class);
verify(transformer, times(3)).apply(document);
assertThat((List<byte[]>) res)
.containsAll(
Arrays.asList(
"transformed".getBytes(StandardCharsets.UTF_8),
"transformed".getBytes(StandardCharsets.UTF_8),
"transformed".getBytes(StandardCharsets.UTF_8)));
}

@Test
public void shouldNotCreateDocuments_whenNoDocumentProvided() {
// given
DocumentHelper documentHelper = new DocumentHelper();
Map<String, Object> input = Map.of("body", Map.of("content", "no document"));
Function<CamundaDocument, Object> transformer = mock(Function.class);

// when
Object res = documentHelper.createDocuments(input, transformer);

// then
assertThat(res).isInstanceOf(Map.class);
assertThat(((Map<?, ?>) res).get("body")).isInstanceOf(Map.class);
assertThat(((Map<?, ?>) ((Map<?, ?>) res).get("body")).get("content")).isEqualTo("no document");
}
}
Loading