@@ -20,6 +20,8 @@ public class LinkConverter {
2020 private static final Logger log = LoggerFactory .getLogger (LinkConverter .class );
2121
2222 private static final String linkPathFormat = "temporal:///namespaces/%s/workflows/%s/%s/history" ;
23+ private static final String linkPathNexusOperationFormat =
24+ "temporal:///namespaces/%s/nexus-operations/%s/%s/details" ;
2325 private static final String linkReferenceTypeKey = "referenceType" ;
2426 private static final String linkEventIDKey = "eventID" ;
2527 private static final String linkEventTypeKey = "eventType" ;
@@ -30,6 +32,13 @@ public class LinkConverter {
3032 private static final String requestIDReferenceType =
3133 Link .WorkflowEvent .RequestIdReference .getDescriptor ().getName ();
3234
35+ // Fully-qualified proto descriptor names used as the `type` field on nexus.v1.Link. Match the
36+ // canonical server implementation at temporalio/temporal/common/nexus/link_converter.go so links
37+ // round-trip cleanly across SDKs.
38+ private static final String workflowEventType = Link .WorkflowEvent .getDescriptor ().getFullName ();
39+ private static final String nexusOperationType =
40+ Link .NexusOperation .getDescriptor ().getFullName ();
41+
3342 public static io .temporal .api .nexus .v1 .Link workflowEventToNexusLink (Link .WorkflowEvent we ) {
3443 try {
3544
@@ -160,6 +169,118 @@ public static Link nexusLinkToWorkflowEvent(io.temporal.api.nexus.v1.Link nexusL
160169 return link .build ();
161170 }
162171
172+ /**
173+ * Encode a {@link Link.NexusOperation} (a link to a standalone Nexus operation) into the (url,
174+ * type) form used on the Nexus wire. URL format matches the canonical server implementation:
175+ * {@code temporal:///namespaces/{ns}/nexus-operations/{op_id}/{run_id}/details}.
176+ */
177+ public static io .temporal .api .nexus .v1 .Link nexusOperationToNexusLink (Link .NexusOperation no ) {
178+ try {
179+ String url =
180+ String .format (
181+ linkPathNexusOperationFormat ,
182+ URLEncoder .encode (no .getNamespace (), StandardCharsets .UTF_8 .toString ()),
183+ URLEncoder .encode (no .getOperationId (), StandardCharsets .UTF_8 .toString ())
184+ .replace ("+" , "%20" ),
185+ URLEncoder .encode (no .getRunId (), StandardCharsets .UTF_8 .toString ()));
186+ return io .temporal .api .nexus .v1 .Link .newBuilder ()
187+ .setUrl (url )
188+ .setType (nexusOperationType )
189+ .build ();
190+ } catch (Exception e ) {
191+ log .error ("Failed to encode NexusOperation Nexus link URL" , e );
192+ }
193+ return null ;
194+ }
195+
196+ /**
197+ * Decode a {@code nexus.v1.Link} whose {@code type} is {@code Link.NexusOperation} into a {@code
198+ * common.v1.Link} carrying a {@link Link.NexusOperation} variant. The URL must match the format
199+ * produced by {@link #nexusOperationToNexusLink}.
200+ */
201+ public static Link nexusLinkToNexusOperation (io .temporal .api .nexus .v1 .Link nexusLink ) {
202+ try {
203+ URI uri = new URI (nexusLink .getUrl ());
204+ if (!"temporal" .equals (uri .getScheme ())) {
205+ log .error (
206+ "Failed to parse NexusOperation Nexus link URL: invalid scheme: {}" , uri .getScheme ());
207+ return null ;
208+ }
209+
210+ StringTokenizer st = new StringTokenizer (uri .getRawPath (), "/" );
211+ if (!st .hasMoreTokens () || !"namespaces" .equals (st .nextToken ())) {
212+ log .error (
213+ "Failed to parse NexusOperation Nexus link URL: invalid path: {}" , uri .getRawPath ());
214+ return null ;
215+ }
216+ String namespace = URLDecoder .decode (st .nextToken (), StandardCharsets .UTF_8 .toString ());
217+ if (!st .hasMoreTokens () || !"nexus-operations" .equals (st .nextToken ())) {
218+ log .error (
219+ "Failed to parse NexusOperation Nexus link URL: invalid path: {}" , uri .getRawPath ());
220+ return null ;
221+ }
222+ String operationId = URLDecoder .decode (st .nextToken (), StandardCharsets .UTF_8 .toString ());
223+ String runId = URLDecoder .decode (st .nextToken (), StandardCharsets .UTF_8 .toString ());
224+ if (!st .hasMoreTokens () || !"details" .equals (st .nextToken ())) {
225+ log .error (
226+ "Failed to parse NexusOperation Nexus link URL: invalid path: {}" , uri .getRawPath ());
227+ return null ;
228+ }
229+
230+ return Link .newBuilder ()
231+ .setNexusOperation (
232+ Link .NexusOperation .newBuilder ()
233+ .setNamespace (namespace )
234+ .setOperationId (operationId )
235+ .setRunId (runId ))
236+ .build ();
237+ } catch (Exception e ) {
238+ log .error ("Failed to parse NexusOperation Nexus link URL" , e );
239+ return null ;
240+ }
241+ }
242+
243+ /**
244+ * Encode a {@code common.v1.Link} into the Nexus-wire {@code nexus.v1.Link} (url, type) form,
245+ * dispatching on the link's variant. Returns {@code null} (with a warn log) for variants the SDK
246+ * does not yet know how to encode ({@code Activity}, {@code BatchJob}, unset) — match the
247+ * server's link-converter behavior so we stay in lockstep.
248+ */
249+ public static io .temporal .api .nexus .v1 .Link commonLinkToNexusLink (Link commonLink ) {
250+ switch (commonLink .getVariantCase ()) {
251+ case WORKFLOW_EVENT :
252+ return workflowEventToNexusLink (commonLink .getWorkflowEvent ());
253+ case NEXUS_OPERATION :
254+ return nexusOperationToNexusLink (commonLink .getNexusOperation ());
255+ default :
256+ log .warn (
257+ "Cannot encode common.v1.Link variant {} as nexus.v1.Link: no encoder implemented" ,
258+ commonLink .getVariantCase ());
259+ return null ;
260+ }
261+ }
262+
263+ /**
264+ * Decode a Nexus-wire {@code nexus.v1.Link} (url, type) into a {@code common.v1.Link},
265+ * dispatching on the link's {@code type} field. Returns {@code null} (with a warn log) for types
266+ * the SDK does not yet know how to decode.
267+ */
268+ public static Link nexusLinkToCommonLink (io .temporal .api .nexus .v1 .Link nexusLink ) {
269+ String type = nexusLink .getType ();
270+ if (workflowEventType .equals (type )) {
271+ return nexusLinkToWorkflowEvent (nexusLink );
272+ }
273+ if (nexusOperationType .equals (type )) {
274+ return nexusLinkToNexusOperation (nexusLink );
275+ }
276+ log .warn (
277+ "Cannot decode nexus.v1.Link of type '{}' to common.v1.Link:"
278+ + " no decoder implemented (url='{}')" ,
279+ type ,
280+ nexusLink .getUrl ());
281+ return null ;
282+ }
283+
163284 private static Map <String , String > parseQueryParams (URI uri ) throws UnsupportedEncodingException {
164285 final String query = uri .getQuery ();
165286 if (query == null || query .isEmpty ()) {
0 commit comments