Skip to content
1 change: 1 addition & 0 deletions gradle/jacoco.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dependencies {
jacocoAggregation project(':temporal-spring-boot-starter')
jacocoAggregation project(':temporal-test-server')
jacocoAggregation project(':temporal-testing')
jacocoAggregation project(':temporal-envconfig')
}

def jacocoExclusions = [
Expand Down
3 changes: 2 additions & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ include 'temporal-kotlin'
include 'temporal-spring-boot-autoconfigure'
include 'temporal-spring-boot-starter'
include 'temporal-remote-data-encoder'
include 'temporal-shaded'
include 'temporal-shaded'
include 'temporal-envconfig'
1 change: 1 addition & 0 deletions temporal-bom/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ dependencies {
api project(':temporal-spring-boot-starter')
api project(':temporal-test-server')
api project(':temporal-testing')
api project(':temporal-envconfig')
}
}
13 changes: 13 additions & 0 deletions temporal-envconfig/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
description = '''Temporal Java SDK Environment Config Module'''
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In terms of naming it would probably more consistent internally in the Java SDK to call this module temporal-enviorment-configuration, but that isn't consistent with the Go SDK

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 The envconfig shortened term for the modules/packages/namespaces myself is kinda nice


dependencies {
// this module shouldn't carry temporal-sdk with it, especially for situations when users may be using a shaded artifact
compileOnly project(':temporal-sdk')

implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-toml:${jacksonVersion}"
testImplementation project(":temporal-testing")
testImplementation "junit:junit:${junitVersion}"
testImplementation "org.mockito:mockito-core:${mockitoVersion}"

testRuntimeOnly group: 'ch.qos.logback', name: 'logback-classic', version: "${logbackVersion}"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package io.temporal.envconfig;

import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.dataformat.toml.TomlMapper;
import io.temporal.common.Experimental;
import java.io.*;
import java.util.Map;
import java.util.Objects;

/** ClientConfig represents a client config file. */
@Experimental
public class ClientConfig {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hrmm, ideally we could have one static call on client config in the SDKs that gives you what you need to connect a client. With Java this is admittedly harder because 1) stubs are separate from client, and 2) Java doesn't support a native tuple to return two things at once.

I wonder if there is any creative approach we can think of here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing I could think of was have a method that gives you a full client that takes a few options customizers to set additional options on the service stub options and client options.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what you have here is good enough

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it sounds pedantic, but can we also expose static calls for getting the default profile stuff to connect. Specifically, can we have:

public static WorkflowServiceStubsOptions loadWorkflowServiceStubsOptions()

and

public static WorkflowClientOptions loadWorkflowClientOptions()

on this class? Or maybe named loadDefaultWorkflowServiceStubsOptions or something? With the other SDKs, we are putting a static call on the highest level class/place we can to be able to load what is needed from the default profile to make client connections. I would guess these would be the calls used by every SDK sample.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we wan't a short cut to call ClientConfigProfile.load().toWorkflowServiceStubsOptions()?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we have been adding this to other SDKs. But it's not a big deal if we don't do it.


/** Get the default config file path: $HOME/.config/temporal/temporal.toml */
private static String getDefaultConfigFilePath() {
String userDir = System.getProperty("user.home");
if (userDir == null || userDir.isEmpty()) {
throw new RuntimeException("failed getting user home directory");
}
return userDir + "/.config/temporal/temporal.toml";
}

/**
* Load all client profiles from given sources.
*
* <p>This does not apply environment variable overrides to the profiles, it only uses an
* environment variable to find the default config file path (TEMPORAL_CONFIG_FILE). To get a
* single profile with environment variables applied, use {@link ClientConfigProfile#load}.
*/
public static ClientConfig load() throws IOException {
return load(LoadClientConfigOptions.newBuilder().build());
}

/**
* Load all client profiles from given sources.
*
* <p>This does not apply environment variable overrides to the profiles, it only uses an
* environment variable to find the default config file path (TEMPORAL_CONFIG_FILE). To get a
* single profile with environment variables applied, use {@link ClientConfigProfile#load}.
*
* @param options options to control loading the config
* @throws IOException if the config file cannot be read or parsed
*/
public static ClientConfig load(LoadClientConfigOptions options) throws IOException {
ObjectReader reader = new TomlMapper().readerFor(ClientConfigToml.TomlClientConfig.class);
if (options.isStrictConfigFile()) {
reader =
reader.withFeatures(
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
} else {
reader =
reader.withoutFeatures(
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
if (options.getConfigFileData() != null && options.getConfigFileData().length > 0) {
if (options.getConfigFilePath() != null && !options.getConfigFilePath().isEmpty()) {
throw new IllegalArgumentException(
"Cannot have both ConfigFileData and ConfigFilePath set");
}
ClientConfigToml.TomlClientConfig result = reader.readValue(options.getConfigFileData());
return new ClientConfig(ClientConfigToml.getClientProfiles(result));
} else {
// Get file name which is either set value, env var, or default path
String file = options.getConfigFilePath();
if (file == null || file.isEmpty()) {
Map<String, String> env = options.getEnvOverrides();
if (env == null) {
env = System.getenv();
}
// Unlike env vars for the config values, empty and unset env var
// for config file path are both treated as unset
file = env.get("TEMPORAL_CONFIG_FILE");
}
if (file == null || file.isEmpty()) {
file = getDefaultConfigFilePath();
}
ClientConfigToml.TomlClientConfig result = reader.readValue(new File(file));
return new ClientConfig(ClientConfigToml.getClientProfiles(result));
}
}

/**
* Load client config from given TOML data.
*
* @param tomlData TOML data to parse
* @return the parsed client config
* @throws IOException if the TOML data cannot be parsed
*/
public static ClientConfig fromToml(byte[] tomlData) throws IOException {
return fromToml(tomlData, ClientConfigFromTomlOptions.getDefaultInstance());
}

/**
* Load client config from given TOML data.
*
* @param tomlData TOML data to parse
* @param options options to control parsing the TOML data
* @return the parsed client config
* @throws IOException if the TOML data cannot be parsed
*/
public static ClientConfig fromToml(byte[] tomlData, ClientConfigFromTomlOptions options)
throws IOException {
ObjectReader reader = new TomlMapper().readerFor(ClientConfigToml.TomlClientConfig.class);
if (options.isStrictConfigFile()) {
reader =
reader.withFeatures(
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
} else {
reader =
reader.withoutFeatures(
com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
}
ClientConfigToml.TomlClientConfig result = reader.readValue(tomlData);
return new ClientConfig(ClientConfigToml.getClientProfiles(result));
}

/**
* Convert the client config to TOML data. Encoding is UTF-8.
*
* @param config the client config to convert
* @return the TOML data as bytes
* @apiNote The output will not be identical to the input if the config was loaded from a file
* because comments and formatting are not preserved.
*/
public static byte[] toTomlAsBytes(ClientConfig config) throws IOException {
ObjectWriter writer = new TomlMapper().writerFor(ClientConfigToml.TomlClientConfig.class);
return writer.writeValueAsBytes(
new ClientConfigToml.TomlClientConfig(
ClientConfigToml.fromClientProfiles(config.getProfiles())));
}

public ClientConfig(Map<String, ClientConfigProfile> profiles) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May want to use a builder here for consistency

this.profiles = profiles;
}

private final Map<String, ClientConfigProfile> profiles;

/** All profiles loaded from the config file, may be empty but never null. */
public Map<String, ClientConfigProfile> getProfiles() {
return profiles;
}

@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
ClientConfig that = (ClientConfig) o;
return Objects.equals(profiles, that.profiles);
}

@Override
public int hashCode() {
return Objects.hashCode(profiles);
}

@Override
public String toString() {
return "ClientConfig{" + "profiles=" + profiles + '}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.temporal.envconfig;

/**
* Options for parsing a client config toml file in {@link ClientConfig#fromToml(byte[],
* ClientConfigFromTomlOptions)}
*/
public class ClientConfigFromTomlOptions {
/** Create a builder for {@link ClientConfigFromTomlOptions}. */
public static Builder newBuilder() {
return new ClientConfigFromTomlOptions.Builder();
}

/** Create a builder from an existing {@link ClientConfigFromTomlOptions}. */
public static Builder newBuilder(ClientConfigFromTomlOptions options) {
return new Builder(options);
}

/** Returns a default instance of {@link ClientConfigFromTomlOptions} with all fields unset. */
public static ClientConfigFromTomlOptions getDefaultInstance() {
return new ClientConfigFromTomlOptions.Builder().build();
}

private final Boolean strictConfigFile;

private ClientConfigFromTomlOptions(Boolean strictConfigFile) {
this.strictConfigFile = strictConfigFile;
}

public Boolean isStrictConfigFile() {
return strictConfigFile;
}

public static class Builder {
private Boolean strictConfigFile = false;

private Builder() {}

private Builder(ClientConfigFromTomlOptions options) {
this.strictConfigFile = options.strictConfigFile;
}

/**
* When true, the parser will fail if the config file contains unknown fields. Default is false.
*/
public Builder setStrictConfigFile(Boolean strictConfigFile) {
this.strictConfigFile = strictConfigFile;
return this;
}

public ClientConfigFromTomlOptions build() {
return new ClientConfigFromTomlOptions(strictConfigFile);
}
}
}
Loading
Loading