diff --git a/connector-runtime/connector-runtime-core/pom.xml b/connector-runtime/connector-runtime-core/pom.xml index 8d54ef4dd2..f4f59fceaf 100644 --- a/connector-runtime/connector-runtime-core/pom.xml +++ b/connector-runtime/connector-runtime-core/pom.xml @@ -25,6 +25,11 @@ connector-core + + io.camunda.connector + connector-document + + com.fasterxml.jackson.core @@ -95,5 +100,11 @@ test + + org.skyscreamer + jsonassert + test + + \ No newline at end of file diff --git a/connector-runtime/connector-runtime-core/src/main/java/io/camunda/connector/runtime/core/inbound/DefaultInboundConnectorContextFactory.java b/connector-runtime/connector-runtime-core/src/main/java/io/camunda/connector/runtime/core/inbound/DefaultInboundConnectorContextFactory.java index a5f50bebb9..e1db3f77bf 100644 --- a/connector-runtime/connector-runtime-core/src/main/java/io/camunda/connector/runtime/core/inbound/DefaultInboundConnectorContextFactory.java +++ b/connector-runtime/connector-runtime-core/src/main/java/io/camunda/connector/runtime/core/inbound/DefaultInboundConnectorContextFactory.java @@ -25,6 +25,7 @@ import io.camunda.connector.runtime.core.inbound.correlation.InboundCorrelationHandler; import io.camunda.connector.runtime.core.inbound.details.InboundConnectorDetails.ValidInboundConnectorDetails; import io.camunda.connector.runtime.core.secret.SecretProviderAggregator; +import io.camunda.document.factory.DocumentFactory; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.function.Consumer; @@ -35,18 +36,21 @@ public class DefaultInboundConnectorContextFactory implements InboundConnectorCo private final SecretProviderAggregator secretProviderAggregator; private final ValidationProvider validationProvider; private final OperateClientAdapter operateClientAdapter; + private final DocumentFactory documentFactory; public DefaultInboundConnectorContextFactory( final ObjectMapper mapper, final InboundCorrelationHandler correlationHandler, final SecretProviderAggregator secretProviderAggregator, final ValidationProvider validationProvider, - final OperateClientAdapter operateClientAdapter) { + final OperateClientAdapter operateClientAdapter, + final DocumentFactory documentFactory) { this.objectMapper = mapper; this.correlationHandler = correlationHandler; this.secretProviderAggregator = secretProviderAggregator; this.validationProvider = validationProvider; this.operateClientAdapter = operateClientAdapter; + this.documentFactory = documentFactory; } @Override @@ -60,6 +64,7 @@ public > InboundConnectorContext createC new InboundConnectorContextImpl( secretProviderAggregator, validationProvider, + documentFactory, connectorDetails, correlationHandler, cancellationCallback, diff --git a/connector-runtime/connector-runtime-core/src/main/java/io/camunda/connector/runtime/core/inbound/InboundConnectorContextImpl.java b/connector-runtime/connector-runtime-core/src/main/java/io/camunda/connector/runtime/core/inbound/InboundConnectorContextImpl.java index 9daf877e51..ea133937c5 100644 --- a/connector-runtime/connector-runtime-core/src/main/java/io/camunda/connector/runtime/core/inbound/InboundConnectorContextImpl.java +++ b/connector-runtime/connector-runtime-core/src/main/java/io/camunda/connector/runtime/core/inbound/InboundConnectorContextImpl.java @@ -31,6 +31,11 @@ import io.camunda.connector.runtime.core.inbound.correlation.InboundCorrelationHandler; import io.camunda.connector.runtime.core.inbound.details.InboundConnectorDetails; import io.camunda.connector.runtime.core.inbound.details.InboundConnectorDetails.ValidInboundConnectorDetails; +import io.camunda.document.Document; +import io.camunda.document.factory.DocumentFactory; +import io.camunda.document.factory.DocumentFactoryImpl; +import io.camunda.document.store.DocumentCreationRequest; +import io.camunda.document.store.InMemoryDocumentStore; import java.util.List; import java.util.Map; import java.util.Objects; @@ -51,20 +56,22 @@ public class InboundConnectorContextImpl extends AbstractConnectorContext private final ObjectMapper objectMapper; private final Consumer cancellationCallback; - - private Health health = Health.unknown(); - private final EvictingQueue logs; + private final DocumentFactory documentFactory; + private Health health = Health.unknown(); + private Map propertiesWithSecrets; public InboundConnectorContextImpl( SecretProvider secretProvider, ValidationProvider validationProvider, + DocumentFactory documentFactory, ValidInboundConnectorDetails connectorDetails, InboundCorrelationHandler correlationHandler, Consumer cancellationCallback, ObjectMapper objectMapper, EvictingQueue logs) { super(secretProvider, validationProvider); + this.documentFactory = documentFactory; this.correlationHandler = correlationHandler; this.connectorDetails = connectorDetails; this.properties = @@ -75,6 +82,25 @@ public InboundConnectorContextImpl( this.logs = logs; } + public InboundConnectorContextImpl( + SecretProvider secretProvider, + ValidationProvider validationProvider, + ValidInboundConnectorDetails connectorDetails, + InboundCorrelationHandler correlationHandler, + Consumer cancellationCallback, + ObjectMapper objectMapper, + EvictingQueue logs) { + this( + secretProvider, + validationProvider, + new DocumentFactoryImpl(InMemoryDocumentStore.INSTANCE), + connectorDetails, + correlationHandler, + cancellationCallback, + objectMapper, + logs); + } + @Override public CorrelationResult correlateWithResult(Object variables) { try { @@ -149,7 +175,10 @@ public List connectorElements() { return connectorDetails.connectorElements(); } - private Map propertiesWithSecrets; + @Override + public Document createDocument(DocumentCreationRequest request) { + return documentFactory.create(request); + } private Map getPropertiesWithSecrets(Map properties) { if (propertiesWithSecrets == null) { diff --git a/connector-runtime/connector-runtime-core/src/main/java/io/camunda/connector/runtime/core/inbound/InboundIntermediateConnectorContextImpl.java b/connector-runtime/connector-runtime-core/src/main/java/io/camunda/connector/runtime/core/inbound/InboundIntermediateConnectorContextImpl.java index 963f83a0ca..1d21c787ab 100644 --- a/connector-runtime/connector-runtime-core/src/main/java/io/camunda/connector/runtime/core/inbound/InboundIntermediateConnectorContextImpl.java +++ b/connector-runtime/connector-runtime-core/src/main/java/io/camunda/connector/runtime/core/inbound/InboundIntermediateConnectorContextImpl.java @@ -27,6 +27,8 @@ import io.camunda.connector.api.validation.ValidationProvider; import io.camunda.connector.runtime.core.inbound.correlation.InboundCorrelationHandler; import io.camunda.connector.runtime.core.inbound.correlation.MessageCorrelationPoint.BoundaryEventCorrelationPoint; +import io.camunda.document.Document; +import io.camunda.document.store.DocumentCreationRequest; import io.camunda.operate.model.FlowNodeInstance; import java.util.List; import java.util.Map; @@ -131,6 +133,11 @@ public Queue getLogs() { return inboundContext.getLogs(); } + @Override + public Document createDocument(DocumentCreationRequest request) { + return inboundContext.createDocument(request); + } + @Override public List connectorElements() { return inboundContext.connectorElements(); diff --git a/connector-runtime/connector-runtime-core/src/main/java/io/camunda/connector/runtime/core/outbound/ConnectorJobHandler.java b/connector-runtime/connector-runtime-core/src/main/java/io/camunda/connector/runtime/core/outbound/ConnectorJobHandler.java index 036e4af0fb..60c94a3bd1 100644 --- a/connector-runtime/connector-runtime-core/src/main/java/io/camunda/connector/runtime/core/outbound/ConnectorJobHandler.java +++ b/connector-runtime/connector-runtime-core/src/main/java/io/camunda/connector/runtime/core/outbound/ConnectorJobHandler.java @@ -33,6 +33,7 @@ import io.camunda.connector.runtime.core.outbound.ErrorExpressionJobContext.ErrorExpressionJob; import io.camunda.connector.runtime.core.secret.SecretProviderAggregator; import io.camunda.connector.runtime.core.secret.SecretProviderDiscovery; +import io.camunda.document.factory.DocumentFactory; import io.camunda.zeebe.client.api.command.FinalCommandStep; import io.camunda.zeebe.client.api.response.ActivatedJob; import io.camunda.zeebe.client.api.response.CompleteJobResponse; @@ -58,6 +59,8 @@ public class ConnectorJobHandler implements JobHandler { protected ValidationProvider validationProvider; + protected DocumentFactory documentFactory; + protected ObjectMapper objectMapper; /** @@ -80,10 +83,12 @@ public ConnectorJobHandler( final OutboundConnectorFunction call, final SecretProvider secretProvider, final ValidationProvider validationProvider, + final DocumentFactory documentFactory, final ObjectMapper objectMapper) { this.call = call; this.secretProvider = secretProvider; this.validationProvider = validationProvider; + this.documentFactory = documentFactory; this.objectMapper = objectMapper; } @@ -180,7 +185,8 @@ public void handle(final JobClient client, final ActivatedJob job) { try { var context = - new JobHandlerContext(job, getSecretProvider(), validationProvider, objectMapper); + new JobHandlerContext( + job, getSecretProvider(), validationProvider, documentFactory, objectMapper); var response = call.execute(context); var responseVariables = ConnectorHelper.createOutputVariables( diff --git a/connector-runtime/connector-runtime-core/src/main/java/io/camunda/connector/runtime/core/outbound/JobHandlerContext.java b/connector-runtime/connector-runtime-core/src/main/java/io/camunda/connector/runtime/core/outbound/JobHandlerContext.java index 3af55aca54..5d1c4948dd 100644 --- a/connector-runtime/connector-runtime-core/src/main/java/io/camunda/connector/runtime/core/outbound/JobHandlerContext.java +++ b/connector-runtime/connector-runtime-core/src/main/java/io/camunda/connector/runtime/core/outbound/JobHandlerContext.java @@ -27,6 +27,11 @@ import io.camunda.connector.api.secret.SecretProvider; import io.camunda.connector.api.validation.ValidationProvider; import io.camunda.connector.runtime.core.AbstractConnectorContext; +import io.camunda.document.Document; +import io.camunda.document.factory.DocumentFactory; +import io.camunda.document.factory.DocumentFactoryImpl; +import io.camunda.document.store.DocumentCreationRequest; +import io.camunda.document.store.InMemoryDocumentStore; import io.camunda.zeebe.client.api.response.ActivatedJob; import java.util.Objects; import org.slf4j.Logger; @@ -45,19 +50,35 @@ public class JobHandlerContext extends AbstractConnectorContext private final ObjectMapper objectMapper; private final JobContext jobContext; + private final DocumentFactory documentFactory; private String jsonWithSecrets = null; public JobHandlerContext( final ActivatedJob job, final SecretProvider secretProvider, final ValidationProvider validationProvider, + final DocumentFactory documentFactory, final ObjectMapper objectMapper) { super(secretProvider, validationProvider); + this.documentFactory = documentFactory; this.job = job; this.objectMapper = objectMapper; this.jobContext = new ActivatedJobContext(job, this::getJsonReplacedWithSecrets); } + public JobHandlerContext( + final ActivatedJob job, + final SecretProvider secretProvider, + final ValidationProvider validationProvider, + final ObjectMapper objectMapper) { + this( + job, + secretProvider, + validationProvider, + new DocumentFactoryImpl(InMemoryDocumentStore.INSTANCE), + objectMapper); + } + @Override public T bindVariables(Class cls) { var mappedObject = mapJson(cls); @@ -110,6 +131,11 @@ public JobContext getJobContext() { return jobContext; } + @Override + public Document createDocument(DocumentCreationRequest document) { + return documentFactory.create(document); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/connector-runtime/connector-runtime-core/src/test/java/io/camunda/connector/runtime/core/inbound/DefaultInboundConnectorContextFactoryTest.java b/connector-runtime/connector-runtime-core/src/test/java/io/camunda/connector/runtime/core/inbound/DefaultInboundConnectorContextFactoryTest.java index 3e65598b81..dea238d025 100644 --- a/connector-runtime/connector-runtime-core/src/test/java/io/camunda/connector/runtime/core/inbound/DefaultInboundConnectorContextFactoryTest.java +++ b/connector-runtime/connector-runtime-core/src/test/java/io/camunda/connector/runtime/core/inbound/DefaultInboundConnectorContextFactoryTest.java @@ -27,6 +27,7 @@ import io.camunda.connector.runtime.core.inbound.correlation.InboundCorrelationHandler; import io.camunda.connector.runtime.core.inbound.details.InboundConnectorDetails.ValidInboundConnectorDetails; import io.camunda.connector.runtime.core.secret.SecretProviderAggregator; +import io.camunda.document.factory.DocumentFactory; import java.util.function.Consumer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -44,6 +45,7 @@ class DefaultInboundConnectorContextFactoryTest { @Mock private OperateClientAdapter operateClientAdapter; @Mock private Consumer cancellationCallback; @Mock private ValidInboundConnectorDetails newConnector; + @Mock private DocumentFactory documentFactory; private DefaultInboundConnectorContextFactory factory; @BeforeEach @@ -54,7 +56,8 @@ void setUp() { correlationHandler, secretProviderAggregator, validationProvider, - operateClientAdapter); + operateClientAdapter, + documentFactory); } @Test diff --git a/connector-runtime/connector-runtime-spring/src/main/java/io/camunda/connector/runtime/inbound/InboundConnectorRuntimeConfiguration.java b/connector-runtime/connector-runtime-spring/src/main/java/io/camunda/connector/runtime/inbound/InboundConnectorRuntimeConfiguration.java index 12cd8287a4..151be365cf 100644 --- a/connector-runtime/connector-runtime-spring/src/main/java/io/camunda/connector/runtime/inbound/InboundConnectorRuntimeConfiguration.java +++ b/connector-runtime/connector-runtime-spring/src/main/java/io/camunda/connector/runtime/inbound/InboundConnectorRuntimeConfiguration.java @@ -38,6 +38,7 @@ import io.camunda.connector.runtime.inbound.state.ProcessStateStore; import io.camunda.connector.runtime.inbound.state.TenantAwareProcessStateStoreImpl; import io.camunda.connector.runtime.inbound.webhook.WebhookConnectorRegistry; +import io.camunda.document.factory.DocumentFactory; import io.camunda.operate.CamundaOperateClient; import io.camunda.zeebe.client.ZeebeClient; import io.camunda.zeebe.spring.client.metrics.MetricsRecorder; @@ -58,6 +59,11 @@ public class InboundConnectorRuntimeConfiguration { @Value("${camunda.connector.inbound.message.ttl:PT1H}") private Duration messageTtl; + @Bean + public static InboundConnectorBeanDefinitionProcessor inboundConnectorBeanDefinitionProcessor() { + return new InboundConnectorBeanDefinitionProcessor(); + } + @Bean public ProcessElementContextFactory processElementContextFactory( ObjectMapper objectMapper, @@ -77,24 +83,21 @@ public InboundCorrelationHandler inboundCorrelationHandler( zeebeClient, feelEngine, metricsRecorder, elementContextFactory, messageTtl); } - @Bean - public static InboundConnectorBeanDefinitionProcessor inboundConnectorBeanDefinitionProcessor() { - return new InboundConnectorBeanDefinitionProcessor(); - } - @Bean public InboundConnectorContextFactory springInboundConnectorContextFactory( ObjectMapper mapper, InboundCorrelationHandler correlationHandler, SecretProviderAggregator secretProviderAggregator, @Autowired(required = false) ValidationProvider validationProvider, - OperateClientAdapter operateClientAdapter) { + OperateClientAdapter operateClientAdapter, + DocumentFactory documentFactory) { return new DefaultInboundConnectorContextFactory( mapper, correlationHandler, secretProviderAggregator, validationProvider, - operateClientAdapter); + operateClientAdapter, + documentFactory); } @Bean diff --git a/connector-runtime/connector-runtime-spring/src/main/java/io/camunda/connector/runtime/outbound/OutboundConnectorRuntimeConfiguration.java b/connector-runtime/connector-runtime-spring/src/main/java/io/camunda/connector/runtime/outbound/OutboundConnectorRuntimeConfiguration.java index 2129e6a1a5..20b511a6d6 100644 --- a/connector-runtime/connector-runtime-spring/src/main/java/io/camunda/connector/runtime/outbound/OutboundConnectorRuntimeConfiguration.java +++ b/connector-runtime/connector-runtime-spring/src/main/java/io/camunda/connector/runtime/outbound/OutboundConnectorRuntimeConfiguration.java @@ -24,6 +24,9 @@ import io.camunda.connector.runtime.core.secret.SecretProviderAggregator; import io.camunda.connector.runtime.outbound.lifecycle.OutboundConnectorAnnotationProcessor; import io.camunda.connector.runtime.outbound.lifecycle.OutboundConnectorManager; +import io.camunda.document.factory.DocumentFactory; +import io.camunda.document.factory.DocumentFactoryImpl; +import io.camunda.document.store.InMemoryDocumentStore; import io.camunda.zeebe.spring.client.jobhandling.CommandExceptionHandlingStrategy; import io.camunda.zeebe.spring.client.jobhandling.JobWorkerManager; import io.camunda.zeebe.spring.client.metrics.MetricsRecorder; @@ -40,6 +43,11 @@ public OutboundConnectorFactory outboundConnectorFactory() { OutboundConnectorDiscovery.loadConnectorConfigurations()); } + @Bean + public DocumentFactory documentFactory() { + return new DocumentFactoryImpl(InMemoryDocumentStore.INSTANCE); + } + @Bean public OutboundConnectorManager outboundConnectorManager( JobWorkerManager jobWorkerManager, @@ -47,6 +55,7 @@ public OutboundConnectorManager outboundConnectorManager( CommandExceptionHandlingStrategy commandExceptionHandlingStrategy, SecretProviderAggregator secretProviderAggregator, @Autowired(required = false) ValidationProvider validationProvider, + DocumentFactory documentFactory, ObjectMapper objectMapper, MetricsRecorder metricsRecorder) { return new OutboundConnectorManager( @@ -55,6 +64,7 @@ public OutboundConnectorManager outboundConnectorManager( commandExceptionHandlingStrategy, secretProviderAggregator, validationProvider, + documentFactory, objectMapper, metricsRecorder); } diff --git a/connector-runtime/connector-runtime-spring/src/main/java/io/camunda/connector/runtime/outbound/jobhandling/SpringConnectorJobHandler.java b/connector-runtime/connector-runtime-spring/src/main/java/io/camunda/connector/runtime/outbound/jobhandling/SpringConnectorJobHandler.java index dfaf794057..254913be1c 100644 --- a/connector-runtime/connector-runtime-spring/src/main/java/io/camunda/connector/runtime/outbound/jobhandling/SpringConnectorJobHandler.java +++ b/connector-runtime/connector-runtime-spring/src/main/java/io/camunda/connector/runtime/outbound/jobhandling/SpringConnectorJobHandler.java @@ -26,6 +26,7 @@ import io.camunda.connector.runtime.core.secret.SecretProviderAggregator; import io.camunda.connector.runtime.metrics.ConnectorMetrics; import io.camunda.connector.runtime.metrics.ConnectorMetrics.Outbound; +import io.camunda.document.factory.DocumentFactory; import io.camunda.zeebe.client.api.command.FinalCommandStep; import io.camunda.zeebe.client.api.response.ActivatedJob; import io.camunda.zeebe.client.api.worker.JobClient; @@ -52,10 +53,16 @@ public SpringConnectorJobHandler( CommandExceptionHandlingStrategy commandExceptionHandlingStrategy, SecretProviderAggregator secretProviderAggregator, ValidationProvider validationProvider, + DocumentFactory documentFactory, ObjectMapper objectMapper, OutboundConnectorFunction connectorFunction, OutboundConnectorConfiguration connectorConfiguration) { - super(connectorFunction, secretProviderAggregator, validationProvider, objectMapper); + super( + connectorFunction, + secretProviderAggregator, + validationProvider, + documentFactory, + objectMapper); this.metricsRecorder = metricsRecorder; this.commandExceptionHandlingStrategy = commandExceptionHandlingStrategy; this.connectorConfiguration = connectorConfiguration; diff --git a/connector-runtime/connector-runtime-spring/src/main/java/io/camunda/connector/runtime/outbound/lifecycle/OutboundConnectorManager.java b/connector-runtime/connector-runtime-spring/src/main/java/io/camunda/connector/runtime/outbound/lifecycle/OutboundConnectorManager.java index c2010d3cbe..202250caf7 100644 --- a/connector-runtime/connector-runtime-spring/src/main/java/io/camunda/connector/runtime/outbound/lifecycle/OutboundConnectorManager.java +++ b/connector-runtime/connector-runtime-spring/src/main/java/io/camunda/connector/runtime/outbound/lifecycle/OutboundConnectorManager.java @@ -23,6 +23,7 @@ import io.camunda.connector.runtime.core.outbound.OutboundConnectorFactory; import io.camunda.connector.runtime.core.secret.SecretProviderAggregator; import io.camunda.connector.runtime.outbound.jobhandling.SpringConnectorJobHandler; +import io.camunda.document.factory.DocumentFactory; import io.camunda.zeebe.client.ZeebeClient; import io.camunda.zeebe.client.api.worker.JobHandler; import io.camunda.zeebe.spring.client.annotation.value.ZeebeWorkerValue; @@ -46,6 +47,7 @@ public class OutboundConnectorManager { private final ValidationProvider validationProvider; private final ObjectMapper objectMapper; private final MetricsRecorder metricsRecorder; + private final DocumentFactory documentFactory; public OutboundConnectorManager( JobWorkerManager jobWorkerManager, @@ -53,6 +55,7 @@ public OutboundConnectorManager( CommandExceptionHandlingStrategy commandExceptionHandlingStrategy, SecretProviderAggregator secretProviderAggregator, ValidationProvider validationProvider, + DocumentFactory documentFactory, ObjectMapper objectMapper, MetricsRecorder metricsRecorder) { this.jobWorkerManager = jobWorkerManager; @@ -60,6 +63,7 @@ public OutboundConnectorManager( this.commandExceptionHandlingStrategy = commandExceptionHandlingStrategy; this.secretProviderAggregator = secretProviderAggregator; this.validationProvider = validationProvider; + this.documentFactory = documentFactory; this.objectMapper = objectMapper; this.metricsRecorder = metricsRecorder; } @@ -99,6 +103,7 @@ private void openWorkerForOutboundConnector( commandExceptionHandlingStrategy, secretProviderAggregator, validationProvider, + documentFactory, objectMapper, connectorFunction, connector); diff --git a/connector-sdk/core/pom.xml b/connector-sdk/core/pom.xml index 87cc0ea42f..8d54d351da 100644 --- a/connector-sdk/core/pom.xml +++ b/connector-sdk/core/pom.xml @@ -41,6 +41,11 @@ jackson-datatype-jdk8 provided + + io.camunda.connector + jackson-datatype-document + 8.6.0-SNAPSHOT + org.junit.jupiter diff --git a/connector-sdk/core/src/main/java/io/camunda/connector/api/inbound/InboundConnectorContext.java b/connector-sdk/core/src/main/java/io/camunda/connector/api/inbound/InboundConnectorContext.java index 42f5de48ec..8ed2fcbec4 100644 --- a/connector-sdk/core/src/main/java/io/camunda/connector/api/inbound/InboundConnectorContext.java +++ b/connector-sdk/core/src/main/java/io/camunda/connector/api/inbound/InboundConnectorContext.java @@ -16,6 +16,8 @@ */ package io.camunda.connector.api.inbound; +import io.camunda.document.Document; +import io.camunda.document.store.DocumentCreationRequest; import java.util.Map; /** @@ -54,7 +56,7 @@ public interface InboundConnectorContext { /** * Low-level properties access method. Allows to perform custom deserialization. For a simpler - * property access, consider using {@link #bindProperties(Class)} (Class)}. + * property access, consider using {@link #bindProperties(Class)}. * *

Note: this method doesn't perform validation or FEEl expression evaluation. Secret * replacement is performed using the {@link io.camunda.connector.api.secret.SecretProvider} @@ -112,4 +114,7 @@ public interface InboundConnectorContext { * implementation requires it. */ void log(Activity activity); + + /** Creates a new document in the Camunda Document Store. */ + Document createDocument(DocumentCreationRequest request); } diff --git a/connector-sdk/core/src/main/java/io/camunda/connector/api/json/ConnectorsObjectMapperSupplier.java b/connector-sdk/core/src/main/java/io/camunda/connector/api/json/ConnectorsObjectMapperSupplier.java index 1a35dab5b5..549b873101 100644 --- a/connector-sdk/core/src/main/java/io/camunda/connector/api/json/ConnectorsObjectMapperSupplier.java +++ b/connector-sdk/core/src/main/java/io/camunda/connector/api/json/ConnectorsObjectMapperSupplier.java @@ -23,16 +23,25 @@ import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import io.camunda.connector.document.annotation.jackson.JacksonModuleDocument; +import io.camunda.connector.document.annotation.jackson.JacksonModuleDocument.DocumentModuleSettings; import io.camunda.connector.feel.jackson.JacksonModuleFeelFunction; +import io.camunda.document.factory.DocumentFactoryImpl; +import io.camunda.document.store.InMemoryDocumentStore; /** Default ObjectMapper supplier to be used by the connector runtime. */ public class ConnectorsObjectMapperSupplier { - private ConnectorsObjectMapperSupplier() {} - public static ObjectMapper DEFAULT_MAPPER = JsonMapper.builder() - .addModules(new JacksonModuleFeelFunction(), new Jdk8Module(), new JavaTimeModule()) + .addModules( + new JacksonModuleFeelFunction(), + new JacksonModuleDocument( + new DocumentFactoryImpl(InMemoryDocumentStore.INSTANCE), + null, + DocumentModuleSettings.create()), + new Jdk8Module(), + new JavaTimeModule()) .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) @@ -40,6 +49,8 @@ private ConnectorsObjectMapperSupplier() {} .disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS) .build(); + private ConnectorsObjectMapperSupplier() {} + public static ObjectMapper getCopy() { return DEFAULT_MAPPER.copy(); } diff --git a/connector-sdk/core/src/main/java/io/camunda/connector/api/outbound/OutboundConnectorContext.java b/connector-sdk/core/src/main/java/io/camunda/connector/api/outbound/OutboundConnectorContext.java index e0767343dc..71010c5f4a 100644 --- a/connector-sdk/core/src/main/java/io/camunda/connector/api/outbound/OutboundConnectorContext.java +++ b/connector-sdk/core/src/main/java/io/camunda/connector/api/outbound/OutboundConnectorContext.java @@ -16,17 +16,16 @@ */ package io.camunda.connector.api.outbound; +import io.camunda.document.Document; +import io.camunda.document.store.DocumentCreationRequest; + /** * The context object provided to a connector function. The context allows to fetch information * injected by the environment runtime. */ public interface OutboundConnectorContext { - /** - * Context information about the activated job. - * - * @return - */ + /** Context information about the activated job. */ JobContext getJobContext(); /** @@ -47,4 +46,7 @@ public interface OutboundConnectorContext { * @return deserialized and validated variables with secrets replaced */ T bindVariables(Class cls); + + /** Creates a new document in the Camunda Document Store. */ + Document createDocument(DocumentCreationRequest request); } diff --git a/connector-sdk/document/pom.xml b/connector-sdk/document/pom.xml new file mode 100644 index 0000000000..d9dfa8869f --- /dev/null +++ b/connector-sdk/document/pom.xml @@ -0,0 +1,19 @@ + + 4.0.0 + + + io.camunda.connector + connector-sdk-parent + ../pom.xml + 8.6.0-SNAPSHOT + + + connector-document + Camunda Connector Document + connector-document + jar + + + + \ No newline at end of file diff --git a/connector-sdk/document/src/main/java/io/camunda/document/CamundaDocument.java b/connector-sdk/document/src/main/java/io/camunda/document/CamundaDocument.java new file mode 100644 index 0000000000..a513178d03 --- /dev/null +++ b/connector-sdk/document/src/main/java/io/camunda/document/CamundaDocument.java @@ -0,0 +1,68 @@ +/* + * 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.document; + +import io.camunda.document.reference.DocumentReference; +import io.camunda.document.reference.DocumentReference.CamundaDocumentReference; +import io.camunda.document.store.CamundaDocumentStore; +import java.io.InputStream; +import java.util.Base64; + +public class CamundaDocument implements Document { + + private final DocumentMetadata metadata; + private final CamundaDocumentReference reference; + private final CamundaDocumentStore documentStore; + + public CamundaDocument( + DocumentMetadata metadata, + CamundaDocumentReference reference, + CamundaDocumentStore documentStore) { + this.metadata = metadata; + this.reference = reference; + this.documentStore = documentStore; + } + + @Override + public DocumentMetadata metadata() { + return metadata; + } + + @Override + public String asBase64() { + return Base64.getEncoder().encodeToString(asByteArray()); + } + + @Override + public InputStream asInputStream() { + return documentStore.getDocumentContent(reference); + } + + @Override + public byte[] asByteArray() { + try { + return documentStore.getDocumentContent(reference).readAllBytes(); + } catch (Exception e) { + throw new RuntimeException("Failed to read document content", e); + } + } + + @Override + public DocumentReference reference() { + return reference; + } +} diff --git a/connector-sdk/document/src/main/java/io/camunda/document/Document.java b/connector-sdk/document/src/main/java/io/camunda/document/Document.java new file mode 100644 index 0000000000..28df9738ae --- /dev/null +++ b/connector-sdk/document/src/main/java/io/camunda/document/Document.java @@ -0,0 +1,41 @@ +/* + * 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.document; + +import io.camunda.document.reference.DocumentReference; +import java.io.InputStream; + +/** + * Represents a uniform document (file) object that can be passed between connectors and used in the + * FEEL engine. + */ +public interface Document { + + /** + * Domain-specific metadata that can be attached to the document. When a file is consumed by a + * connector as input, the metadata originates from the + */ + DocumentMetadata metadata(); + + String asBase64(); + + InputStream asInputStream(); + + byte[] asByteArray(); + + DocumentReference reference(); +} diff --git a/connector-sdk/document/src/main/java/io/camunda/document/DocumentMetadata.java b/connector-sdk/document/src/main/java/io/camunda/document/DocumentMetadata.java new file mode 100644 index 0000000000..8d67bb1683 --- /dev/null +++ b/connector-sdk/document/src/main/java/io/camunda/document/DocumentMetadata.java @@ -0,0 +1,52 @@ +/* + * 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.document; + +import java.util.Map; + +public class DocumentMetadata { + + public static final String CONTENT_TYPE = "contentType"; + public static final String FILE_NAME = "fileName"; + public static final String DESCRIPTION = "description"; + + private final Map keys; + + public DocumentMetadata(Map keys) { + this.keys = keys; + } + + public Map getKeys() { + return keys; + } + + public Object getKey(String key) { + return keys.get(key); + } + + public String getContentType() { + return (String) keys.get(CONTENT_TYPE); + } + + public String getFileName() { + return (String) keys.get(FILE_NAME); + } + + public String getDescription() { + return (String) keys.get(DESCRIPTION); + } +} diff --git a/connector-sdk/document/src/main/java/io/camunda/document/factory/DocumentFactory.java b/connector-sdk/document/src/main/java/io/camunda/document/factory/DocumentFactory.java new file mode 100644 index 0000000000..3462559a28 --- /dev/null +++ b/connector-sdk/document/src/main/java/io/camunda/document/factory/DocumentFactory.java @@ -0,0 +1,33 @@ +/* + * 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.document.factory; + +import io.camunda.document.Document; +import io.camunda.document.reference.DocumentReference; +import io.camunda.document.store.DocumentCreationRequest; + +public interface DocumentFactory { + + /** Given a document reference, create the Document object */ + Document resolve(DocumentReference reference); + + /** + * Upload a document to the underlying document store and parse the document reference into a + * Document object + */ + Document create(DocumentCreationRequest request); +} diff --git a/connector-sdk/document/src/main/java/io/camunda/document/factory/DocumentFactoryImpl.java b/connector-sdk/document/src/main/java/io/camunda/document/factory/DocumentFactoryImpl.java new file mode 100644 index 0000000000..21dc9237e5 --- /dev/null +++ b/connector-sdk/document/src/main/java/io/camunda/document/factory/DocumentFactoryImpl.java @@ -0,0 +1,56 @@ +/* + * 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.document.factory; + +import io.camunda.document.CamundaDocument; +import io.camunda.document.Document; +import io.camunda.document.reference.DocumentReference; +import io.camunda.document.reference.DocumentReference.CamundaDocumentReference; +import io.camunda.document.reference.DocumentReference.ExternalDocumentReference; +import io.camunda.document.store.CamundaDocumentStore; +import io.camunda.document.store.DocumentCreationRequest; + +public class DocumentFactoryImpl implements DocumentFactory { + + private final CamundaDocumentStore documentStore; + + public DocumentFactoryImpl(CamundaDocumentStore documentStore) { + this.documentStore = documentStore; + } + + @Override + public Document resolve(DocumentReference reference) { + if (reference == null) { + return null; + } + if (reference instanceof CamundaDocumentReference camundaDocumentReference) { + return new CamundaDocument( + camundaDocumentReference.metadata(), camundaDocumentReference, documentStore); + } + if (reference instanceof ExternalDocumentReference ignored) { + throw new IllegalArgumentException( + "External document references are not yet supported: " + reference.getClass()); + } + throw new IllegalArgumentException("Unknown document reference type: " + reference.getClass()); + } + + @Override + public Document create(DocumentCreationRequest request) { + var reference = documentStore.createDocument(request); + return resolve(reference); + } +} diff --git a/connector-sdk/document/src/main/java/io/camunda/document/operation/AggregatingOperationExecutor.java b/connector-sdk/document/src/main/java/io/camunda/document/operation/AggregatingOperationExecutor.java new file mode 100644 index 0000000000..9847f506c2 --- /dev/null +++ b/connector-sdk/document/src/main/java/io/camunda/document/operation/AggregatingOperationExecutor.java @@ -0,0 +1,34 @@ +/* + * 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.document.operation; + +import io.camunda.document.Document; + +public class AggregatingOperationExecutor implements DocumentOperationExecutor { + + public AggregatingOperationExecutor() {} + + @Override + public boolean matches(DocumentOperation operationReference) { + return true; + } + + @Override + public Object execute(DocumentOperation operationReference, Document document) { + return null; + } +} diff --git a/connector-sdk/document/src/main/java/io/camunda/document/operation/Base64OperationExecutor.java b/connector-sdk/document/src/main/java/io/camunda/document/operation/Base64OperationExecutor.java new file mode 100644 index 0000000000..31c18d4d9d --- /dev/null +++ b/connector-sdk/document/src/main/java/io/camunda/document/operation/Base64OperationExecutor.java @@ -0,0 +1,32 @@ +/* + * 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.document.operation; + +import io.camunda.document.Document; + +public class Base64OperationExecutor implements DocumentOperationExecutor { + + @Override + public boolean matches(DocumentOperation operationReference) { + return "base64".equalsIgnoreCase(operationReference.name()); + } + + @Override + public Object execute(DocumentOperation operationReference, Document document) { + return document.asBase64(); + } +} diff --git a/connector-sdk/document/src/main/java/io/camunda/document/operation/DocumentOperation.java b/connector-sdk/document/src/main/java/io/camunda/document/operation/DocumentOperation.java new file mode 100644 index 0000000000..24bf3c064f --- /dev/null +++ b/connector-sdk/document/src/main/java/io/camunda/document/operation/DocumentOperation.java @@ -0,0 +1,21 @@ +/* + * 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.document.operation; + +import java.util.Map; + +public record DocumentOperation(String name, Map params) {} diff --git a/connector-sdk/document/src/main/java/io/camunda/document/operation/DocumentOperationExecutor.java b/connector-sdk/document/src/main/java/io/camunda/document/operation/DocumentOperationExecutor.java new file mode 100644 index 0000000000..b07323654d --- /dev/null +++ b/connector-sdk/document/src/main/java/io/camunda/document/operation/DocumentOperationExecutor.java @@ -0,0 +1,26 @@ +/* + * 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.document.operation; + +import io.camunda.document.Document; + +public interface DocumentOperationExecutor { + + boolean matches(DocumentOperation operationReference); + + Object execute(DocumentOperation operationReference, Document document); +} diff --git a/connector-sdk/document/src/main/java/io/camunda/document/reference/CamundaDocumentReferenceImpl.java b/connector-sdk/document/src/main/java/io/camunda/document/reference/CamundaDocumentReferenceImpl.java new file mode 100644 index 0000000000..439945acf6 --- /dev/null +++ b/connector-sdk/document/src/main/java/io/camunda/document/reference/CamundaDocumentReferenceImpl.java @@ -0,0 +1,23 @@ +/* + * 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.document.reference; + +import io.camunda.document.DocumentMetadata; + +public record CamundaDocumentReferenceImpl( + String storeId, String documentId, DocumentMetadata metadata) + implements DocumentReference.CamundaDocumentReference {} diff --git a/connector-sdk/document/src/main/java/io/camunda/document/reference/DocumentReference.java b/connector-sdk/document/src/main/java/io/camunda/document/reference/DocumentReference.java new file mode 100644 index 0000000000..3c384ff698 --- /dev/null +++ b/connector-sdk/document/src/main/java/io/camunda/document/reference/DocumentReference.java @@ -0,0 +1,34 @@ +/* + * 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.document.reference; + +import io.camunda.document.DocumentMetadata; + +public interface DocumentReference { + + interface CamundaDocumentReference extends DocumentReference { + String storeId(); + + String documentId(); + + DocumentMetadata metadata(); + } + + interface ExternalDocumentReference extends DocumentReference { + String url(); + } +} diff --git a/connector-sdk/document/src/main/java/io/camunda/document/store/CamundaDocumentStore.java b/connector-sdk/document/src/main/java/io/camunda/document/store/CamundaDocumentStore.java new file mode 100644 index 0000000000..ab36db890a --- /dev/null +++ b/connector-sdk/document/src/main/java/io/camunda/document/store/CamundaDocumentStore.java @@ -0,0 +1,29 @@ +/* + * 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.document.store; + +import io.camunda.document.reference.DocumentReference.CamundaDocumentReference; +import java.io.InputStream; + +public interface CamundaDocumentStore { + + CamundaDocumentReference createDocument(DocumentCreationRequest request); + + InputStream getDocumentContent(CamundaDocumentReference reference); + + void deleteDocument(CamundaDocumentReference reference); +} diff --git a/connector-sdk/document/src/main/java/io/camunda/document/store/DocumentCreationRequest.java b/connector-sdk/document/src/main/java/io/camunda/document/store/DocumentCreationRequest.java new file mode 100644 index 0000000000..a2109cf500 --- /dev/null +++ b/connector-sdk/document/src/main/java/io/camunda/document/store/DocumentCreationRequest.java @@ -0,0 +1,77 @@ +/* + * 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.document.store; + +import io.camunda.document.DocumentMetadata; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.Map; + +public record DocumentCreationRequest( + DocumentMetadata metadata, InputStream content, String documentId, String storeId) { + + public static BuilderStepMetadata from(InputStream content) { + return new BuilderStepMetadata(content); + } + + public static BuilderStepMetadata from(byte[] content) { + return new BuilderStepMetadata(new ByteArrayInputStream(content)); + } + + public static class BuilderStepMetadata { + private final InputStream content; + + public BuilderStepMetadata(InputStream content) { + this.content = content; + } + + public BuilderFinalStep metadata(DocumentMetadata metadata) { + return new BuilderFinalStep(metadata, content); + } + + public BuilderFinalStep metadata(Map metadata) { + return new BuilderFinalStep(new DocumentMetadata(metadata), content); + } + } + + public static class BuilderFinalStep { + private final DocumentMetadata metadata; + private final InputStream content; + private String documentId; + private String storeId; + + public BuilderFinalStep(DocumentMetadata metadata, InputStream content) { + this.metadata = metadata; + this.content = content; + } + + public BuilderFinalStep documentId(String documentId) { + this.documentId = documentId; + return this; + } + + public BuilderFinalStep storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public DocumentCreationRequest build() { + return new DocumentCreationRequest(metadata, content, documentId, storeId); + } + } +} diff --git a/connector-sdk/document/src/main/java/io/camunda/document/store/InMemoryDocumentStore.java b/connector-sdk/document/src/main/java/io/camunda/document/store/InMemoryDocumentStore.java new file mode 100644 index 0000000000..d0b48f8a01 --- /dev/null +++ b/connector-sdk/document/src/main/java/io/camunda/document/store/InMemoryDocumentStore.java @@ -0,0 +1,71 @@ +/* + * 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.document.store; + +import io.camunda.document.DocumentMetadata; +import io.camunda.document.reference.CamundaDocumentReferenceImpl; +import io.camunda.document.reference.DocumentReference.CamundaDocumentReference; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** Use this document store to store documents in memory. This is useful for testing purposes. */ +public class InMemoryDocumentStore implements CamundaDocumentStore { + + public static final String STORE_ID = "in-memory"; + + public static InMemoryDocumentStore INSTANCE = new InMemoryDocumentStore(); + + private final Map documents = new HashMap<>(); + + private InMemoryDocumentStore() {} + + @Override + public CamundaDocumentReference createDocument(DocumentCreationRequest request) { + final String id = + request.documentId() != null ? request.documentId() : UUID.randomUUID().toString(); + final DocumentMetadata metadata = request.metadata(); + final byte[] content; + try (InputStream contentStream = request.content()) { + content = contentStream.readAllBytes(); + } catch (Exception e) { + throw new RuntimeException("Failed to read document content", e); + } + documents.put(id, content); + return new CamundaDocumentReferenceImpl(STORE_ID, id, metadata); + } + + @Override + public InputStream getDocumentContent(CamundaDocumentReference reference) { + var content = documents.get(reference.documentId()); + if (content == null) { + throw new RuntimeException("Document not found: " + reference.documentId()); + } + return new ByteArrayInputStream(content); + } + + @Override + public void deleteDocument(CamundaDocumentReference reference) { + documents.remove(reference.documentId()); + } + + public void clear() { + documents.clear(); + } +} diff --git a/connector-sdk/jackson-datatype-document/pom.xml b/connector-sdk/jackson-datatype-document/pom.xml new file mode 100644 index 0000000000..86d91be000 --- /dev/null +++ b/connector-sdk/jackson-datatype-document/pom.xml @@ -0,0 +1,55 @@ + + 4.0.0 + + + io.camunda.connector + connector-sdk-parent + ../pom.xml + 8.6.0-SNAPSHOT + + + jackson-datatype-document + Jackson module to handle Connector SDK file objects + + + + io.camunda.connector + connector-document + + + com.fasterxml.jackson.core + jackson-databind + + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + test + + + org.skyscreamer + jsonassert + test + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + test + + + diff --git a/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/DocumentOperationResult.java b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/DocumentOperationResult.java new file mode 100644 index 0000000000..16bc3f3997 --- /dev/null +++ b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/DocumentOperationResult.java @@ -0,0 +1,21 @@ +/* + * 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.document.annotation.jackson; + +import java.util.function.Supplier; + +public interface DocumentOperationResult extends Supplier {} diff --git a/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/DocumentReferenceModel.java b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/DocumentReferenceModel.java new file mode 100644 index 0000000000..8a211ec51b --- /dev/null +++ b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/DocumentReferenceModel.java @@ -0,0 +1,84 @@ +/* + * 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.document.annotation.jackson; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeInfo.As; +import io.camunda.connector.document.annotation.jackson.DocumentReferenceModel.CamundaDocumentReferenceModel; +import io.camunda.connector.document.annotation.jackson.DocumentReferenceModel.ExternalDocumentReferenceModel; +import io.camunda.document.DocumentMetadata; +import io.camunda.document.operation.DocumentOperation; +import io.camunda.document.reference.DocumentReference; +import java.util.Map; +import java.util.Optional; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + property = DocumentReferenceModel.DISCRIMINATOR_KEY, + visible = true, + include = As.EXISTING_PROPERTY) +@JsonSubTypes({ + @JsonSubTypes.Type(value = CamundaDocumentReferenceModel.class, name = "camunda"), + @JsonSubTypes.Type(value = ExternalDocumentReferenceModel.class, name = "external") +}) +@JsonIgnoreProperties(ignoreUnknown = true) +public sealed interface DocumentReferenceModel extends DocumentReference { + + String DISCRIMINATOR_KEY = "documentType"; + + /** + * Document references may have operations associated with them. Operation indicates that the + * document should not be used as is, but should be transformed or processed in some way. This + * processing must take place in the context of the connector. + */ + @JsonInclude(Include.NON_EMPTY) + Optional operation(); + + record CamundaDocumentReferenceModel( + String storeId, + String documentId, + @JsonProperty("metadata") Map rawMetadata, + Optional operation) + implements DocumentReferenceModel, CamundaDocumentReference { + + @JsonProperty(DISCRIMINATOR_KEY) + private String documentType() { + return "camunda"; + } + + @Override + @JsonIgnore + public DocumentMetadata metadata() { + return new DocumentMetadata(rawMetadata); + } + } + + record ExternalDocumentReferenceModel(String url, Optional operation) + implements DocumentReferenceModel, ExternalDocumentReference { + + @JsonProperty(DISCRIMINATOR_KEY) + private String documentType() { + return "external"; + } + } +} diff --git a/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/JacksonModuleDocument.java b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/JacksonModuleDocument.java new file mode 100644 index 0000000000..c9d40868cf --- /dev/null +++ b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/JacksonModuleDocument.java @@ -0,0 +1,126 @@ +/* + * 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.document.annotation.jackson; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.module.SimpleModule; +import io.camunda.connector.document.annotation.jackson.deserializer.ByteArrayDocumentDeserializer; +import io.camunda.connector.document.annotation.jackson.deserializer.DocumentDeserializer; +import io.camunda.connector.document.annotation.jackson.deserializer.DocumentOperationResultDeserializer; +import io.camunda.connector.document.annotation.jackson.deserializer.InputStreamDocumentDeserializer; +import io.camunda.connector.document.annotation.jackson.deserializer.ObjectDocumentDeserializer; +import io.camunda.connector.document.annotation.jackson.deserializer.StringDocumentDeserializer; +import io.camunda.connector.document.annotation.jackson.serializer.DocumentSerializer; +import io.camunda.document.Document; +import io.camunda.document.factory.DocumentFactory; +import io.camunda.document.operation.DocumentOperationExecutor; +import java.io.InputStream; + +public class JacksonModuleDocument extends SimpleModule { + + private final DocumentFactory documentFactory; + private final DocumentOperationExecutor operationExecutor; + private final DocumentModuleSettings settings; + + public JacksonModuleDocument( + DocumentFactory documentFactory, + DocumentOperationExecutor operationExecutor, + DocumentModuleSettings settings) { + this.documentFactory = documentFactory; + this.operationExecutor = operationExecutor; + this.settings = settings; + } + + public JacksonModuleDocument( + DocumentFactory documentFactory, DocumentOperationExecutor operationExecutor) { + this(documentFactory, operationExecutor, DocumentModuleSettings.create()); + } + + @Override + public String getModuleName() { + return "JacksonModuleDocument"; + } + + @Override + public Version version() { + // TODO: get version from pom.xml + return new Version(0, 1, 0, null, "io.camunda", "jackson-datatype-document"); + } + + @Override + public void setupModule(SetupContext context) { + addDeserializer(Document.class, new DocumentDeserializer(operationExecutor, documentFactory)); + addDeserializer( + DocumentOperationResult.class, + new DocumentOperationResultDeserializer(operationExecutor, documentFactory)); + addDeserializer( + byte[].class, new ByteArrayDocumentDeserializer(operationExecutor, documentFactory)); + addDeserializer( + InputStream.class, new InputStreamDocumentDeserializer(operationExecutor, documentFactory)); + if (settings.enableObject) { + addDeserializer( + Object.class, + new ObjectDocumentDeserializer(operationExecutor, documentFactory, settings.lazy)); + } + if (settings.enableString) { + addDeserializer( + String.class, new StringDocumentDeserializer(operationExecutor, documentFactory)); + } + addSerializer(Document.class, new DocumentSerializer(operationExecutor)); + super.setupModule(context); + } + + public static class DocumentModuleSettings { + + private boolean lazy = true; + private boolean enableObject = true; + private boolean enableString = true; + + private DocumentModuleSettings() {} + + public static DocumentModuleSettings create() { + return new DocumentModuleSettings(); + } + + /** + * Enable lazy operations for document deserialization. + * + *

When enabled, given that the connector consumes a document as a generic {@link Object} + * type, and an operation is present in the document reference, the operation is not executed in + * the deserialization phase. Instead, the operation is executed during serialization using the + * {@link DocumentSerializer}. + * + *

Disable lazy operations if your connector doesn't use the document module for + * serialization (or doesn't use Jackson at all). + * + *

This takes no effect if {@link #enableObject(boolean)} is disabled. + */ + public void lazyOperations(boolean lazy) { + this.lazy = lazy; + } + + /** Enable deserialization of document references into objects. */ + public void enableObject(boolean enable) { + this.enableObject = enable; + } + + /** Enable deserialization of document references into strings. */ + public void enableString(boolean enable) { + this.enableString = enable; + } + } +} diff --git a/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/deserializer/ByteArrayDocumentDeserializer.java b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/deserializer/ByteArrayDocumentDeserializer.java new file mode 100644 index 0000000000..06451acf6a --- /dev/null +++ b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/deserializer/ByteArrayDocumentDeserializer.java @@ -0,0 +1,53 @@ +/* + * 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.document.annotation.jackson.deserializer; + +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.PrimitiveArrayDeserializers; +import io.camunda.connector.document.annotation.jackson.DocumentReferenceModel; +import io.camunda.document.factory.DocumentFactory; +import io.camunda.document.operation.DocumentOperationExecutor; +import java.io.IOException; + +public class ByteArrayDocumentDeserializer extends DocumentDeserializerBase { + + private final JsonDeserializer fallbackDeserializer = + PrimitiveArrayDeserializers.forType(byte.class); + + public ByteArrayDocumentDeserializer( + DocumentOperationExecutor operationExecutor, DocumentFactory documentFactory) { + super(operationExecutor, documentFactory); + } + + @Override + public byte[] deserializeDocumentReference( + DocumentReferenceModel reference, DeserializationContext ctx) { + + ensureNoOperation(reference); + var document = createDocument(reference); + return document.asByteArray(); + } + + @Override + public byte[] fallback(JsonNode node, DeserializationContext ctx) throws IOException { + var parser = node.traverse(ctx.getParser().getCodec()); + parser.nextToken(); + return (byte[]) fallbackDeserializer.deserialize(parser, ctx); + } +} diff --git a/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/deserializer/DocumentDeserializer.java b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/deserializer/DocumentDeserializer.java new file mode 100644 index 0000000000..94e4302909 --- /dev/null +++ b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/deserializer/DocumentDeserializer.java @@ -0,0 +1,50 @@ +/* + * 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.document.annotation.jackson.deserializer; + +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import io.camunda.connector.document.annotation.jackson.DocumentReferenceModel; +import io.camunda.document.Document; +import io.camunda.document.factory.DocumentFactory; +import io.camunda.document.operation.DocumentOperationExecutor; +import java.io.IOException; + +public class DocumentDeserializer extends DocumentDeserializerBase { + + public DocumentDeserializer( + DocumentOperationExecutor operationExecutor, DocumentFactory documentFactory) { + super(operationExecutor, documentFactory); + } + + @Override + public Document deserializeDocumentReference( + DocumentReferenceModel reference, DeserializationContext ctx) { + + ensureNoOperation(reference); + return createDocument(reference); + } + + @Override + public Document fallback(JsonNode node, DeserializationContext ctx) throws IOException { + var fieldName = ctx.getParser().currentName(); + throw new IllegalArgumentException( + fieldName + + ": unsupported document format. Expected a document reference, got: " + + fieldName); + } +} diff --git a/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/deserializer/DocumentDeserializerBase.java b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/deserializer/DocumentDeserializerBase.java new file mode 100644 index 0000000000..9bff270900 --- /dev/null +++ b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/deserializer/DocumentDeserializerBase.java @@ -0,0 +1,96 @@ +/* + * 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.document.annotation.jackson.deserializer; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import io.camunda.connector.document.annotation.jackson.DocumentOperationResult; +import io.camunda.connector.document.annotation.jackson.DocumentReferenceModel; +import io.camunda.document.Document; +import io.camunda.document.factory.DocumentFactory; +import io.camunda.document.operation.DocumentOperation; +import io.camunda.document.operation.DocumentOperationExecutor; +import java.io.IOException; + +public abstract class DocumentDeserializerBase extends JsonDeserializer { + + protected final DocumentOperationExecutor operationExecutor; + protected final DocumentFactory documentFactory; + + public DocumentDeserializerBase( + DocumentOperationExecutor operationExecutor, DocumentFactory documentFactory) { + this.operationExecutor = operationExecutor; + this.documentFactory = documentFactory; + } + + @Override + public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + + JsonNode node = p.readValueAsTree(); + if (node == null || node.isNull()) { + return null; + } + if (isDocumentReference(node)) { + var reference = toReference(node, ctxt); + return deserializeDocumentReference(reference, ctxt); + } + return fallback(node, ctxt); + } + + /** Will be invoked when the deserializable data is a document reference. */ + public abstract T deserializeDocumentReference( + DocumentReferenceModel reference, DeserializationContext ctx) throws IOException; + + /** + * Will be invoked when the deserializable data is not a document reference. Deserializers should + * implement this method to provide a fallback behavior. + */ + public abstract T fallback(JsonNode node, DeserializationContext ctx) throws IOException; + + protected boolean isDocumentReference(JsonNode node) { + return node.has(DocumentReferenceModel.DISCRIMINATOR_KEY); + } + + protected DocumentReferenceModel toReference(JsonNode node, DeserializationContext ctx) + throws IOException { + + if (!isDocumentReference(node)) { + throw new IllegalArgumentException( + "Unsupported document format. Expected a document reference, got: " + node); + } + return ctx.readTreeAsValue(node, DocumentReferenceModel.class); + } + + protected void ensureNoOperation(DocumentReferenceModel reference) { + if (reference.operation().isPresent()) { + throw new IllegalArgumentException( + "Unsupported document format. Expected a document reference without operation, got: " + + reference); + } + } + + protected Document createDocument(DocumentReferenceModel reference) { + return documentFactory.resolve(reference); + } + + protected DocumentOperationResult deserializeOperation( + DocumentReferenceModel reference, DocumentOperation operation) { + return () -> operationExecutor.execute(operation, createDocument(reference)); + } +} diff --git a/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/deserializer/DocumentOperationResultDeserializer.java b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/deserializer/DocumentOperationResultDeserializer.java new file mode 100644 index 0000000000..13f3bd105f --- /dev/null +++ b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/deserializer/DocumentOperationResultDeserializer.java @@ -0,0 +1,91 @@ +/* + * 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.document.annotation.jackson.deserializer; + +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; +import io.camunda.connector.document.annotation.jackson.DocumentOperationResult; +import io.camunda.connector.document.annotation.jackson.DocumentReferenceModel; +import io.camunda.document.factory.DocumentFactory; +import io.camunda.document.operation.DocumentOperationExecutor; + +public class DocumentOperationResultDeserializer + extends DocumentDeserializerBase> implements ContextualDeserializer { + + private final JavaType valueType; + + public DocumentOperationResultDeserializer( + DocumentOperationExecutor operationExecutor, DocumentFactory documentFactory) { + super(operationExecutor, documentFactory); + this.valueType = null; + } + + public DocumentOperationResultDeserializer( + DocumentOperationExecutor operationExecutor, + DocumentFactory documentFactory, + JavaType valueType) { + super(operationExecutor, documentFactory); + this.valueType = valueType; + } + + @Override + public DocumentOperationResult deserializeDocumentReference( + DocumentReferenceModel reference, DeserializationContext ctx) { + var operation = + reference + .operation() + .orElseThrow( + () -> new IllegalArgumentException("Document reference must contain an operation")); + var resultSupplier = deserializeOperation(reference, operation); + + return () -> { + var result = resultSupplier.get(); + if (valueType == null) { + return result; + } + if (valueType.getContentType().isTypeOrSubTypeOf(result.getClass())) { + return result; + } else { + throw new IllegalArgumentException( + "Unexpected operation result type: " + + result.getClass() + + " while executing operation " + + operation.name() + + ". Expected " + + valueType.getContentType() + + ", but got " + + result.getClass()); + } + }; + } + + @Override + public DocumentOperationResult fallback(JsonNode node, DeserializationContext ctx) { + throw new IllegalArgumentException( + "Unsupported document format. Expected a document reference, got: " + node); + } + + @Override + public JsonDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) { + var valueType = property.getType(); + return new DocumentOperationResultDeserializer(operationExecutor, documentFactory, valueType); + } +} diff --git a/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/deserializer/InputStreamDocumentDeserializer.java b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/deserializer/InputStreamDocumentDeserializer.java new file mode 100644 index 0000000000..e339f1d057 --- /dev/null +++ b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/deserializer/InputStreamDocumentDeserializer.java @@ -0,0 +1,46 @@ +/* + * 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.document.annotation.jackson.deserializer; + +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import io.camunda.connector.document.annotation.jackson.DocumentReferenceModel; +import io.camunda.document.factory.DocumentFactory; +import io.camunda.document.operation.DocumentOperationExecutor; +import java.io.InputStream; + +public class InputStreamDocumentDeserializer extends DocumentDeserializerBase { + + public InputStreamDocumentDeserializer( + DocumentOperationExecutor operationExecutor, DocumentFactory documentFactory) { + super(operationExecutor, documentFactory); + } + + @Override + public InputStream deserializeDocumentReference( + DocumentReferenceModel reference, DeserializationContext ctx) { + ensureNoOperation(reference); + var document = createDocument(reference); + return document.asInputStream(); + } + + @Override + public InputStream fallback(JsonNode node, DeserializationContext ctx) { + throw new IllegalArgumentException( + "unsupported document format. Expected a document reference, got: " + node); + } +} diff --git a/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/deserializer/ObjectDocumentDeserializer.java b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/deserializer/ObjectDocumentDeserializer.java new file mode 100644 index 0000000000..9a8712f85c --- /dev/null +++ b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/deserializer/ObjectDocumentDeserializer.java @@ -0,0 +1,87 @@ +/* + * 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.document.annotation.jackson.deserializer; + +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.UntypedObjectDeserializer; +import io.camunda.connector.document.annotation.jackson.DocumentReferenceModel; +import io.camunda.document.factory.DocumentFactory; +import io.camunda.document.operation.DocumentOperationExecutor; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; + +public class ObjectDocumentDeserializer extends DocumentDeserializerBase { + + private final UntypedObjectDeserializer fallbackDeserializer = + new UntypedObjectDeserializer(null, null); + private final boolean lazy; + + public ObjectDocumentDeserializer( + DocumentOperationExecutor operationExecutor, DocumentFactory documentFactory, boolean lazy) { + super(operationExecutor, documentFactory); + this.lazy = lazy; + } + + @Override + public Object deserializeDocumentReference( + DocumentReferenceModel reference, DeserializationContext ctx) throws IOException { + + if (reference.operation().isPresent()) { + var operationResultSupplier = deserializeOperation(reference, reference.operation().get()); + if (lazy) { + return operationResultSupplier; + } + // TODO: check output type + return operationResultSupplier.get(); + } + // if no operation, return the document + return createDocument(reference); + } + + @Override + public Object fallback(JsonNode node, DeserializationContext ctx) throws IOException { + if (node.isObject()) { + var fields = node.fields(); + var map = new LinkedHashMap(); + while (fields.hasNext()) { + var field = fields.next(); + var parser = field.getValue().traverse(); + parser.setCodec(ctx.getParser().getCodec()); + // invoke the deserializer for the field + map.put(field.getKey(), ctx.readValue(parser, Object.class)); + } + return map; + } + + if (node.isArray()) { + var list = new ArrayList<>(); + for (int i = 0; i < node.size(); i++) { + var parser = node.get(i).traverse(); + parser.setCodec(ctx.getParser().getCodec()); + // invoke the deserializer for the element + list.add(ctx.readValue(parser, Object.class)); + } + return list; + } + + var parser = node.traverse(ctx.getParser().getCodec()); + parser.nextToken(); + return fallbackDeserializer.deserialize(parser, ctx); + } +} diff --git a/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/deserializer/StringDocumentDeserializer.java b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/deserializer/StringDocumentDeserializer.java new file mode 100644 index 0000000000..00342db119 --- /dev/null +++ b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/deserializer/StringDocumentDeserializer.java @@ -0,0 +1,61 @@ +/* + * 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.document.annotation.jackson.deserializer; + +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StringDeserializer; +import io.camunda.connector.document.annotation.jackson.DocumentReferenceModel; +import io.camunda.document.factory.DocumentFactory; +import io.camunda.document.operation.DocumentOperationExecutor; +import java.io.IOException; + +public class StringDocumentDeserializer extends DocumentDeserializerBase { + + private final StringDeserializer fallbackDeserializer = new StringDeserializer(); + + public StringDocumentDeserializer( + DocumentOperationExecutor operationExecutor, DocumentFactory documentFactory) { + super(operationExecutor, documentFactory); + } + + @Override + public String deserializeDocumentReference( + DocumentReferenceModel reference, DeserializationContext ctx) { + + if (reference.operation().isPresent()) { + var operationResultSupplier = deserializeOperation(reference, reference.operation().get()); + var result = operationResultSupplier.get(); + if (result instanceof String) { + return (String) result; + } else { + throw new IllegalArgumentException( + "Unexpected operation result type: " + result.getClass() + ". Expected String"); + } + } + // if no operation, return base64 encoded content + var document = createDocument(reference); + return document.asBase64(); + } + + @Override + public String fallback(JsonNode node, DeserializationContext ctx) throws IOException { + var parser = node.traverse(ctx.getParser().getCodec()); + parser.nextToken(); + return fallbackDeserializer.deserialize(parser, ctx); + } +} diff --git a/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/serializer/DocumentOperationResultSerializer.java b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/serializer/DocumentOperationResultSerializer.java new file mode 100644 index 0000000000..4a37ea7e58 --- /dev/null +++ b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/serializer/DocumentOperationResultSerializer.java @@ -0,0 +1,54 @@ +/* + * 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.document.annotation.jackson.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import io.camunda.connector.document.annotation.jackson.DocumentOperationResult; +import java.io.IOException; + +public class DocumentOperationResultSerializer extends JsonSerializer> { + + @Override + public void serialize( + DocumentOperationResult value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + var result = value.get(); + if (result == null) { + gen.writeNull(); + return; + } + if (result instanceof byte[]) { + gen.writeBinary((byte[]) result); + return; + } + if (result instanceof String) { + gen.writeString((String) result); + return; + } + if (result instanceof Boolean) { + gen.writeBoolean((Boolean) result); + return; + } + if (result instanceof Number) { + gen.writeNumber(result.toString()); + return; + } + gen.writeObject(result); + } +} diff --git a/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/serializer/DocumentSerializer.java b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/serializer/DocumentSerializer.java new file mode 100644 index 0000000000..4a13a6f00e --- /dev/null +++ b/connector-sdk/jackson-datatype-document/src/main/java/io/camunda/connector/document/annotation/jackson/serializer/DocumentSerializer.java @@ -0,0 +1,59 @@ +/* + * 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.document.annotation.jackson.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import io.camunda.connector.document.annotation.jackson.DocumentReferenceModel.CamundaDocumentReferenceModel; +import io.camunda.document.Document; +import io.camunda.document.operation.DocumentOperationExecutor; +import io.camunda.document.reference.DocumentReference.CamundaDocumentReference; +import java.io.IOException; +import java.util.Optional; + +public class DocumentSerializer extends JsonSerializer { + + private final DocumentOperationExecutor operationExecutor; + + public DocumentSerializer(DocumentOperationExecutor operationExecutor) { + this.operationExecutor = operationExecutor; + } + + @Override + public void serialize( + Document document, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException { + + var reference = document.reference(); + if (!(reference instanceof CamundaDocumentReference camundaReference)) { + throw new IllegalArgumentException("Unsupported document reference type: " + reference); + } + final CamundaDocumentReferenceModel model; + if (camundaReference instanceof CamundaDocumentReferenceModel camundaModel) { + model = camundaModel; + } else { + model = + new CamundaDocumentReferenceModel( + camundaReference.storeId(), + camundaReference.documentId(), + camundaReference.metadata().getKeys(), + Optional.empty()); + } + jsonGenerator.writeObject(model); + } +} diff --git a/connector-sdk/jackson-datatype-document/src/test/java/io/camunda/connector/document/jackson/DocumentDeserializationTest.java b/connector-sdk/jackson-datatype-document/src/test/java/io/camunda/connector/document/jackson/DocumentDeserializationTest.java new file mode 100644 index 0000000000..a440bd53f2 --- /dev/null +++ b/connector-sdk/jackson-datatype-document/src/test/java/io/camunda/connector/document/jackson/DocumentDeserializationTest.java @@ -0,0 +1,143 @@ +/* + * 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.document.jackson; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import io.camunda.connector.document.annotation.jackson.DocumentReferenceModel; +import io.camunda.connector.document.annotation.jackson.DocumentReferenceModel.CamundaDocumentReferenceModel; +import io.camunda.connector.document.annotation.jackson.JacksonModuleDocument; +import io.camunda.document.Document; +import io.camunda.document.factory.DocumentFactory; +import io.camunda.document.operation.DocumentOperationExecutor; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class DocumentDeserializationTest { + + @Mock private DocumentFactory factory; + @Mock private DocumentOperationExecutor operationExecutor; + + private ObjectMapper objectMapper; + + @BeforeEach + public void initialize() { + objectMapper = + new ObjectMapper() + .registerModule(new JacksonModuleDocument(factory, operationExecutor)) + .registerModule(new Jdk8Module()); + } + + @Test + void targetTypeDocument() { + var ref = createDocumentMock("Hello World"); + var payload = Map.of("document", ref); + var result = objectMapper.convertValue(payload, TargetTypeDocument.class); + assertEquals(ref, result.document.reference()); + } + + @Test + void targetTypeByteArray() { + var contentString = "Hello World"; + var ref = createDocumentMock(contentString); + var payload = Map.of("document", ref); + var result = objectMapper.convertValue(payload, TargetTypeByteArray.class); + assertEquals(contentString, new String(result.document)); + } + + @Test + void targetTypeInputStream() throws IOException { + var contentString = "Hello World"; + var ref = createDocumentMock(contentString); + var payload = Map.of("document", ref); + var result = objectMapper.convertValue(payload, TargetTypeInputStream.class); + assertEquals(contentString, new String(result.document.readAllBytes())); + } + + @Test + void targetTypeObject() { + var ref = createDocumentMock("Hello World"); + var payload = Map.of("document", ref); + var result = objectMapper.convertValue(payload, TargetTypeObject.class); + assertInstanceOf(Document.class, result.document); + assertEquals(ref, ((Document) result.document).reference()); + } + + @Test + @SuppressWarnings("unchecked") + void targetTypeObject_NestedObject() { + var ref = createDocumentMock("Hello World"); + var payload = Map.of("document", Map.of("document", ref)); + var result = objectMapper.convertValue(payload, TargetTypeObject.class); + assertInstanceOf(Map.class, result.document); + var nested = (Map) result.document; + assertInstanceOf(Document.class, nested.get("document")); + assertEquals(ref, ((Document) nested.get("document")).reference()); + } + + @Test + @SuppressWarnings("unchecked") + void targetTypeObject_Array() { + var ref = createDocumentMock("Hello World"); + var payload = Map.of("document", List.of(ref)); + var result = objectMapper.convertValue(payload, TargetTypeObject.class); + Assertions.assertInstanceOf(List.class, result.document); + var list = (List) result.document; + assertInstanceOf(Document.class, list.get(0)); + assertEquals(ref, ((Document) list.get(0)).reference()); + } + + private DocumentReferenceModel createDocumentMock(String content) { + var ref = + new CamundaDocumentReferenceModel( + "default", UUID.randomUUID().toString(), Map.of(), Optional.empty()); + Document document = mock(Document.class); + lenient().when(document.asByteArray()).thenReturn(content.getBytes()); + lenient().when(document.reference()).thenReturn(ref); + lenient() + .when(document.asInputStream()) + .thenReturn(new ByteArrayInputStream(content.getBytes())); + when((factory.resolve(ref))).thenReturn(document); + return ref; + } + + public record TargetTypeDocument(Document document) {} + + public record TargetTypeByteArray(byte[] document) {} + + public record TargetTypeInputStream(InputStream document) {} + + public record TargetTypeObject(Object document) {} +} diff --git a/connector-sdk/jackson-datatype-document/src/test/java/io/camunda/connector/document/jackson/DocumentSerializationTest.java b/connector-sdk/jackson-datatype-document/src/test/java/io/camunda/connector/document/jackson/DocumentSerializationTest.java new file mode 100644 index 0000000000..8bcaf3fa3e --- /dev/null +++ b/connector-sdk/jackson-datatype-document/src/test/java/io/camunda/connector/document/jackson/DocumentSerializationTest.java @@ -0,0 +1,92 @@ +/* + * 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.document.jackson; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import io.camunda.connector.document.annotation.jackson.DocumentReferenceModel.CamundaDocumentReferenceModel; +import io.camunda.connector.document.annotation.jackson.JacksonModuleDocument; +import io.camunda.document.Document; +import io.camunda.document.DocumentMetadata; +import io.camunda.document.factory.DocumentFactory; +import io.camunda.document.operation.DocumentOperationExecutor; +import io.camunda.document.reference.CamundaDocumentReferenceImpl; +import java.util.Map; +import java.util.Optional; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.skyscreamer.jsonassert.JSONAssert; + +public class DocumentSerializationTest { + + @Mock private DocumentFactory factory; + @Mock private DocumentOperationExecutor operationExecutor; + + private final ObjectMapper objectMapper = + new ObjectMapper() + .registerModule(new JacksonModuleDocument(factory, operationExecutor)) + .registerModule(new Jdk8Module()); + + @Test + void sourceTypeDocument_jacksonInternalModel() throws JsonProcessingException, JSONException { + var ref = new CamundaDocumentReferenceModel("test", "test", Map.of(), Optional.empty()); + var document = mock(Document.class); + when(document.reference()).thenReturn(ref); + var source = new SourceTypeDocument(document); + var result = objectMapper.writeValueAsString(source); + var expectedResult = + """ + { + "document": { + "documentType": "camunda", + "storeId": "test", + "documentId": "test", + "metadata": {} + } + } + """; + JSONAssert.assertEquals(expectedResult, result, true); + } + + @Test + void sourceTypeDocument_connectorSdkModel() throws JsonProcessingException, JSONException { + var ref = new CamundaDocumentReferenceImpl("test", "test", new DocumentMetadata(Map.of())); + var document = mock(Document.class); + when(document.reference()).thenReturn(ref); + var source = new SourceTypeDocument(document); + var result = objectMapper.writeValueAsString(source); + var expectedResult = + """ + { + "document": { + "documentType": "camunda", + "storeId": "test", + "documentId": "test", + "metadata": {} + } + } + """; + JSONAssert.assertEquals(expectedResult, result, true); + } + + record SourceTypeDocument(Document document) {} +} diff --git a/connector-sdk/pom.xml b/connector-sdk/pom.xml index d1e7e513f1..5c2b2de326 100644 --- a/connector-sdk/pom.xml +++ b/connector-sdk/pom.xml @@ -20,8 +20,10 @@ core validation test + document feel-wrapper jackson-datatype-feel + jackson-datatype-document diff --git a/connector-sdk/test/src/main/java/io/camunda/connector/test/inbound/InboundConnectorContextBuilder.java b/connector-sdk/test/src/main/java/io/camunda/connector/test/inbound/InboundConnectorContextBuilder.java index 71f82beb45..6697bb92fe 100644 --- a/connector-sdk/test/src/main/java/io/camunda/connector/test/inbound/InboundConnectorContextBuilder.java +++ b/connector-sdk/test/src/main/java/io/camunda/connector/test/inbound/InboundConnectorContextBuilder.java @@ -36,6 +36,11 @@ import io.camunda.connector.runtime.core.inbound.InboundConnectorElement; import io.camunda.connector.runtime.core.inbound.InboundConnectorReportingContext; import io.camunda.connector.test.ConnectorContextTestUtil; +import io.camunda.document.Document; +import io.camunda.document.factory.DocumentFactory; +import io.camunda.document.factory.DocumentFactoryImpl; +import io.camunda.document.store.DocumentCreationRequest; +import io.camunda.document.store.InMemoryDocumentStore; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -58,6 +63,9 @@ public class InboundConnectorContextBuilder { protected CorrelationResult result; + protected DocumentFactory documentFactory = + new DocumentFactoryImpl(InMemoryDocumentStore.INSTANCE); + public static InboundConnectorContextBuilder create() { return new InboundConnectorContextBuilder(); } @@ -192,16 +200,21 @@ public TestInboundConnectorContext build() { return new TestInboundConnectorContext(secretProvider, validationProvider, result); } + /** + * @return the {@link io.camunda.connector.api.inbound.InboundIntermediateConnectorContext} + * including all previously defined properties + */ + public TestInboundIntermediateConnectorContext buildIntermediateConnectorContext() { + return new TestInboundIntermediateConnectorContext(secretProvider, validationProvider); + } + public class TestInboundConnectorContext extends AbstractConnectorContext implements InboundConnectorContext, InboundConnectorReportingContext { private final List correlatedEvents = new ArrayList<>(); - - private Health health = Health.unknown(); - private final String propertiesWithSecrets; - private final CorrelationResult result; + private Health health = Health.unknown(); protected TestInboundConnectorContext( SecretProvider secretProvider, @@ -302,6 +315,11 @@ public Health getHealth() { @Override public void log(Activity activity) {} + @Override + public Document createDocument(DocumentCreationRequest request) { + return documentFactory.create(request); + } + @Override public Queue getLogs() { return new ConcurrentLinkedQueue<>(); @@ -314,14 +332,6 @@ public List connectorElements() { } } - /** - * @return the {@link io.camunda.connector.api.inbound.InboundIntermediateConnectorContext} - * including all previously defined properties - */ - public TestInboundIntermediateConnectorContext buildIntermediateConnectorContext() { - return new TestInboundIntermediateConnectorContext(secretProvider, validationProvider); - } - public class TestInboundIntermediateConnectorContext extends TestInboundConnectorContext implements InboundIntermediateConnectorContext { diff --git a/connector-sdk/test/src/main/java/io/camunda/connector/test/outbound/OutboundConnectorContextBuilder.java b/connector-sdk/test/src/main/java/io/camunda/connector/test/outbound/OutboundConnectorContextBuilder.java index 84c1e81e12..f9b63a785c 100644 --- a/connector-sdk/test/src/main/java/io/camunda/connector/test/outbound/OutboundConnectorContextBuilder.java +++ b/connector-sdk/test/src/main/java/io/camunda/connector/test/outbound/OutboundConnectorContextBuilder.java @@ -26,6 +26,11 @@ import io.camunda.connector.api.validation.ValidationProvider; import io.camunda.connector.runtime.core.AbstractConnectorContext; import io.camunda.connector.test.ConnectorContextTestUtil; +import io.camunda.document.Document; +import io.camunda.document.factory.DocumentFactory; +import io.camunda.document.factory.DocumentFactoryImpl; +import io.camunda.document.store.DocumentCreationRequest; +import io.camunda.document.store.InMemoryDocumentStore; import java.util.HashMap; import java.util.Map; @@ -33,14 +38,12 @@ public class OutboundConnectorContextBuilder { protected final Map secrets = new HashMap<>(); + protected final Map headers = new HashMap<>(); protected SecretProvider secretProvider = secrets::get; - protected ValidationProvider validationProvider; - protected Map variables; - - protected final Map headers = new HashMap<>(); - + protected DocumentFactory documentFactory = + new DocumentFactoryImpl(InMemoryDocumentStore.INSTANCE); private ObjectMapper objectMapper = ConnectorsObjectMapperSupplier.getCopy(); /** @@ -208,5 +211,10 @@ public T bindVariables(Class cls) { throw new RuntimeException(e); } } + + @Override + public Document createDocument(DocumentCreationRequest request) { + return documentFactory.create(request); + } } } diff --git a/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/client/apache/builder/parts/ApacheRequestBodyBuilder.java b/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/client/apache/builder/parts/ApacheRequestBodyBuilder.java index 6656933e2a..491bfd9a83 100644 --- a/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/client/apache/builder/parts/ApacheRequestBodyBuilder.java +++ b/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/client/apache/builder/parts/ApacheRequestBodyBuilder.java @@ -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; @@ -89,6 +94,9 @@ private Optional tryGetContentType(HttpCommonRequest request) { private HttpEntity createStringEntity(HttpCommonRequest request) { Object body = request.getBody(); + if (body instanceof Map map) { + body = new DocumentHelper().parseDocumentsInBody(map, Document::asByteArray); + } Optional contentType = tryGetContentType(request); try { return body instanceof String s @@ -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()), + ContentType.DEFAULT_BINARY, + metadata.getFileName()); + } } diff --git a/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/utils/DocumentHelper.java b/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/utils/DocumentHelper.java new file mode 100644 index 0000000000..4c0de773a4 --- /dev/null +++ b/connectors/http/http-base/src/main/java/io/camunda/connector/http/base/utils/DocumentHelper.java @@ -0,0 +1,51 @@ +/* + * 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 { + + /** + * 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 parseDocumentsInBody(Object input, Function transformer) { + return switch (input) { + case Map map -> + map.entrySet().stream() + .map( + (Map.Entry e) -> + new AbstractMap.SimpleEntry<>( + e.getKey(), parseDocumentsInBody(e.getValue(), transformer))) + .collect( + Collectors.toMap( + AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue)); + + case Collection list -> list.stream().map(o -> parseDocumentsInBody(o, transformer)).toList(); + case CamundaDocument doc -> transformer.apply(doc); + default -> input; + }; + } +} diff --git a/connectors/http/http-base/src/test/java/io/camunda/connector/http/base/client/apache/CustomApacheHttpClientTest.java b/connectors/http/http-base/src/test/java/io/camunda/connector/http/base/client/apache/CustomApacheHttpClientTest.java index 517b8b13de..7014312685 100644 --- a/connectors/http/http-base/src/test/java/io/camunda/connector/http/base/client/apache/CustomApacheHttpClientTest.java +++ b/connectors/http/http-base/src/test/java/io/camunda/connector/http/base/client/apache/CustomApacheHttpClientTest.java @@ -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; @@ -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 { diff --git a/connectors/http/http-base/src/test/java/io/camunda/connector/http/base/utils/DocumentHelperTest.java b/connectors/http/http-base/src/test/java/io/camunda/connector/http/base/utils/DocumentHelperTest.java new file mode 100644 index 0000000000..844eacccbb --- /dev/null +++ b/connectors/http/http-base/src/test/java/io/camunda/connector/http/base/utils/DocumentHelperTest.java @@ -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 shouldParseDocuments_InBody_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 input = + Map.of("body", Map.of("content", Arrays.asList(document, document, document))); + Function transformer = mock(Function.class); + when(transformer.apply(document)).thenReturn("transformed".getBytes(StandardCharsets.UTF_8)); + + // when + Object res = documentHelper.parseDocumentsInBody(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) ((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 shouldParseDocuments_InBody_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 input = Arrays.asList(document, document, document); + Function transformer = mock(Function.class); + when(transformer.apply(document)).thenReturn("transformed".getBytes(StandardCharsets.UTF_8)); + + // when + Object res = documentHelper.parseDocumentsInBody(input, transformer); + + // then + assertThat(res).isInstanceOf(List.class); + verify(transformer, times(3)).apply(document); + assertThat((List) res) + .containsAll( + Arrays.asList( + "transformed".getBytes(StandardCharsets.UTF_8), + "transformed".getBytes(StandardCharsets.UTF_8), + "transformed".getBytes(StandardCharsets.UTF_8))); + } + + @Test + public void shouldNotParseDocuments_InBody_whenNoDocumentProvided() { + // given + DocumentHelper documentHelper = new DocumentHelper(); + Map input = Map.of("body", Map.of("content", "no document")); + Function transformer = mock(Function.class); + + // when + Object res = documentHelper.parseDocumentsInBody(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"); + } +} diff --git a/parent/pom.xml b/parent/pom.xml index 146adf220c..e7bc6bd052 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -269,6 +269,12 @@ limitations under the License. ${project.version} + + io.camunda.connector + connector-document + ${project.version} + + io.camunda zeebe-client-java