From 4c23b759c12ef1d1a87044364dab4da950742cf7 Mon Sep 17 00:00:00 2001 From: fjtirado Date: Thu, 14 Aug 2025 20:42:02 +0200 Subject: [PATCH] Fix #709 Handling AllOf Signed-off-by: fjtirado --- .../{Union.java => ExclusiveUnion.java} | 2 +- .../annotations/InclusiveUnion.java | 28 +++++++ .../io/serverlessworkflow/api/ApiTest.java | 77 +++++++++++++++++-- .../generator/jackson/JacksonMixInPojo.java | 31 ++++++-- .../generator/AllAnyOneOfSchemaRule.java | 54 ++----------- .../generator/GeneratorUtils.java | 11 +++ .../generator/JTypeWrapper.java | 62 +++++++++++++++ 7 files changed, 205 insertions(+), 60 deletions(-) rename annotations/src/main/java/io/serverlessworkflow/annotations/{Union.java => ExclusiveUnion.java} (96%) create mode 100644 annotations/src/main/java/io/serverlessworkflow/annotations/InclusiveUnion.java create mode 100644 generators/types/src/main/java/io/serverlessworkflow/generator/JTypeWrapper.java diff --git a/annotations/src/main/java/io/serverlessworkflow/annotations/Union.java b/annotations/src/main/java/io/serverlessworkflow/annotations/ExclusiveUnion.java similarity index 96% rename from annotations/src/main/java/io/serverlessworkflow/annotations/Union.java rename to annotations/src/main/java/io/serverlessworkflow/annotations/ExclusiveUnion.java index e6cc4ecb..4e83437b 100644 --- a/annotations/src/main/java/io/serverlessworkflow/annotations/Union.java +++ b/annotations/src/main/java/io/serverlessworkflow/annotations/ExclusiveUnion.java @@ -23,6 +23,6 @@ @Retention(RUNTIME) @Target(ElementType.TYPE) -public @interface Union { +public @interface ExclusiveUnion { Class[] value(); } diff --git a/annotations/src/main/java/io/serverlessworkflow/annotations/InclusiveUnion.java b/annotations/src/main/java/io/serverlessworkflow/annotations/InclusiveUnion.java new file mode 100644 index 00000000..9df8732a --- /dev/null +++ b/annotations/src/main/java/io/serverlessworkflow/annotations/InclusiveUnion.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.annotations; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(ElementType.TYPE) +public @interface InclusiveUnion { + Class[] value(); +} diff --git a/api/src/test/java/io/serverlessworkflow/api/ApiTest.java b/api/src/test/java/io/serverlessworkflow/api/ApiTest.java index ba0aefc3..36d6841e 100644 --- a/api/src/test/java/io/serverlessworkflow/api/ApiTest.java +++ b/api/src/test/java/io/serverlessworkflow/api/ApiTest.java @@ -18,13 +18,20 @@ import static io.serverlessworkflow.api.WorkflowReader.readWorkflowFromClasspath; import static org.assertj.core.api.Assertions.assertThat; +import io.serverlessworkflow.api.types.BearerAuthenticationPolicy; import io.serverlessworkflow.api.types.CallFunction; import io.serverlessworkflow.api.types.CallHTTP; import io.serverlessworkflow.api.types.CallTask; import io.serverlessworkflow.api.types.HTTPArguments; +import io.serverlessworkflow.api.types.OAuth2AutenthicationData; +import io.serverlessworkflow.api.types.OAuth2AutenthicationData.OAuth2AutenthicationDataGrant; +import io.serverlessworkflow.api.types.OAuth2AuthenticationPolicy; +import io.serverlessworkflow.api.types.OAuth2AuthenticationPolicyConfiguration; +import io.serverlessworkflow.api.types.OAuth2AuthenticationPropertiesEndpoints; import io.serverlessworkflow.api.types.Task; import io.serverlessworkflow.api.types.Workflow; import java.io.IOException; +import java.net.URI; import org.junit.jupiter.api.Test; public class ApiTest { @@ -66,11 +73,69 @@ void testCallFunctionAPIWithoutArguments() throws IOException { CallTask callTask = task.getCallTask(); assertThat(callTask).isNotNull(); assertThat(callTask.get()).isInstanceOf(CallFunction.class); - if (callTask.get() instanceof CallFunction) { - CallFunction functionCall = callTask.getCallFunction(); - assertThat(functionCall).isNotNull(); - assertThat(callTask.getCallAsyncAPI()).isNull(); - assertThat(functionCall.getWith()).isNull(); - } + CallFunction functionCall = callTask.getCallFunction(); + assertThat(functionCall).isNotNull(); + assertThat(callTask.getCallAsyncAPI()).isNull(); + assertThat(functionCall.getWith()).isNull(); + } + + @Test + void testOauth2Auth() throws IOException { + Workflow workflow = readWorkflowFromClasspath("features/authentication-oauth2.yaml"); + assertThat(workflow.getDo()).isNotEmpty(); + assertThat(workflow.getDo().get(0).getName()).isNotNull(); + assertThat(workflow.getDo().get(0).getTask()).isNotNull(); + Task task = workflow.getDo().get(0).getTask(); + CallTask callTask = task.getCallTask(); + assertThat(callTask).isNotNull(); + assertThat(callTask.get()).isInstanceOf(CallHTTP.class); + CallHTTP httpCall = callTask.getCallHTTP(); + OAuth2AuthenticationPolicy oauthPolicy = + httpCall + .getWith() + .getEndpoint() + .getEndpointConfiguration() + .getAuthentication() + .getAuthenticationPolicy() + .getOAuth2AuthenticationPolicy(); + assertThat(oauthPolicy).isNotNull(); + OAuth2AuthenticationPolicyConfiguration oauth2Props = + oauthPolicy.getOauth2().getOAuth2ConnectAuthenticationProperties(); + assertThat(oauth2Props).isNotNull(); + OAuth2AuthenticationPropertiesEndpoints endpoints = + oauth2Props.getOAuth2ConnectAuthenticationProperties().getEndpoints(); + assertThat(endpoints.getToken()).isEqualTo("/auth/token"); + assertThat(endpoints.getIntrospection()).isEqualTo("/auth/introspect"); + + OAuth2AutenthicationData oauth2Data = oauth2Props.getOAuth2AutenthicationData(); + assertThat(oauth2Data.getAuthority().getLiteralUri()) + .isEqualTo(URI.create("http://keycloak/realms/fake-authority")); + assertThat(oauth2Data.getGrant()).isEqualTo(OAuth2AutenthicationDataGrant.CLIENT_CREDENTIALS); + assertThat(oauth2Data.getClient().getId()).isEqualTo("workflow-runtime-id"); + assertThat(oauth2Data.getClient().getSecret()).isEqualTo("workflow-runtime-secret"); + } + + @Test + void testBearerAuth() throws IOException { + Workflow workflow = readWorkflowFromClasspath("features/authentication-bearer.yaml"); + assertThat(workflow.getDo()).isNotEmpty(); + assertThat(workflow.getDo().get(0).getName()).isNotNull(); + assertThat(workflow.getDo().get(0).getTask()).isNotNull(); + Task task = workflow.getDo().get(0).getTask(); + CallTask callTask = task.getCallTask(); + assertThat(callTask).isNotNull(); + assertThat(callTask.get()).isInstanceOf(CallHTTP.class); + CallHTTP httpCall = callTask.getCallHTTP(); + BearerAuthenticationPolicy bearerPolicy = + httpCall + .getWith() + .getEndpoint() + .getEndpointConfiguration() + .getAuthentication() + .getAuthenticationPolicy() + .getBearerAuthenticationPolicy(); + assertThat(bearerPolicy).isNotNull(); + assertThat(bearerPolicy.getBearer().getBearerAuthenticationProperties().getToken()) + .isEqualTo("${ .token }"); } } diff --git a/generators/jackson/src/main/java/io/serverlessworkflow/generator/jackson/JacksonMixInPojo.java b/generators/jackson/src/main/java/io/serverlessworkflow/generator/jackson/JacksonMixInPojo.java index dc84e9ed..1b633a42 100644 --- a/generators/jackson/src/main/java/io/serverlessworkflow/generator/jackson/JacksonMixInPojo.java +++ b/generators/jackson/src/main/java/io/serverlessworkflow/generator/jackson/JacksonMixInPojo.java @@ -19,6 +19,8 @@ import com.fasterxml.jackson.annotation.JsonAnySetter; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonUnwrapped; import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.databind.Module.SetupContext; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @@ -43,10 +45,11 @@ import io.github.classgraph.TypeArgument; import io.github.classgraph.TypeSignature; import io.serverlessworkflow.annotations.AdditionalProperties; +import io.serverlessworkflow.annotations.ExclusiveUnion; +import io.serverlessworkflow.annotations.InclusiveUnion; import io.serverlessworkflow.annotations.Item; import io.serverlessworkflow.annotations.ItemKey; import io.serverlessworkflow.annotations.ItemValue; -import io.serverlessworkflow.annotations.Union; import java.io.File; import java.io.IOException; import java.io.PrintStream; @@ -110,7 +113,8 @@ public void execute() throws MojoExecutionException, MojoFailureException { ._class("JacksonMixInModule") ._extends(SimpleModule.class) .method(JMod.PUBLIC, codeModel.VOID, SETUP_METHOD); - processAnnotatedClasses(result, Union.class, this::buildUnionMixIn); + processAnnotatedClasses(result, ExclusiveUnion.class, this::buildExclusiveUnionMixIn); + processAnnotatedClasses(result, InclusiveUnion.class, this::buildInclusiveUnionMixIn); processAnnotatedClasses(result, AdditionalProperties.class, this::buildAdditionalPropsMixIn); processAnnotatedClasses(result, Item.class, this::buildItemMixIn); processAnnotatedClasses(result.getAllEnums(), this::buildEnumMixIn); @@ -188,7 +192,7 @@ private void buildItemMixIn(ClassInfo classInfo, JDefinedClass mixClass) GeneratorUtils.generateDeserializer(rootPackage, relClass, getReturnType(valueMethod))); } - private void buildUnionMixIn(ClassInfo unionClassInfo, JDefinedClass unionMixClass) + private void buildExclusiveUnionMixIn(ClassInfo unionClassInfo, JDefinedClass unionMixClass) throws JClassAlreadyExistsException { JClass unionClass = codeModel.ref(unionClassInfo.getName()); unionMixClass @@ -199,13 +203,25 @@ private void buildUnionMixIn(ClassInfo unionClassInfo, JDefinedClass unionMixCla .param( "using", GeneratorUtils.generateDeserializer( - rootPackage, unionClass, getUnionClasses(unionClassInfo))); + rootPackage, unionClass, getUnionClasses(ExclusiveUnion.class, unionClassInfo))); + } + + private void buildInclusiveUnionMixIn(ClassInfo unionClassInfo, JDefinedClass unionMixClass) + throws JClassAlreadyExistsException { + Collection unionClasses = getUnionClasses(InclusiveUnion.class, unionClassInfo); + for (MethodInfo methodInfo : unionClassInfo.getMethodInfo()) { + JClass typeClass = getReturnType(methodInfo); + if (unionClasses.contains(typeClass)) { + JMethod method = unionMixClass.method(JMod.ABSTRACT, typeClass, methodInfo.getName()); + method.annotate(JsonUnwrapped.class); + method.annotate(JsonIgnoreProperties.class).param("ignoreUnknown", true); + } + } } private void buildEnumMixIn(ClassInfo classInfo, JDefinedClass mixClass) throws JClassAlreadyExistsException { mixClass.method(JMod.ABSTRACT, String.class, "value").annotate(JsonValue.class); - JMethod staticMethod = mixClass.method(JMod.STATIC, codeModel.ref(classInfo.getName()), "fromValue"); staticMethod.param(String.class, "value"); @@ -217,8 +233,9 @@ private JDefinedClass createMixInClass(ClassInfo classInfo) throws JClassAlready return rootPackage._class(JMod.ABSTRACT, classInfo.getSimpleName() + "MixIn"); } - private Collection getUnionClasses(ClassInfo unionClassInfo) { - AnnotationInfo info = unionClassInfo.getAnnotationInfoRepeatable(Union.class).get(0); + private Collection getUnionClasses( + Class annotation, ClassInfo unionClassInfo) { + AnnotationInfo info = unionClassInfo.getAnnotationInfoRepeatable(annotation).get(0); Object[] unionClasses = (Object[]) info.getParameterValues().getValue("value"); return Stream.of(unionClasses) .map(AnnotationClassRef.class::cast) diff --git a/generators/types/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java b/generators/types/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java index a9823ce6..35fa64e3 100644 --- a/generators/types/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java +++ b/generators/types/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; -import com.sun.codemodel.JAnnotationArrayMember; import com.sun.codemodel.JBlock; import com.sun.codemodel.JClass; import com.sun.codemodel.JClassAlreadyExistsException; @@ -32,9 +31,10 @@ import com.sun.codemodel.JPackage; import com.sun.codemodel.JType; import com.sun.codemodel.JVar; +import io.serverlessworkflow.annotations.ExclusiveUnion; +import io.serverlessworkflow.annotations.InclusiveUnion; import io.serverlessworkflow.annotations.OneOfSetter; import io.serverlessworkflow.annotations.OneOfValueProvider; -import io.serverlessworkflow.annotations.Union; import jakarta.validation.ConstraintViolationException; import java.io.UnsupportedEncodingException; import java.net.URI; @@ -62,9 +62,9 @@ class AllAnyOneOfSchemaRule extends SchemaRule { this.ruleFactory = ruleFactory; } - private static final String REF = "$ref"; + static final String REF = "$ref"; private static final String TITLE = "title"; - private static final String PATTERN = "pattern"; + static final String PATTERN = "pattern"; private enum Format { URI_TEMPLATE("^[A-Za-z][A-Za-z0-9+\\-.]*://.*"); @@ -91,46 +91,6 @@ String pattern() { } } - private static class JTypeWrapper implements Comparable { - - private final JType type; - private final JsonNode node; - - public JTypeWrapper(JType type, JsonNode node) { - this.type = type; - this.node = node; - } - - public JType getType() { - return type; - } - - public JsonNode getNode() { - return node; - } - - @Override - public int compareTo(JTypeWrapper other) { - return typeToNumber() - other.typeToNumber(); - } - - private int typeToNumber() { - if (type.name().equals("Object")) { - return 6; - } else if (type.name().equals("String")) { - return node.has(PATTERN) || node.has(REF) ? 4 : 5; - } else if (type.isPrimitive()) { - return 3; - } else if (type.isReference()) { - return 2; - } else if (type.isArray()) { - return 1; - } else { - return 0; - } - } - } - @Override public JType apply( String nodeName, @@ -303,6 +263,9 @@ private Schema childSchema(Schema parentSchema, String prefix, int pos) { private JDefinedClass populateAllOf( Schema parentSchema, JDefinedClass definedClass, Collection allOfTypes) { + if (!allOfTypes.isEmpty()) { + GeneratorUtils.annotateCollection(definedClass, InclusiveUnion.class, allOfTypes); + } return wrapAll(parentSchema, definedClass, Optional.empty(), allOfTypes, Optional.empty()); } @@ -321,8 +284,7 @@ private JDefinedClass populateOneOf( definedClass._implements( definedClass.owner().ref(OneOfValueProvider.class).narrow(valueField.type())); GeneratorUtils.implementInterface(definedClass, valueField); - JAnnotationArrayMember unionAnnotation = definedClass.annotate(Union.class).paramArray("value"); - oneOfTypes.forEach(t -> unionAnnotation.param(t.getType())); + GeneratorUtils.annotateCollection(definedClass, ExclusiveUnion.class, oneOfTypes); return wrapAll(parentSchema, definedClass, commonType, oneOfTypes, Optional.of(valueField)); } diff --git a/generators/types/src/main/java/io/serverlessworkflow/generator/GeneratorUtils.java b/generators/types/src/main/java/io/serverlessworkflow/generator/GeneratorUtils.java index abcf40eb..5059dcf4 100644 --- a/generators/types/src/main/java/io/serverlessworkflow/generator/GeneratorUtils.java +++ b/generators/types/src/main/java/io/serverlessworkflow/generator/GeneratorUtils.java @@ -15,10 +15,13 @@ */ package io.serverlessworkflow.generator; +import com.sun.codemodel.JAnnotationArrayMember; import com.sun.codemodel.JDefinedClass; import com.sun.codemodel.JFieldVar; import com.sun.codemodel.JMethod; import com.sun.codemodel.JMod; +import java.lang.annotation.Annotation; +import java.util.Collection; import org.jsonschema2pojo.util.NameHelper; public class GeneratorUtils { @@ -41,5 +44,13 @@ public static JMethod getterMethod( return method; } + public static void annotateCollection( + JDefinedClass definedClass, + Class annotation, + Collection types) { + JAnnotationArrayMember unionAnnotation = definedClass.annotate(annotation).paramArray("value"); + types.forEach(t -> unionAnnotation.param(t.getType())); + } + private GeneratorUtils() {} } diff --git a/generators/types/src/main/java/io/serverlessworkflow/generator/JTypeWrapper.java b/generators/types/src/main/java/io/serverlessworkflow/generator/JTypeWrapper.java new file mode 100644 index 00000000..811a5995 --- /dev/null +++ b/generators/types/src/main/java/io/serverlessworkflow/generator/JTypeWrapper.java @@ -0,0 +1,62 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.generator; + +import static io.serverlessworkflow.generator.AllAnyOneOfSchemaRule.PATTERN; +import static io.serverlessworkflow.generator.AllAnyOneOfSchemaRule.REF; + +import com.fasterxml.jackson.databind.JsonNode; +import com.sun.codemodel.JType; + +class JTypeWrapper implements Comparable { + + private final JType type; + private final JsonNode node; + + public JTypeWrapper(JType type, JsonNode node) { + this.type = type; + this.node = node; + } + + public JType getType() { + return type; + } + + public JsonNode getNode() { + return node; + } + + @Override + public int compareTo(JTypeWrapper other) { + return typeToNumber() - other.typeToNumber(); + } + + private int typeToNumber() { + if (type.name().equals("Object")) { + return 6; + } else if (type.name().equals("String")) { + return node.has(PATTERN) || node.has(REF) ? 4 : 5; + } else if (type.isPrimitive()) { + return 3; + } else if (type.isReference()) { + return 2; + } else if (type.isArray()) { + return 1; + } else { + return 0; + } + } +}