Skip to content

Commit

Permalink
Add new functional mapper to create a shared data store (#254)
Browse files Browse the repository at this point in the history
Create a shared data store, so that the shared data is accessible among
the process instances and also among the different workflows.

The shared data is grouped by its namespace, the key of the data must be
unique in its namespace, but could be duplicate in different namespace.
  • Loading branch information
yinan-symphony authored Apr 20, 2023
1 parent 8f7b715 commit 7a45422
Show file tree
Hide file tree
Showing 20 changed files with 350 additions and 18 deletions.
12 changes: 9 additions & 3 deletions allow-list.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,18 @@
</suppress>
<suppress>
<notes><![CDATA[
Testing false positives by suppressing a CVE
https://github.com/spring-projects/spring-framework/issues/24434 (Do not expose HttpInvoker)
]]></notes>
No fix available
]]></notes>
<gav>org.springframework:spring-web:5.3.26</gav>
<cve>CVE-2016-1000027</cve>
</suppress>
<suppress>
<notes><![CDATA[
No fix available
]]></notes>
<gav>org.springframework:spring-expression:5.3.26</gav>
<cve>CVE-2023-20863</cve>
</suppress>
<suppress>
<notes><![CDATA[
Dependency not found in the dependency analyzer, no idea where it is found
Expand Down
44 changes: 44 additions & 0 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2578,3 +2578,47 @@ activities:
println wdk.session().id
println wdk.session().displayName
```

### Object readShared(String namespace, String key)
Along side the global variables, WDK provides shared data, which is accessible (read and write via utility functions) among the
a workflow process instances, and also among different workflows.

The shared data is organized by its namespace and and its key.

This method will read the value from the given namespace and the key.

Example:

in [send-message](#send-message)

```yaml
activities:
- send-message:
id: sendBotDisplayName
content: ${readShared("namespace", "key")}
```

in [execute-script](#execute-script)

```yaml
activities:
- execute-script:
id: printBotInfo
script: |
println wdk.readShared("namespace", "key")
```

### void writeShared(String namespace, String key, Object value)
This method will write the shared data to the given namespace and under the given key.

Example:

in [execute-script](#execute-script)

```yaml
activities:
- execute-script:
id: printBotInfo
script: |
wdk.writeShared("namespace", "key", value);
```
1 change: 1 addition & 0 deletions workflow-bot-app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ dependencies {

implementation 'com.github.ben-manes.caffeine:caffeine:3.1.1'
implementation 'io.springfox:springfox-boot-starter:3.0.0'
implementation 'io.hypersistence:hypersistence-utils-hibernate-55:3.3.1'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.junit.jupiter:junit-jupiter'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
@Configuration
@ConditionalOnPropertyNotEmpty("wdk.properties.management-token")
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = "com.symphony.bdk.workflow.versioning",
@EnableJpaRepositories(basePackages = {"com.symphony.bdk.workflow.versioning", "com.symphony.bdk.workflow.shared"},
transactionManagerRef = "transactionManager")
@Profile("!test")
@Slf4j
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.symphony.bdk.workflow.engine.camunda;

import com.symphony.bdk.workflow.engine.executor.BdkGateway;
import com.symphony.bdk.workflow.engine.executor.SharedDataStore;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -28,6 +29,8 @@ public class CamundaEngineConfiguration implements ProcessEnginePlugin {

private final BdkGateway bdkGateway;

private final SharedDataStore sharedDataStore;

@Override
public void preInit(ProcessEngineConfigurationImpl processEngineConfiguration) {
ExpressionManager expressionManager = processEngineConfiguration.getExpressionManager();
Expand All @@ -47,12 +50,18 @@ public void preInit(ProcessEngineConfigurationImpl processEngineConfiguration) {
ReflectUtil.getMethod(UtilityFunctionsMapper.class, UtilityFunctionsMapper.EMOJIS, Object.class));
expressionManager.addFunction(UtilityFunctionsMapper.SESSION,
ReflectUtil.getMethod(UtilityFunctionsMapper.class, UtilityFunctionsMapper.SESSION));
expressionManager.addFunction(UtilityFunctionsMapper.READSHARED,
ReflectUtil.getMethod(UtilityFunctionsMapper.class, UtilityFunctionsMapper.READSHARED, String.class,
String.class));
expressionManager.addFunction(UtilityFunctionsMapper.WRITESHARED,
ReflectUtil.getMethod(UtilityFunctionsMapper.class, UtilityFunctionsMapper.WRITESHARED, String.class,
String.class, Object.class));
}

@Override
public void postInit(ProcessEngineConfigurationImpl processEngineConfiguration) {
processEngineConfiguration.getBeans().put(
UtilityFunctionsMapper.WDK_PREFIX, new UtilityFunctionsMapper(this.bdkGateway.session()));
UtilityFunctionsMapper.WDK_PREFIX, new UtilityFunctionsMapper(this.bdkGateway.session(), this.sharedDataStore));
handleScriptExceptionsAsBpmnErrors(processEngineConfiguration);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.symphony.bdk.workflow.engine.executor.ActivityExecutorContext;
import com.symphony.bdk.workflow.engine.executor.BdkGateway;
import com.symphony.bdk.workflow.engine.executor.EventHolder;
import com.symphony.bdk.workflow.engine.executor.SharedDataStore;
import com.symphony.bdk.workflow.engine.handler.audit.AuditTrailLogAction;
import com.symphony.bdk.workflow.swadl.v1.activity.BaseActivity;

Expand Down Expand Up @@ -72,13 +73,15 @@ public class CamundaExecutor implements JavaDelegate {
}

private final BdkGateway bdk;
private final SharedDataStore sharedDataStore;
private final AuditTrailLogAction auditTrailLogger;
private final ResourceProvider resourceLoader;
private final ApplicationContext applicationContext;

public CamundaExecutor(BdkGateway bdk, AuditTrailLogAction auditTrailLogger,
public CamundaExecutor(BdkGateway bdk, SharedDataStore sharedDataStore, AuditTrailLogAction auditTrailLogger,
@Qualifier("workflowResourcesProvider") ResourceProvider resourceLoader, ApplicationContext applicationContext) {
this.bdk = bdk;
this.sharedDataStore = sharedDataStore;
this.auditTrailLogger = auditTrailLogger;
this.resourceLoader = resourceLoader;
this.applicationContext = applicationContext;
Expand Down Expand Up @@ -115,7 +118,7 @@ public void execute(DelegateExecution execution) throws Exception {
setMdc(execution);
auditTrailLogger.execute(execution, activity.getClass().getSimpleName());
executor.execute(
new CamundaActivityExecutorContext(execution, activity, event, resourceLoader, bdk));
new CamundaActivityExecutorContext(execution, activity, event, resourceLoader, bdk, sharedDataStore));
} catch (Exception e) {
log.error(String.format("Activity from workflow %s failed", execution.getProcessDefinitionId()), e);
logErrorVariables(execution, activity, e);
Expand Down Expand Up @@ -154,14 +157,16 @@ private static class CamundaActivityExecutorContext<T extends BaseActivity> impl
private final EventHolder<Object> event;
private final ResourceProvider resourceLoader;
private final BdkGateway bdk;
private final SharedDataStore sharedDataStore;

public CamundaActivityExecutorContext(DelegateExecution execution, T activity, EventHolder<Object> event,
ResourceProvider resourceLoader, BdkGateway bdk) {
ResourceProvider resourceLoader, BdkGateway bdk, SharedDataStore sharedDataStore) {
this.execution = execution;
this.activity = activity;
this.event = event;
this.resourceLoader = resourceLoader;
this.bdk = bdk;
this.sharedDataStore = sharedDataStore;
}

@Override
Expand Down Expand Up @@ -211,6 +216,11 @@ public BdkGateway bdk() {
return bdk;
}

@Override
public SharedDataStore sharedDataStore() {
return sharedDataStore;
}

@Override
public T getActivity() {
return activity;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.symphony.bdk.gen.api.model.UserV2;
import com.symphony.bdk.gen.api.model.V4MessageSent;
import com.symphony.bdk.workflow.engine.executor.EventHolder;
import com.symphony.bdk.workflow.engine.executor.SharedDataStore;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.io.JsonStringEncoder;
Expand All @@ -28,13 +29,20 @@
public class UtilityFunctionsMapper extends FunctionMapper {
private static SessionService staticSessionService;

private static SharedDataStore sharedDataStore;

public static void setStaticSessionService(SessionService sessionService) {
UtilityFunctionsMapper.staticSessionService = sessionService;
}

public static void setSharedStateService(SharedDataStore sharedDataStore) {
UtilityFunctionsMapper.sharedDataStore = sharedDataStore;
}

@Autowired
public UtilityFunctionsMapper(SessionService sessionService) {
public UtilityFunctionsMapper(SessionService sessionService, SharedDataStore sharedDataStore) {
setStaticSessionService(sessionService);
setSharedStateService(sharedDataStore);
}

/**
Expand All @@ -48,8 +56,10 @@ public UtilityFunctionsMapper(SessionService sessionService) {
public static final String MENTIONS = "mentions";
public static final String HASHTAGS = "hashTags";
public static final String CASHTAGS = "cashTags";
public static final String EMOJIS = "emojis";
public static final String SESSION = "session";
public static final String EMOJIS = "emojis";
public static final String SESSION = "session";
public static final String READSHARED = "readShared";
public static final String WRITESHARED = "writeShared";

private static final Map<String, Method> FUNCTION_MAP;
private static final ObjectMapper objectMapper = new ObjectMapper();
Expand All @@ -64,6 +74,10 @@ public UtilityFunctionsMapper(SessionService sessionService) {
FUNCTION_MAP.put(CASHTAGS, ReflectUtil.getMethod(UtilityFunctionsMapper.class, CASHTAGS, Object.class));
FUNCTION_MAP.put(EMOJIS, ReflectUtil.getMethod(UtilityFunctionsMapper.class, ESCAPE, Object.class));
FUNCTION_MAP.put(SESSION, ReflectUtil.getMethod(UtilityFunctionsMapper.class, SESSION));
FUNCTION_MAP.put(READSHARED,
ReflectUtil.getMethod(UtilityFunctionsMapper.class, READSHARED, String.class, String.class));
FUNCTION_MAP.put(WRITESHARED,
ReflectUtil.getMethod(UtilityFunctionsMapper.class, WRITESHARED, String.class, String.class, Object.class));
}

@Override
Expand All @@ -83,6 +97,14 @@ public static Object json(String string) {
}
}

public static Object readShared(String namespace, String key) {
return sharedDataStore.getNamespaceData(namespace).get(key);
}

public static void writeShared(String namespace, String key, Object data) {
sharedDataStore.putNamespaceData(namespace, key, data);
}

public static String text(String presentationMl) throws PresentationMLParserException {
return PresentationMLParser.getTextContent(presentationMl);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public static String extractContent(ActivityExecutorContext<?> execution, String
} else {
Map<String, Object> templateVariables = new HashMap<>(execution.getVariables());
// also bind our utility functions so they can be used inside templates
templateVariables.put(UtilityFunctionsMapper.WDK_PREFIX, new UtilityFunctionsMapper(execution.bdk().session()));
templateVariables.put(UtilityFunctionsMapper.WDK_PREFIX,
new UtilityFunctionsMapper(execution.bdk().session(), execution.sharedDataStore()));

if (templatePath != null) {
File file = execution.getResourceFile(Path.of(templatePath));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.symphony.bdk.workflow.shared;

import com.symphony.bdk.workflow.engine.executor.SharedDataStore;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;
import java.util.Map;

@Component
@RequiredArgsConstructor
@Transactional(propagation = Propagation.REQUIRES_NEW)
public class DefaultSharedDataStore implements SharedDataStore {
private final SharedDataRepository repository;

@Override
public Map<String, Object> getNamespaceData(String namespace) {
return repository.findByNamespace(namespace).orElse(new SharedData()).getProperties();
}

@Override
public void putNamespaceData(String namespace, String key, Object data) {
SharedData sharedData = repository.findByNamespace(namespace).orElse(new SharedData().namespace(namespace));
sharedData.getProperties().put(key, data);
sharedData.setLastUpdated(Instant.now().toEpochMilli());
repository.save(sharedData);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.symphony.bdk.workflow.shared;

import io.hypersistence.utils.hibernate.type.json.JsonType;
import lombok.Data;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.NaturalId;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;

import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "SHARED_STATE_DATA")
@TypeDef(name = "json", typeClass = JsonType.class)
@Data
public class SharedData {
@Id
@GeneratedValue(generator = "system-uuid")
@GenericGenerator(name = "system-uuid", strategy = "uuid2")
@Column(name = "ID")
private String id;

@NaturalId
@Column(length = 15)
private String namespace;

@Type(type = "json")
@Column(columnDefinition = "json")
private Map<String, Object> properties = new HashMap<>();

@Column(name = "LAST_UPDATED", length = 50)
private Long lastUpdated;

public SharedData namespace(String namespace) {
this.setNamespace(namespace);
return this;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.symphony.bdk.workflow.shared;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface SharedDataRepository extends JpaRepository<SharedData, String> {
Optional<SharedData> findByNamespace(String namespace);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
public class VersionedWorkflow {
@Id
@GeneratedValue(generator = "system-uuid")
@GenericGenerator(name = "system-uuid", strategy = "uuid")
@GenericGenerator(name = "system-uuid", strategy = "uuid2")
@Column(name = "ID")
private String id;
@Column(name = "WORKFLOW_ID", nullable = false, length = 100)
Expand Down
6 changes: 6 additions & 0 deletions workflow-bot-app/src/main/resources/application-local.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ bdk:
privateKey:
path: xxx

spring:
jpa:
show-sql: true

logging:
level:
org.camunda.bpm.engine.bpmn.parser: DEBUG
Expand All @@ -23,3 +27,5 @@ logging:
org.camunda.bpm.engine.script: DEBUG
org.camunda.bpm: OFF
com.symphony.bdk.workflow.engine.camunda.bpmn.CamundaBpmnBuilder: DEBUG
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql: TRACE
3 changes: 2 additions & 1 deletion workflow-bot-app/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ spring:
driver-class-name: org.h2.Driver
jdbc-url: jdbc:h2:mem:process_engine;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
jpa:
show-sql: true
open-in-view: false
show-sql: false
hibernate:
ddl-auto: update

Expand Down
Loading

0 comments on commit 7a45422

Please sign in to comment.