@@ -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 nexusOperationLinkPathFormat =
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" ;
@@ -29,6 +31,10 @@ public class LinkConverter {
2931 Link .WorkflowEvent .EventReference .getDescriptor ().getName ();
3032 private static final String requestIDReferenceType =
3133 Link .WorkflowEvent .RequestIdReference .getDescriptor ().getName ();
34+ private static final String workflowEventLinkType =
35+ Link .WorkflowEvent .getDescriptor ().getFullName ();
36+ private static final String nexusOperationLinkType =
37+ Link .NexusOperation .getDescriptor ().getFullName ();
3238
3339 public static io .temporal .api .nexus .v1 .Link workflowEventToNexusLink (Link .WorkflowEvent we ) {
3440 try {
@@ -160,6 +166,107 @@ public static Link nexusLinkToWorkflowEvent(io.temporal.api.nexus.v1.Link nexusL
160166 return link .build ();
161167 }
162168
169+ /**
170+ * Dispatches on the oneof variant of {@code commonLink} and converts to the matching {@link
171+ * io.temporal.api.nexus.v1.Link}. Returns {@code null} if no variant is set or encoding fails.
172+ */
173+ public static io .temporal .api .nexus .v1 .Link linkToNexusLink (Link commonLink ) {
174+ if (commonLink .hasWorkflowEvent ()) {
175+ return workflowEventToNexusLink (commonLink .getWorkflowEvent ());
176+ }
177+ if (commonLink .hasNexusOperation ()) {
178+ return nexusOperationToNexusLink (commonLink .getNexusOperation ());
179+ }
180+ return null ;
181+ }
182+
183+ /**
184+ * Dispatches on {@link io.temporal.api.nexus.v1.Link#getType()} and converts to the matching
185+ * {@link Link} variant. Returns {@code null} for unknown or unparseable types.
186+ */
187+ public static Link nexusLinkToLink (io .temporal .api .nexus .v1 .Link nexusLink ) {
188+ String type = nexusLink .getType ();
189+ if (workflowEventLinkType .equals (type )) {
190+ return nexusLinkToWorkflowEvent (nexusLink );
191+ }
192+ if (nexusOperationLinkType .equals (type )) {
193+ return nexusLinkToNexusOperation (nexusLink );
194+ }
195+ log .warn ("ignoring unsupported nexus link type: {}" , type );
196+ return null ;
197+ }
198+
199+ public static io .temporal .api .nexus .v1 .Link nexusOperationToNexusLink (Link .NexusOperation no ) {
200+ try {
201+ String url =
202+ String .format (
203+ nexusOperationLinkPathFormat ,
204+ URLEncoder .encode (no .getNamespace (), StandardCharsets .UTF_8 .toString ()),
205+ // See the WorkflowId comment in workflowEventToNexusLink for why '+' is rewritten to
206+ // '%20'. OperationId is user-supplied and can legally contain spaces.
207+ URLEncoder .encode (no .getOperationId (), StandardCharsets .UTF_8 .toString ())
208+ .replace ("+" , "%20" ),
209+ URLEncoder .encode (no .getRunId (), StandardCharsets .UTF_8 .toString ()));
210+ return io .temporal .api .nexus .v1 .Link .newBuilder ()
211+ .setUrl (url )
212+ .setType (nexusOperationLinkType )
213+ .build ();
214+ } catch (Exception e ) {
215+ log .error ("Failed to encode Nexus operation link URL" , e );
216+ }
217+ return null ;
218+ }
219+
220+ public static Link nexusLinkToNexusOperation (io .temporal .api .nexus .v1 .Link nexusLink ) {
221+ if (!nexusOperationLinkType .equals (nexusLink .getType ())) {
222+ log .error (
223+ "Failed to parse Nexus link URL: cannot parse link type {} to {}" ,
224+ nexusLink .getType (),
225+ nexusOperationLinkType );
226+ return null ;
227+ }
228+ Link .Builder link = Link .newBuilder ();
229+ try {
230+ URI uri = new URI (nexusLink .getUrl ());
231+
232+ if (!"temporal" .equals (uri .getScheme ())) {
233+ log .error ("Failed to parse Nexus link URL: invalid scheme: {}" , uri .getScheme ());
234+ return null ;
235+ }
236+
237+ StringTokenizer st = new StringTokenizer (uri .getRawPath (), "/" );
238+ if (!st .hasMoreTokens () || !st .nextToken ().equals ("namespaces" )) {
239+ log .error ("Failed to parse Nexus link URL: invalid path: {}" , uri .getRawPath ());
240+ return null ;
241+ }
242+ String namespace = URLDecoder .decode (st .nextToken (), StandardCharsets .UTF_8 .toString ());
243+ if (!st .hasMoreTokens () || !st .nextToken ().equals ("nexus-operations" )) {
244+ log .error ("Failed to parse Nexus link URL: invalid path: {}" , uri .getRawPath ());
245+ return null ;
246+ }
247+ String operationId = URLDecoder .decode (st .nextToken (), StandardCharsets .UTF_8 .toString ());
248+ if (!st .hasMoreTokens ()) {
249+ log .error ("Failed to parse Nexus link URL: invalid path: {}" , uri .getRawPath ());
250+ return null ;
251+ }
252+ String runId = URLDecoder .decode (st .nextToken (), StandardCharsets .UTF_8 .toString ());
253+ if (!st .hasMoreTokens () || !st .nextToken ().equals ("details" )) {
254+ log .error ("Failed to parse Nexus link URL: invalid path: {}" , uri .getRawPath ());
255+ return null ;
256+ }
257+
258+ link .setNexusOperation (
259+ Link .NexusOperation .newBuilder ()
260+ .setNamespace (namespace )
261+ .setOperationId (operationId )
262+ .setRunId (runId ));
263+ } catch (Exception e ) {
264+ log .error ("Failed to parse Nexus link URL" , e );
265+ return null ;
266+ }
267+ return link .build ();
268+ }
269+
163270 private static Map <String , String > parseQueryParams (URI uri ) throws UnsupportedEncodingException {
164271 final String query = uri .getQuery ();
165272 if (query == null || query .isEmpty ()) {
0 commit comments