diff --git a/api/pom.xml b/api/pom.xml index 2b08c827..19414b85 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -14,29 +14,28 @@ - org.slf4j - slf4j-api + io.serverlessworkflow + serverlessworkflow-types + ${project.version} - com.fasterxml.jackson.core - jackson-core + io.serverlessworkflow + serverlessworkflow-serialization + ${project.version} - com.networknt - json-schema-validator + org.slf4j + slf4j-api - com.fasterxml.jackson.core - jackson-databind + com.networknt + json-schema-validator com.fasterxml.jackson.dataformat jackson-dataformat-yaml - - jakarta.validation - jakarta.validation-api - + org.hibernate.validator hibernate-validator @@ -79,54 +78,46 @@ test - - - - - org.jsonschema2pojo - jsonschema2pojo-maven-plugin + + + + io.serverlessworkflow + jackson-generator + ${project.version} + + + io.serverlessworkflow.api.types + + + + + generate + + generate-sources + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.3.0 + + + add-mixin + generate-sources + + add-source + - ${basedir}/src/main/resources/schema - - - yamlschema - io.serverlessworkflow.api.types - ${project.build.directory}/generated-sources/src/main/java - true - true - true - true - false - false - true - true - true - true - ${java.version} - true - true - io.serverlessworkflow.generator.UnreferencedFactory - io.serverlessworkflow.generator.ConstAnnotator + + ${project.build.directory}/generated-sources/jacksonmixinpojo + - - - io.serverlessworkflow - serverless-workflow-custom-generator - ${project.version} - - - - - - generate - - generate-sources - - - - - - + + + + + + diff --git a/api/src/main/java/io/serverlessworkflow/api/ObjectMapperFactory.java b/api/src/main/java/io/serverlessworkflow/api/ObjectMapperFactory.java index c8211586..78b1e24e 100644 --- a/api/src/main/java/io/serverlessworkflow/api/ObjectMapperFactory.java +++ b/api/src/main/java/io/serverlessworkflow/api/ObjectMapperFactory.java @@ -15,11 +15,13 @@ */ package io.serverlessworkflow.api; +import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature; +import io.serverlessworkflow.api.types.JacksonMixInModule; import io.serverlessworkflow.serialization.BeanDeserializerModifierWithValidation; import io.serverlessworkflow.serialization.URIDeserializer; import io.serverlessworkflow.serialization.URISerializer; @@ -47,10 +49,12 @@ private static ObjectMapper configure(ObjectMapper mapper) { validationModule.setDeserializerModifier(new BeanDeserializerModifierWithValidation()); return mapper + .setSerializationInclusion(Include.NON_NULL) .configure(SerializationFeature.INDENT_OUTPUT, true) .configure(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS, false) .configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false) - .registerModule(validationModule); + .registerModule(validationModule) + .registerModule(new JacksonMixInModule()); } private ObjectMapperFactory() {} diff --git a/custom-generator/pom.xml b/custom-generator/pom.xml index 40bf6b11..55b2215b 100644 --- a/custom-generator/pom.xml +++ b/custom-generator/pom.xml @@ -12,6 +12,12 @@ org.jsonschema2pojo jsonschema2pojo-core + + io.serverlessworkflow + serverlessworkflow-annotations + ${project.version} + + diff --git a/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java b/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java index 28a611cb..a9823ce6 100644 --- a/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java +++ b/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java @@ -16,9 +16,8 @@ package io.serverlessworkflow.generator; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; 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; @@ -33,6 +32,9 @@ import com.sun.codemodel.JPackage; import com.sun.codemodel.JType; import com.sun.codemodel.JVar; +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; @@ -316,28 +318,11 @@ private JDefinedClass populateOneOf( commonType.orElse(definedClass.owner().ref(Object.class)), ruleFactory.getNameHelper().getPropertyName("value", null), null); - definedClass._implements( - definedClass - .owner() - .ref(GeneratorUtils.ONE_OF_VALUE_PROVIDER_INTERFACE_NAME) - .narrow(valueField.type())); + definedClass.owner().ref(OneOfValueProvider.class).narrow(valueField.type())); GeneratorUtils.implementInterface(definedClass, valueField); - try { - JDefinedClass serializer = generateSerializer(definedClass); - definedClass.annotate(JsonSerialize.class).param("using", serializer); - } catch (JClassAlreadyExistsException ex) { - // already serialized aware - } - - try { - JDefinedClass deserializer = - generateDeserializer(definedClass, oneOfTypes, "deserializeOneOf"); - definedClass.annotate(JsonDeserialize.class).param("using", deserializer); - } catch (JClassAlreadyExistsException ex) { - // already deserialized aware - } - + JAnnotationArrayMember unionAnnotation = definedClass.annotate(Union.class).paramArray("value"); + oneOfTypes.forEach(t -> unionAnnotation.param(t.getType())); return wrapAll(parentSchema, definedClass, commonType, oneOfTypes, Optional.of(valueField)); } @@ -388,51 +373,6 @@ private static boolean isStringType(JType type) { return type.name().equals("String"); } - private JDefinedClass generateSerializer(JDefinedClass relatedClass) - throws JClassAlreadyExistsException { - JDefinedClass definedClass = GeneratorUtils.serializerClass(relatedClass); - GeneratorUtils.fillSerializer( - definedClass, - relatedClass, - (method, valueParam, genParam) -> - method - .body() - .staticInvoke( - definedClass.owner().ref(GeneratorUtils.SERIALIZE_HELPER_NAME), - "serializeOneOf") - .arg(genParam) - .arg(valueParam)); - return definedClass; - } - - private JDefinedClass generateDeserializer( - JDefinedClass relatedClass, Collection oneOfTypes, String methodName) - throws JClassAlreadyExistsException { - JDefinedClass definedClass = GeneratorUtils.deserializerClass(relatedClass); - GeneratorUtils.fillDeserializer( - definedClass, - relatedClass, - (method, parserParam) -> { - JBlock body = method.body(); - - body._return( - definedClass - .owner() - .ref(GeneratorUtils.DESERIALIZE_HELPER_NAME) - .staticInvoke(methodName) - .arg(parserParam) - .arg(relatedClass.dotclass()) - .arg(list(definedClass, oneOfTypes))); - }); - return definedClass; - } - - private JInvocation list(JDefinedClass definedClass, Collection list) { - JInvocation result = definedClass.owner().ref(List.class).staticInvoke("of"); - list.forEach(c -> result.arg(((JClass) c.getType()).dotclass())); - return result; - } - private void wrapIt( Schema parentSchema, JDefinedClass definedClass, @@ -460,7 +400,7 @@ private JVar setupMethod( v -> { method.body().assign(JExpr._this().ref(v), methodParam); method - .annotate(definedClass.owner().ref(GeneratorUtils.SETTER_ANNOTATION_NAME)) + .annotate(definedClass.owner().ref(OneOfSetter.class)) .param("value", instanceField.type()); }); return methodParam; diff --git a/custom-generator/src/main/java/io/serverlessworkflow/generator/ConstAnnotator.java b/custom-generator/src/main/java/io/serverlessworkflow/generator/CustomAnnotator.java similarity index 78% rename from custom-generator/src/main/java/io/serverlessworkflow/generator/ConstAnnotator.java rename to custom-generator/src/main/java/io/serverlessworkflow/generator/CustomAnnotator.java index a893cfb5..657ba3ce 100644 --- a/custom-generator/src/main/java/io/serverlessworkflow/generator/ConstAnnotator.java +++ b/custom-generator/src/main/java/io/serverlessworkflow/generator/CustomAnnotator.java @@ -18,18 +18,24 @@ import com.fasterxml.jackson.databind.JsonNode; import com.sun.codemodel.JDefinedClass; import com.sun.codemodel.JFieldVar; +import io.serverlessworkflow.annotations.AdditionalProperties; import jakarta.validation.constraints.Pattern; import org.jsonschema2pojo.AbstractAnnotator; import org.jsonschema2pojo.GenerationConfig; -public class ConstAnnotator extends AbstractAnnotator { +public class CustomAnnotator extends AbstractAnnotator { private static final String CONST = "const"; - public ConstAnnotator(GenerationConfig generationConfig) { + public CustomAnnotator(GenerationConfig generationConfig) { super(generationConfig); } + @Override + public void additionalPropertiesField(JFieldVar field, JDefinedClass clazz, String propertyName) { + clazz.annotate(AdditionalProperties.class); + } + @Override public void propertyField( JFieldVar field, JDefinedClass clazz, String propertyName, JsonNode propertyNode) { diff --git a/custom-generator/src/main/java/io/serverlessworkflow/generator/GeneratorUtils.java b/custom-generator/src/main/java/io/serverlessworkflow/generator/GeneratorUtils.java index e7af60d5..abcf40eb 100644 --- a/custom-generator/src/main/java/io/serverlessworkflow/generator/GeneratorUtils.java +++ b/custom-generator/src/main/java/io/serverlessworkflow/generator/GeneratorUtils.java @@ -15,52 +15,14 @@ */ package io.serverlessworkflow.generator; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.sun.codemodel.JClassAlreadyExistsException; import com.sun.codemodel.JDefinedClass; import com.sun.codemodel.JFieldVar; import com.sun.codemodel.JMethod; import com.sun.codemodel.JMod; -import com.sun.codemodel.JVar; -import java.io.IOException; import org.jsonschema2pojo.util.NameHelper; public class GeneratorUtils { - public static final String SERIALIZE_HELPER_NAME = - "io.serverlessworkflow.serialization.SerializeHelper"; - public static final String DESERIALIZE_HELPER_NAME = - "io.serverlessworkflow.serialization.DeserializeHelper"; - public static final String ONE_OF_VALUE_PROVIDER_INTERFACE_NAME = - "io.serverlessworkflow.api.OneOfValueProvider"; - public static final String SETTER_ANNOTATION_NAME = - "io.serverlessworkflow.serialization.OneOfSetter"; - - @FunctionalInterface - public interface SerializerFiller { - void accept(JMethod method, JVar valueParam, JVar genParam); - } - - @FunctionalInterface - public interface DeserializerFiller { - void accept(JMethod method, JVar parserParam); - } - - public static JDefinedClass serializerClass(JDefinedClass relatedClass) - throws JClassAlreadyExistsException { - return createClass(relatedClass, JsonSerializer.class, "Serializer"); - } - - public static JDefinedClass deserializerClass(JDefinedClass relatedClass) - throws JClassAlreadyExistsException { - return createClass(relatedClass, JsonDeserializer.class, "Deserializer"); - } - public static JMethod implementInterface(JDefinedClass definedClass, JFieldVar valueField) { JMethod method = definedClass.method(JMod.PUBLIC, valueField.type(), "get"); method.annotate(Override.class); @@ -79,35 +41,5 @@ public static JMethod getterMethod( return method; } - public static void fillSerializer( - JDefinedClass definedClass, JDefinedClass relatedClass, SerializerFiller filler) { - JMethod method = definedClass.method(JMod.PUBLIC, void.class, "serialize"); - method.annotate(Override.class); - method._throws(IOException.class); - JVar valueParam = method.param(relatedClass, "value"); - JVar genParam = method.param(JsonGenerator.class, "gen"); - method.param(SerializerProvider.class, "serializers"); - filler.accept(method, valueParam, genParam); - } - - public static void fillDeserializer( - JDefinedClass definedClass, JDefinedClass relatedClass, DeserializerFiller filler) { - JMethod method = definedClass.method(JMod.PUBLIC, relatedClass, "deserialize"); - method.annotate(Override.class); - method._throws(IOException.class); - JVar parserParam = method.param(JsonParser.class, "parser"); - method.param(DeserializationContext.class, "dctx"); - filler.accept(method, parserParam); - } - - private static JDefinedClass createClass( - JDefinedClass relatedClass, Class serializerClass, String suffix) - throws JClassAlreadyExistsException { - JDefinedClass definedClass = - relatedClass._package()._class(JMod.NONE, relatedClass.name() + suffix); - definedClass._extends(definedClass.owner().ref(serializerClass).narrow(relatedClass)); - return definedClass; - } - private GeneratorUtils() {} } diff --git a/custom-generator/src/main/java/io/serverlessworkflow/generator/UnevaluatedPropertiesRule.java b/custom-generator/src/main/java/io/serverlessworkflow/generator/UnevaluatedPropertiesRule.java index 18bdbba6..5388a503 100644 --- a/custom-generator/src/main/java/io/serverlessworkflow/generator/UnevaluatedPropertiesRule.java +++ b/custom-generator/src/main/java/io/serverlessworkflow/generator/UnevaluatedPropertiesRule.java @@ -16,10 +16,6 @@ package io.serverlessworkflow.generator; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.sun.codemodel.JBlock; -import com.sun.codemodel.JClass; import com.sun.codemodel.JClassAlreadyExistsException; import com.sun.codemodel.JDefinedClass; import com.sun.codemodel.JExpr; @@ -27,6 +23,9 @@ import com.sun.codemodel.JMethod; import com.sun.codemodel.JMod; import com.sun.codemodel.JType; +import io.serverlessworkflow.annotations.Item; +import io.serverlessworkflow.annotations.ItemKey; +import io.serverlessworkflow.annotations.ItemValue; import org.jsonschema2pojo.Schema; import org.jsonschema2pojo.rules.AdditionalPropertiesRule; import org.jsonschema2pojo.rules.Rule; @@ -99,12 +98,10 @@ private JDefinedClass addKeyValueFields( JMod.PRIVATE, propertyType, nameHelper.getPropertyName(propertyType.name(), null)); JMethod valueMethod = GeneratorUtils.getterMethod(jclass, valueField, nameHelper, propertyType.name()); - jclass - .annotate(JsonSerialize.class) - .param("using", generateSerializer(jclass, nameMethod, valueMethod)); - jclass - .annotate(JsonDeserialize.class) - .param("using", generateDeserializer(jclass, propertyType)); + + jclass.annotate(Item.class); + nameMethod.annotate(ItemKey.class); + valueMethod.annotate(ItemValue.class); JMethod constructor = jclass.constructor(JMod.PUBLIC); constructor .body() @@ -113,44 +110,6 @@ private JDefinedClass addKeyValueFields( return jclass; } - private JDefinedClass generateDeserializer(JDefinedClass relatedClass, JType propertyType) - throws JClassAlreadyExistsException { - JDefinedClass definedClass = GeneratorUtils.deserializerClass(relatedClass); - GeneratorUtils.fillDeserializer( - definedClass, - relatedClass, - (method, parserParam) -> - method - .body() - ._return( - definedClass - .owner() - .ref(GeneratorUtils.DESERIALIZE_HELPER_NAME) - .staticInvoke("deserializeItem") - .arg(parserParam) - .arg(relatedClass.dotclass()) - .arg(((JClass) propertyType).dotclass()))); - return definedClass; - } - - private JDefinedClass generateSerializer( - JDefinedClass relatedClass, JMethod nameMethod, JMethod valueMethod) - throws JClassAlreadyExistsException { - JDefinedClass definedClass = GeneratorUtils.serializerClass(relatedClass); - GeneratorUtils.fillSerializer( - definedClass, - relatedClass, - (method, valueParam, genParam) -> { - JBlock body = method.body(); - body.invoke(genParam, "writeStartObject"); - body.invoke(genParam, "writeObjectField") - .arg(valueParam.invoke(nameMethod)) - .arg(valueParam.invoke(valueMethod)); - body.invoke(genParam, "writeEndObject"); - }); - return definedClass; - } - private boolean checkIntValue(JsonNode node, String propName, int value) { return node.has(propName) && node.get(propName).asInt() == value; } diff --git a/jackson-generator/pom.xml b/jackson-generator/pom.xml new file mode 100644 index 00000000..55d2e03c --- /dev/null +++ b/jackson-generator/pom.xml @@ -0,0 +1,80 @@ + + 4.0.0 + maven-plugin + + io.serverlessworkflow + serverlessworkflow-parent + 8.0.0-SNAPSHOT + + jackson-generator + + 3.15.1 + + + + + + org.apache.maven.plugins + maven-plugin-plugin + ${maven-plugin-tools.version} + + jackson-generator + + + + help-mojo + + + helpmojo + + + + + + + + + + + + org.apache.maven.plugins + maven-plugin-report-plugin + ${maven-plugin-tools.version} + + + + + + org.apache.maven + maven-plugin-api + ${version.maven} + provided + + + com.sun.codemodel + codemodel + 2.6 + + + io.github.classgraph + classgraph + 4.8.180 + + + org.apache.maven.plugin-tools + maven-plugin-annotations + ${maven-plugin-tools.version} + provided + + + io.serverlessworkflow + serverlessworkflow-types + ${project.version} + + + io.serverlessworkflow + serverlessworkflow-serialization + ${project.version} + + + \ No newline at end of file diff --git a/jackson-generator/src/main/java/io/serverlessworkflow/generator/jackson/GeneratorUtils.java b/jackson-generator/src/main/java/io/serverlessworkflow/generator/jackson/GeneratorUtils.java new file mode 100644 index 00000000..9463106f --- /dev/null +++ b/jackson-generator/src/main/java/io/serverlessworkflow/generator/jackson/GeneratorUtils.java @@ -0,0 +1,172 @@ +/* + * 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.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.sun.codemodel.JBlock; +import com.sun.codemodel.JClass; +import com.sun.codemodel.JClassAlreadyExistsException; +import com.sun.codemodel.JDefinedClass; +import com.sun.codemodel.JInvocation; +import com.sun.codemodel.JMethod; +import com.sun.codemodel.JMod; +import com.sun.codemodel.JType; +import com.sun.codemodel.JVar; +import io.serverlessworkflow.serialization.DeserializeHelper; +import io.serverlessworkflow.serialization.SerializeHelper; +import java.io.IOException; +import java.util.Collection; +import java.util.List; + +public class GeneratorUtils { + + @FunctionalInterface + public interface SerializerFiller { + void accept(JMethod method, JVar valueParam, JVar genParam); + } + + @FunctionalInterface + public interface DeserializerFiller { + void accept(JMethod method, JVar parserParam); + } + + public static JDefinedClass serializerClass(JClass relatedClass) + throws JClassAlreadyExistsException { + return createClass(relatedClass, JsonSerializer.class, "Serializer"); + } + + public static JDefinedClass deserializerClass(JClass relatedClass) + throws JClassAlreadyExistsException { + return createClass(relatedClass, JsonDeserializer.class, "Deserializer"); + } + + public static void fillSerializer( + JDefinedClass definedClass, JClass relatedClass, SerializerFiller filler) { + JMethod method = definedClass.method(JMod.PUBLIC, void.class, "serialize"); + method.annotate(Override.class); + method._throws(IOException.class); + JVar valueParam = method.param(relatedClass, "value"); + JVar genParam = method.param(JsonGenerator.class, "gen"); + method.param(SerializerProvider.class, "serializers"); + filler.accept(method, valueParam, genParam); + } + + public static void fillDeserializer( + JDefinedClass definedClass, JClass relatedClass, DeserializerFiller filler) { + JMethod method = definedClass.method(JMod.PUBLIC, relatedClass, "deserialize"); + method.annotate(Override.class); + method._throws(IOException.class); + JVar parserParam = method.param(JsonParser.class, "parser"); + method.param(DeserializationContext.class, "dctx"); + filler.accept(method, parserParam); + } + + private static JDefinedClass createClass( + JClass relatedClass, Class serializerClass, String suffix) + throws JClassAlreadyExistsException { + JDefinedClass definedClass = + relatedClass._package()._class(JMod.NONE, relatedClass.name() + suffix); + definedClass._extends(definedClass.owner().ref(serializerClass).narrow(relatedClass)); + return definedClass; + } + + public static JDefinedClass generateSerializer(JClass relatedClass) + throws JClassAlreadyExistsException { + JDefinedClass definedClass = GeneratorUtils.serializerClass(relatedClass); + GeneratorUtils.fillSerializer( + definedClass, + relatedClass, + (method, valueParam, genParam) -> + method + .body() + .staticInvoke(definedClass.owner().ref(SerializeHelper.class), "serializeOneOf") + .arg(genParam) + .arg(valueParam)); + return definedClass; + } + + public static JDefinedClass generateDeserializer( + JClass relatedClass, Collection oneOfTypes) throws JClassAlreadyExistsException { + JDefinedClass definedClass = GeneratorUtils.deserializerClass(relatedClass); + GeneratorUtils.fillDeserializer( + definedClass, + relatedClass, + (method, parserParam) -> { + JBlock body = method.body(); + + body._return( + definedClass + .owner() + .ref(DeserializeHelper.class) + .staticInvoke("deserializeOneOf") + .arg(parserParam) + .arg(relatedClass.dotclass()) + .arg(list(definedClass, oneOfTypes))); + }); + return definedClass; + } + + public static JDefinedClass generateDeserializer(JClass relatedClass, JType propertyType) + throws JClassAlreadyExistsException { + JDefinedClass definedClass = GeneratorUtils.deserializerClass(relatedClass); + GeneratorUtils.fillDeserializer( + definedClass, + relatedClass, + (method, parserParam) -> + method + .body() + ._return( + definedClass + .owner() + .ref(DeserializeHelper.class) + .staticInvoke("deserializeItem") + .arg(parserParam) + .arg(relatedClass.dotclass()) + .arg(((JClass) propertyType).dotclass()))); + return definedClass; + } + + public static JDefinedClass generateSerializer( + JClass relatedClass, String keyMethod, String valueMethod) + throws JClassAlreadyExistsException { + JDefinedClass definedClass = GeneratorUtils.serializerClass(relatedClass); + GeneratorUtils.fillSerializer( + definedClass, + relatedClass, + (method, valueParam, genParam) -> { + JBlock body = method.body(); + body.invoke(genParam, "writeStartObject"); + body.invoke(genParam, "writeObjectField") + .arg(valueParam.invoke(keyMethod)) + .arg(valueParam.invoke(valueMethod)); + body.invoke(genParam, "writeEndObject"); + }); + return definedClass; + } + + private static JInvocation list(JDefinedClass definedClass, Collection list) { + JInvocation result = definedClass.owner().ref(List.class).staticInvoke("of"); + list.forEach(c -> result.arg(c.dotclass())); + return result; + } + + private GeneratorUtils() {} +} diff --git a/jackson-generator/src/main/java/io/serverlessworkflow/generator/jackson/JacksonMixInPojo.java b/jackson-generator/src/main/java/io/serverlessworkflow/generator/jackson/JacksonMixInPojo.java new file mode 100644 index 00000000..b80238ed --- /dev/null +++ b/jackson-generator/src/main/java/io/serverlessworkflow/generator/jackson/JacksonMixInPojo.java @@ -0,0 +1,245 @@ +/* + * 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.jackson; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.databind.Module.SetupContext; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.sun.codemodel.JClass; +import com.sun.codemodel.JClassAlreadyExistsException; +import com.sun.codemodel.JCodeModel; +import com.sun.codemodel.JDefinedClass; +import com.sun.codemodel.JExpr; +import com.sun.codemodel.JExpression; +import com.sun.codemodel.JMethod; +import com.sun.codemodel.JMod; +import com.sun.codemodel.JPackage; +import io.github.classgraph.AnnotationClassRef; +import io.github.classgraph.AnnotationInfo; +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ClassRefTypeSignature; +import io.github.classgraph.MethodInfo; +import io.github.classgraph.ScanResult; +import io.github.classgraph.TypeArgument; +import io.github.classgraph.TypeSignature; +import io.serverlessworkflow.annotations.AdditionalProperties; +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; +import java.lang.annotation.Annotation; +import java.nio.file.Files; +import java.util.Collection; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugin.MojoFailureException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; + +@Mojo( + name = "generate", + defaultPhase = LifecyclePhase.GENERATE_SOURCES, + requiresDependencyResolution = ResolutionScope.COMPILE, + threadSafe = true) +public class JacksonMixInPojo extends AbstractMojo { + + @Parameter( + property = "jacksonmixinpojo.outputDirectory", + defaultValue = "${project.build.directory}/generated-sources/jacksonmixinpojo") + private File outputDirectory; + + /** + * Package name used for generated Java classes (for types where a fully qualified name has not + * been supplied in the schema using the 'javaType' property). + * + * @since 0.1.0 + */ + @Parameter(property = "jsonschema2pojo.targetPackage") + private String targetPackage = ""; + + private static final String MIXIN_METHOD = "setMixInAnnotation"; + private static final String ADD_PROPERTIES_METHOD = "getAdditionalProperties"; + private static final String SETUP_METHOD = "setupModule"; + private JCodeModel codeModel; + private JPackage rootPackage; + private JMethod setupMethod; + + @FunctionalInterface + interface AnnotationProcessor { + void accept(ClassInfo classInfo, JDefinedClass definedClass) + throws JClassAlreadyExistsException; + } + + @Override + public void execute() throws MojoExecutionException, MojoFailureException { + + try (ScanResult result = + new ClassGraph() + .enableAnnotationInfo() + .enableMethodInfo() + .acceptPackages(targetPackage) + .scan()) { + codeModel = new JCodeModel(); + rootPackage = codeModel._package(targetPackage); + setupMethod = + rootPackage + ._class("JacksonMixInModule") + ._extends(SimpleModule.class) + .method(JMod.PUBLIC, codeModel.VOID, SETUP_METHOD); + processAnnotatedClasses(result, Union.class, this::buildUnionMixIn); + processAnnotatedClasses(result, AdditionalProperties.class, this::buildAdditionalPropsMixIn); + processAnnotatedClasses(result, Item.class, this::buildItemMixIn); + processAnnotatedClasses(result.getAllEnums(), this::buildEnumMixIn); + setupMethod + .body() + .invoke(JExpr._super(), SETUP_METHOD) + .arg(setupMethod.param(SetupContext.class, "context")); + Files.createDirectories(outputDirectory.toPath()); + codeModel.build(outputDirectory, (PrintStream) null); + } catch (JClassAlreadyExistsException | IOException e) { + getLog().error(e); + } + } + + private void processAnnotatedClasses( + Iterable classesInfo, AnnotationProcessor processor) + throws JClassAlreadyExistsException { + for (ClassInfo classInfo : classesInfo) { + setupMethod + .body() + .invoke(JExpr._super(), MIXIN_METHOD) + .arg(JExpr.dotclass(codeModel.ref(classInfo.getName()))) + .arg(processAnnotatedClass(classInfo, processor)); + } + } + + private void processAnnotatedClasses( + ScanResult result, Class annotation, AnnotationProcessor processor) + throws JClassAlreadyExistsException { + processAnnotatedClasses(result.getClassesWithAnnotation(annotation), processor); + } + + private JExpression processAnnotatedClass(ClassInfo classInfo, AnnotationProcessor processor) + throws JClassAlreadyExistsException { + JDefinedClass result = createMixInClass(classInfo); + processor.accept(classInfo, result); + return JExpr.dotclass(result); + } + + private void buildAdditionalPropsMixIn( + ClassInfo addPropsClassInfo, JDefinedClass addPropsMixClass) { + JClass mapStringObject = + getReturnType(addPropsClassInfo.getMethodInfo().getSingleMethod(ADD_PROPERTIES_METHOD)); + JClass objectType = mapStringObject.getTypeParameters().get(1); + addPropsMixClass + .method(JMod.ABSTRACT, mapStringObject, ADD_PROPERTIES_METHOD) + .annotate(JsonAnyGetter.class); + addPropsMixClass + .field(JMod.NONE, mapStringObject, "additionalProperties") + .annotate(JsonIgnore.class); + JMethod setter = + addPropsMixClass.method(JMod.ABSTRACT, codeModel.VOID, "setAdditionalProperty"); + setter.param(String.class, "name"); + setter.param(objectType, "value"); + setter.annotate(JsonAnySetter.class); + } + + private void buildItemMixIn(ClassInfo classInfo, JDefinedClass mixClass) + throws JClassAlreadyExistsException { + JClass relClass = codeModel.ref(classInfo.getName()); + MethodInfo keyMethod = + classInfo.getMethodInfo().filter(m -> m.hasAnnotation(ItemKey.class)).get(0); + MethodInfo valueMethod = + classInfo.getMethodInfo().filter(m -> m.hasAnnotation(ItemValue.class)).get(0); + mixClass + .annotate(JsonSerialize.class) + .param( + "using", + GeneratorUtils.generateSerializer( + relClass, keyMethod.getName(), valueMethod.getName())); + mixClass + .annotate(JsonDeserialize.class) + .param("using", GeneratorUtils.generateDeserializer(relClass, getReturnType(valueMethod))); + } + + private void buildUnionMixIn(ClassInfo unionClassInfo, JDefinedClass unionMixClass) + throws JClassAlreadyExistsException { + JClass unionClass = codeModel.ref(unionClassInfo.getName()); + unionMixClass + .annotate(JsonSerialize.class) + .param("using", GeneratorUtils.generateSerializer(unionClass)); + unionMixClass + .annotate(JsonDeserialize.class) + .param( + "using", + GeneratorUtils.generateDeserializer(unionClass, getUnionClasses(unionClassInfo))); + } + + 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"); + staticMethod.annotate(JsonCreator.class); + staticMethod.body()._return(JExpr._null()); + } + + private JDefinedClass createMixInClass(ClassInfo classInfo) throws JClassAlreadyExistsException { + return rootPackage._class(JMod.ABSTRACT, classInfo.getSimpleName() + "MixIn"); + } + + private Collection getUnionClasses(ClassInfo unionClassInfo) { + AnnotationInfo info = unionClassInfo.getAnnotationInfoRepeatable(Union.class).get(0); + Object[] unionClasses = (Object[]) info.getParameterValues().getValue("value"); + return Stream.of(unionClasses) + .map(AnnotationClassRef.class::cast) + .map(ref -> codeModel.ref(ref.getName())) + .collect(Collectors.toList()); + } + + private JClass getReturnType(MethodInfo info) { + return getReturnType(info.getTypeSignatureOrTypeDescriptor().getResultType()); + } + + private JClass getReturnType(ClassRefTypeSignature refTypeSignature) { + JClass result = codeModel.ref(refTypeSignature.getFullyQualifiedClassName()); + for (TypeArgument t : refTypeSignature.getTypeArguments()) + result = result.narrow(getReturnType(t.getTypeSignature())); + return result; + } + + private JClass getReturnType(TypeSignature t) { + return t instanceof ClassRefTypeSignature refTypeSignature + ? getReturnType(refTypeSignature) + : codeModel.ref(t.toString()); + } +} diff --git a/pom.xml b/pom.xml index a1d961ad..a187fbfe 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,10 @@ api custom-generator impl + serverlessworkflow-types + serverlessworkflow-annotations + jackson-generator + serverlessworkflow-serialization diff --git a/serverlessworkflow-annotations/pom.xml b/serverlessworkflow-annotations/pom.xml new file mode 100644 index 00000000..6e583ab9 --- /dev/null +++ b/serverlessworkflow-annotations/pom.xml @@ -0,0 +1,10 @@ + + 4.0.0 + + io.serverlessworkflow + serverlessworkflow-parent + 8.0.0-SNAPSHOT + + Serverless Workflow :: Annotations + serverlessworkflow-annotations + \ No newline at end of file diff --git a/serverlessworkflow-annotations/src/main/java/io/serverlessworkflow/annotations/AdditionalProperties.java b/serverlessworkflow-annotations/src/main/java/io/serverlessworkflow/annotations/AdditionalProperties.java new file mode 100644 index 00000000..69a3b023 --- /dev/null +++ b/serverlessworkflow-annotations/src/main/java/io/serverlessworkflow/annotations/AdditionalProperties.java @@ -0,0 +1,26 @@ +/* + * 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.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(TYPE) +public @interface AdditionalProperties {} diff --git a/serverlessworkflow-annotations/src/main/java/io/serverlessworkflow/annotations/Item.java b/serverlessworkflow-annotations/src/main/java/io/serverlessworkflow/annotations/Item.java new file mode 100644 index 00000000..f42a86d5 --- /dev/null +++ b/serverlessworkflow-annotations/src/main/java/io/serverlessworkflow/annotations/Item.java @@ -0,0 +1,26 @@ +/* + * 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.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(TYPE) +public @interface Item {} diff --git a/serverlessworkflow-annotations/src/main/java/io/serverlessworkflow/annotations/ItemKey.java b/serverlessworkflow-annotations/src/main/java/io/serverlessworkflow/annotations/ItemKey.java new file mode 100644 index 00000000..ea75094d --- /dev/null +++ b/serverlessworkflow-annotations/src/main/java/io/serverlessworkflow/annotations/ItemKey.java @@ -0,0 +1,26 @@ +/* + * 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.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(METHOD) +public @interface ItemKey {} diff --git a/serverlessworkflow-annotations/src/main/java/io/serverlessworkflow/annotations/ItemValue.java b/serverlessworkflow-annotations/src/main/java/io/serverlessworkflow/annotations/ItemValue.java new file mode 100644 index 00000000..2aaf09e6 --- /dev/null +++ b/serverlessworkflow-annotations/src/main/java/io/serverlessworkflow/annotations/ItemValue.java @@ -0,0 +1,26 @@ +/* + * 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.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(METHOD) +public @interface ItemValue {} diff --git a/api/src/main/java/io/serverlessworkflow/serialization/OneOfSetter.java b/serverlessworkflow-annotations/src/main/java/io/serverlessworkflow/annotations/OneOfSetter.java similarity index 95% rename from api/src/main/java/io/serverlessworkflow/serialization/OneOfSetter.java rename to serverlessworkflow-annotations/src/main/java/io/serverlessworkflow/annotations/OneOfSetter.java index 098df425..d67f0292 100644 --- a/api/src/main/java/io/serverlessworkflow/serialization/OneOfSetter.java +++ b/serverlessworkflow-annotations/src/main/java/io/serverlessworkflow/annotations/OneOfSetter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.serialization; +package io.serverlessworkflow.annotations; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; diff --git a/api/src/main/java/io/serverlessworkflow/api/OneOfValueProvider.java b/serverlessworkflow-annotations/src/main/java/io/serverlessworkflow/annotations/OneOfValueProvider.java similarity index 94% rename from api/src/main/java/io/serverlessworkflow/api/OneOfValueProvider.java rename to serverlessworkflow-annotations/src/main/java/io/serverlessworkflow/annotations/OneOfValueProvider.java index 9d17b872..d275ff9d 100644 --- a/api/src/main/java/io/serverlessworkflow/api/OneOfValueProvider.java +++ b/serverlessworkflow-annotations/src/main/java/io/serverlessworkflow/annotations/OneOfValueProvider.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.api; +package io.serverlessworkflow.annotations; public interface OneOfValueProvider { T get(); diff --git a/serverlessworkflow-annotations/src/main/java/io/serverlessworkflow/annotations/Union.java b/serverlessworkflow-annotations/src/main/java/io/serverlessworkflow/annotations/Union.java new file mode 100644 index 00000000..e6cc4ecb --- /dev/null +++ b/serverlessworkflow-annotations/src/main/java/io/serverlessworkflow/annotations/Union.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 Union { + Class[] value(); +} diff --git a/serverlessworkflow-serialization/pom.xml b/serverlessworkflow-serialization/pom.xml new file mode 100644 index 00000000..55787c00 --- /dev/null +++ b/serverlessworkflow-serialization/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + io.serverlessworkflow + serverlessworkflow-parent + 8.0.0-SNAPSHOT + + serverlessworkflow-serialization + + + io.serverlessworkflow + serverlessworkflow-annotations + ${project.version} + + + com.fasterxml.jackson.core + jackson-databind + + + jakarta.validation + jakarta.validation-api + + + \ No newline at end of file diff --git a/api/src/main/java/io/serverlessworkflow/serialization/DeserializeHelper.java b/serverlessworkflow-serialization/src/main/java/io/serverlessworkflow/serialization/DeserializeHelper.java similarity index 98% rename from api/src/main/java/io/serverlessworkflow/serialization/DeserializeHelper.java rename to serverlessworkflow-serialization/src/main/java/io/serverlessworkflow/serialization/DeserializeHelper.java index cfbd54ca..041474d9 100644 --- a/api/src/main/java/io/serverlessworkflow/serialization/DeserializeHelper.java +++ b/serverlessworkflow-serialization/src/main/java/io/serverlessworkflow/serialization/DeserializeHelper.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.TreeNode; import com.fasterxml.jackson.databind.JsonMappingException; +import io.serverlessworkflow.annotations.OneOfSetter; import jakarta.validation.ConstraintViolationException; import java.io.IOException; import java.lang.reflect.InvocationTargetException; diff --git a/api/src/main/java/io/serverlessworkflow/serialization/SerializeHelper.java b/serverlessworkflow-serialization/src/main/java/io/serverlessworkflow/serialization/SerializeHelper.java similarity index 91% rename from api/src/main/java/io/serverlessworkflow/serialization/SerializeHelper.java rename to serverlessworkflow-serialization/src/main/java/io/serverlessworkflow/serialization/SerializeHelper.java index e074a656..47ab1a5e 100644 --- a/api/src/main/java/io/serverlessworkflow/serialization/SerializeHelper.java +++ b/serverlessworkflow-serialization/src/main/java/io/serverlessworkflow/serialization/SerializeHelper.java @@ -16,11 +16,11 @@ package io.serverlessworkflow.serialization; import com.fasterxml.jackson.core.JsonGenerator; -import io.serverlessworkflow.api.OneOfValueProvider; +import io.serverlessworkflow.annotations.OneOfValueProvider; import java.io.IOException; public class SerializeHelper { - public static void serializeOneOf(JsonGenerator jgen, OneOfValueProvider item) + public static void serializeOneOf(JsonGenerator jgen, OneOfValueProvider item) throws IOException { jgen.writeObject(item.get()); } diff --git a/serverlessworkflow-types/pom.xml b/serverlessworkflow-types/pom.xml new file mode 100644 index 00000000..90aa8996 --- /dev/null +++ b/serverlessworkflow-types/pom.xml @@ -0,0 +1,70 @@ + + 4.0.0 + + io.serverlessworkflow + serverlessworkflow-parent + 8.0.0-SNAPSHOT + + Serverless Workflow :: Types + serverlessworkflow-types + + + io.serverlessworkflow + serverlessworkflow-annotations + ${project.version} + + + jakarta.validation + jakarta.validation-api + + + + + + org.jsonschema2pojo + jsonschema2pojo-maven-plugin + + ${basedir}/src/main/resources/schema + + + yamlschema + io.serverlessworkflow.api.types + ${project.build.directory}/generated-sources/src/main/java + true + true + true + true + false + false + true + true + true + true + ${java.version} + true + none + true + io.serverlessworkflow.generator.UnreferencedFactory + io.serverlessworkflow.generator.CustomAnnotator + + + + io.serverlessworkflow + serverless-workflow-custom-generator + ${project.version} + + + + + + generate + + generate-sources + + + + + + \ No newline at end of file diff --git a/api/src/main/resources/schema/workflow.yaml b/serverlessworkflow-types/src/main/resources/schema/workflow.yaml similarity index 100% rename from api/src/main/resources/schema/workflow.yaml rename to serverlessworkflow-types/src/main/resources/schema/workflow.yaml pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy