Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions temporal-sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,35 @@ dependencies {
java21Implementation files(sourceSets.main.output.classesDirs) { builtBy compileJava }
}

// --- Payload visitor code generation ---
// A build-time generator (compiled in its own source set against the proto classes from
// temporal-serviceclient) emits GeneratedPayloadVisitor.java, which knows how to walk every
// payload-bearing Temporal API message. The generated source is added to the main source set.
sourceSets {
payloadVisitorGenerator {
java {
srcDirs = ['src/payloadVisitorGenerator/java']
}
}
}

dependencies {
payloadVisitorGeneratorImplementation project(':temporal-serviceclient')
}

def generatedPayloadVisitorDir = layout.buildDirectory.dir('generated/payloadvisitor/java')

def generatePayloadVisitor = tasks.register('generatePayloadVisitor', JavaExec) {
dependsOn 'compilePayloadVisitorGeneratorJava'
classpath = sourceSets.payloadVisitorGenerator.runtimeClasspath
mainClass = 'io.temporal.internal.payload.visitor.gen.PayloadVisitorGenerator'
args generatedPayloadVisitorDir.get().asFile.absolutePath
inputs.files(sourceSets.payloadVisitorGenerator.runtimeClasspath)
outputs.dir(generatedPayloadVisitorDir)
}

sourceSets.main.java.srcDir(generatePayloadVisitor)

tasks.named('compileJava17Java') {
options.release = 17
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.temporal.internal.payload.visitor;

import com.google.protobuf.Message;

/**
* Generated traversal for one message type: visits the message's payload fields and recurses into
* its child messages. There is one per message type that can contain a payload.
*/
interface GeneratedVisitor {
void visit(Traversal traversal, Message.Builder builder);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.temporal.internal.payload.visitor;

import com.google.protobuf.Message;
import java.util.function.Supplier;

/**
* How to traverse one message type, and how to create an empty builder for it (used to unpack
* {@code google.protobuf.Any} values).
*/
final class MessageRegistryEntry {
final GeneratedVisitor visitor;
final Supplier<Message.Builder> newBuilder;

MessageRegistryEntry(GeneratedVisitor visitor, Supplier<Message.Builder> newBuilder) {
this.visitor = visitor;
this.newBuilder = newBuilder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.temporal.internal.payload.visitor;

import com.google.protobuf.MessageOrBuilder;

/**
* Callback invoked when traversal enters a proto message. The returned value becomes the contextual
* value in scope for that message and everything within it, and is restored to the enclosing value
* once traversal leaves the message. The message is provided as a builder and may be inspected or
* mutated.
*
* @param <C> type of the contextual value
*/
@FunctionalInterface
public interface MessageVisitor<C> {
/**
* Handles a message being entered and returns the contextual value for it and its contents.
*
* @param current the contextual value in scope from the enclosing message
* @param message the message being entered
* @return the contextual value to use for this message and its contents
*/
C onEnter(C current, MessageOrBuilder message);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package io.temporal.internal.payload.visitor;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

/**
* Options for visiting the messages of a proto message, without visiting individual payloads.
*
* @param <C> type of the contextual value supplied to the visitor
*/
public final class MessageVisitorOptions<C> {
private final @Nonnull MessageVisitor<C> messageVisitor;
private final @Nullable C initialContext;

private MessageVisitorOptions(Builder<C> b) {
this.messageVisitor = b.messageVisitor;
this.initialContext = b.initialContext;
}

public static <C> Builder<C> newBuilder() {
return new Builder<>();
}

@Nonnull
public MessageVisitor<C> getMessageVisitor() {
return messageVisitor;
}

@Nullable
public C getInitialContext() {
return initialContext;
}

public static final class Builder<C> {
private MessageVisitor<C> messageVisitor;
private C initialContext;

private Builder() {}

/** Required. The message visitor. */
public Builder<C> setMessageVisitor(@Nonnull MessageVisitor<C> messageVisitor) {
this.messageVisitor = messageVisitor;
return this;
}

/** Optional. The contextual value in scope before any message is entered. */
public Builder<C> setInitialContext(@Nullable C initialContext) {
this.initialContext = initialContext;
return this;
}

public MessageVisitorOptions<C> build() {
if (messageVisitor == null) {
throw new IllegalArgumentException("messageVisitor is required");
}
return new MessageVisitorOptions<>(this);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.temporal.internal.payload.visitor;

import com.google.protobuf.Message;
import javax.annotation.Nonnull;

/**
* Visits the messages within a proto message, invoking the message visitor on each, without
* visiting individual payloads. Only messages that can contain a payload are visited.
*
* <p>This is an SDK-internal utility; it is not part of the public API.
*/
public final class MessageVisitors {
private MessageVisitors() {}

/** Visits the messages in {@code builder} in place. */
public static <C> void visit(
@Nonnull Message.Builder builder, @Nonnull MessageVisitorOptions<C> options) {
Traversal traversal =
new Traversal(
null,
options.getMessageVisitor(),
options.getInitialContext(),
/* skipSearchAttributes= */ false,
/* skipHeaders= */ false,
1,
null,
GeneratedPayloadVisitor.REGISTRY);
traversal.dispatch(builder);
traversal.execute();
}

/**
* Visits the messages in {@code message}, returning a copy with any changes applied; the input is
* unchanged.
*/
@SuppressWarnings("unchecked")
public static <C, T extends Message> T visit(
@Nonnull T message, @Nonnull MessageVisitorOptions<C> options) {
Message.Builder builder = message.toBuilder();
visit(builder, options);
return (T) builder.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.temporal.internal.payload.visitor;

import io.temporal.api.common.v1.Payload;
import java.util.List;

/**
* Callback for a sequence of payloads found in a proto message. The returned list replaces those
* payloads; return the same list to leave them unchanged.
*
* <p>When the visited field holds a single payload the list has one element and the visitor must
* return exactly one payload. With a concurrency limit greater than one, visits may run on multiple
* threads, so implementations must be thread-safe.
*
* @param <C> type of the contextual value supplied to each visit
*/
@FunctionalInterface
public interface PayloadVisitor<C> {
/**
* Visits a sequence of payloads and returns their replacements.
*
* @param context the location of these payloads and the contextual value in scope
* @param payloads the payloads found at this location
* @return the replacement payloads
*/
List<Payload> visit(PayloadVisitorContext<C> context, List<Payload> payloads);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package io.temporal.internal.payload.visitor;

import com.google.protobuf.MessageOrBuilder;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

/**
* The context for one payload visitor call: the contextual value in scope and the message that
* contains the payloads being visited.
*
* @param <C> type of the contextual value
*/
public final class PayloadVisitorContext<C> {
private final @Nullable C context;
private final @Nonnull MessageOrBuilder parent;
private final boolean singlePayloadRequired;

PayloadVisitorContext(
@Nullable C context, @Nonnull MessageOrBuilder parent, boolean singlePayloadRequired) {
this.context = context;
this.parent = parent;
this.singlePayloadRequired = singlePayloadRequired;
}

/** The contextual value in scope at this location, or {@code null} if none. */
@Nullable
public C getContext() {
return context;
}

/** The message that directly contains the payloads being visited. */
@Nonnull
public MessageOrBuilder getParent() {
return parent;
}

/**
* Whether the visited field holds a single payload. When {@code true}, the visitor must return
* exactly one payload.
*/
public boolean isSinglePayloadRequired() {
return singlePayloadRequired;
}
}
Loading
Loading