diff --git a/doc/changelog.markdown b/doc/changelog.markdown index 04a81cd19d..9f0e2242ca 100644 --- a/doc/changelog.markdown +++ b/doc/changelog.markdown @@ -1,13 +1,14 @@ Lombok Changelog ---------------- -### v.18.13 "Edgy Guinea Pig" +### v1.18.13 "Edgy Guinea Pig" * BREAKING CHANGE: mapstruct users should not add a dependency to lombok-mapstruct-binding. This solves compiling modules with lombok (and mapstruct). * FEATURE: Similar to `@Builder`, you can now configure a `@SuperBuilder`'s 'setter' prefixes via `@SuperBuilder(setterPrefix = "set")` for example. We still discourage doing this. [Pull Request #2357](https://github.com/rzwitserloot/lombok/pull/2357). * FEATURE: If using `@Synchronized("lockVar")`, if `lockVar` is referring to a static field, the code lombok generates no longer causes a warning about accessing a static entity incorrectly. [Issue #678](https://github.com/rzwitserloot/lombok/issues/678) * BUGFIX: Using `@SuperBuilder` on a class that has some fairly convoluted generics usage would fail with 'Wrong number of type arguments'. [Issue #2359](https://github.com/rzwitserloot/lombok/issues/2359) [Pull Request #2362](https://github.com/rzwitserloot/lombok/pull/2362) * BUGFIX: Various lombok annotations on classes nested inside enums or interfaces would cause errors in eclipse. [Issue #2369](https://github.com/rzwitserloot/lombok/issues/2369) * BUGFIX: Trying to add `@ExtensionMethod`s with exactly 2 arguments would fail in eclipse. [Issue #1441](https://github.com/rzwitserloot/lombok/issues/1441) [Pull Request #2376](https://github.com/rzwitserloot/lombok/pull/2376) thanks to __@Rawi01__. +* FEATURE: `@Jacksonized` on a `@Builder` or `@SuperBuilder` will configure [Jackson](https://github.com/FasterXML/jackson) to use this builder when deserializing. [Pull Request #2387](https://github.com/rzwitserloot/lombok/pull/2387). [@Jacksonized documentation](https://projectlombok.org/features/experimental/Jacksonized). ### v1.18.12 (February 1st, 2020) * PLATFORM: Support for JDK13 (including `yield` in switch expressions, as well as delombok having a nicer style for arrow-style switch blocks, and text blocks). diff --git a/src/core/lombok/ConfigurationKeys.java b/src/core/lombok/ConfigurationKeys.java index 8a02885861..46cf7412b6 100644 --- a/src/core/lombok/ConfigurationKeys.java +++ b/src/core/lombok/ConfigurationKeys.java @@ -651,6 +651,15 @@ private ConfigurationKeys() {} * If set, any usage of {@code @WithBy} results in a warning / error. */ public static final ConfigurationKey WITHBY_FLAG_USAGE = new ConfigurationKey("lombok.withBy.flagUsage", "Emit a warning or error if @WithBy is used.") {}; + + // ----- Jacksonized ----- + + /** + * lombok configuration: {@code lombok.jacksonized.flagUsage} = {@code WARNING} | {@code ERROR}. + * + * If set, any usage of {@code @Jacksonized} results in a warning / error. + */ + public static final ConfigurationKey JACKSONIZED_FLAG_USAGE = new ConfigurationKey("lombok.jacksonized.flagUsage", "Emit a warning or error if @Jacksonized is used.") {}; // ----- Configuration System ----- diff --git a/src/core/lombok/core/handlers/HandlerUtil.java b/src/core/lombok/core/handlers/HandlerUtil.java index 4945adb9e8..94fd21d953 100644 --- a/src/core/lombok/core/handlers/HandlerUtil.java +++ b/src/core/lombok/core/handlers/HandlerUtil.java @@ -76,7 +76,7 @@ public static int primeForNull() { return 43; } - public static final List NONNULL_ANNOTATIONS, BASE_COPYABLE_ANNOTATIONS, COPY_TO_SETTER_ANNOTATIONS; + public static final List NONNULL_ANNOTATIONS, BASE_COPYABLE_ANNOTATIONS, COPY_TO_SETTER_ANNOTATIONS, JACKSON_COPY_TO_BUILDER_ANNOTATIONS; static { NONNULL_ANNOTATIONS = Collections.unmodifiableList(Arrays.asList(new String[] { "androidx.annotation.NonNull", @@ -315,6 +315,18 @@ public static int primeForNull() { "com.fasterxml.jackson.annotation.JsonProperty", "com.fasterxml.jackson.annotation.JsonSetter", })); + JACKSON_COPY_TO_BUILDER_ANNOTATIONS = Collections.unmodifiableList(Arrays.asList(new String[] { + "com.fasterxml.jackson.annotation.JsonFormat", + "com.fasterxml.jackson.annotation.JsonIgnoreProperties", + "com.fasterxml.jackson.annotation.JsonIgnoreType", + "com.fasterxml.jackson.annotation.JsonPropertyOrder", + "com.fasterxml.jackson.annotation.JsonRootName", + "com.fasterxml.jackson.annotation.JsonSubTypes", + "com.fasterxml.jackson.annotation.JsonTypeInfo", + "com.fasterxml.jackson.annotation.JsonTypeName", + "com.fasterxml.jackson.annotation.JsonView", + "com.fasterxml.jackson.databind.annotation.JsonNaming", + })); } /** Checks if the given name is a valid identifier. diff --git a/src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java b/src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java index ae899d00d9..4df7a90b4f 100644 --- a/src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java +++ b/src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java @@ -1957,12 +1957,16 @@ public static Annotation[] addGenerated(EclipseNode node, ASTNode source, Annota result = addAnnotation(source, result, JAVAX_ANNOTATION_GENERATED, new StringLiteral(LOMBOK, 0, 0, 0)); } if (Boolean.TRUE.equals(node.getAst().readConfiguration(ConfigurationKeys.ADD_LOMBOK_GENERATED_ANNOTATIONS))) { - result = addAnnotation(source, result, LOMBOK_GENERATED, null); + result = addAnnotation(source, result, LOMBOK_GENERATED); } return result; } - static Annotation[] addAnnotation(ASTNode source, Annotation[] originalAnnotationArray, char[][] annotationTypeFqn, ASTNode arg) { + static Annotation[] addAnnotation(ASTNode source, Annotation[] originalAnnotationArray, char[][] annotationTypeFqn) { + return addAnnotation(source, originalAnnotationArray, annotationTypeFqn, (ASTNode[]) null); + } + + static Annotation[] addAnnotation(ASTNode source, Annotation[] originalAnnotationArray, char[][] annotationTypeFqn, ASTNode... args) { char[] simpleName = annotationTypeFqn[annotationTypeFqn.length - 1]; if (originalAnnotationArray != null) for (Annotation ann : originalAnnotationArray) { @@ -1984,20 +1988,23 @@ static Annotation[] addAnnotation(ASTNode source, Annotation[] originalAnnotatio QualifiedTypeReference qualifiedType = new QualifiedTypeReference(annotationTypeFqn, poss); setGeneratedBy(qualifiedType, source); Annotation ann; - if (arg instanceof Expression) { + if (args != null && args.length == 1 && args[0] instanceof Expression) { SingleMemberAnnotation sma = new SingleMemberAnnotation(qualifiedType, pS); sma.declarationSourceEnd = pE; - arg.sourceStart = pS; - arg.sourceEnd = pE; - sma.memberValue = (Expression) arg; + args[0].sourceStart = pS; + args[0].sourceEnd = pE; + sma.memberValue = (Expression) args[0]; setGeneratedBy(sma.memberValue, source); ann = sma; - } else if (arg instanceof MemberValuePair) { + } else if (args != null && args.length >= 1 && arrayHasOnlyElementsOfType(args, MemberValuePair.class)) { NormalAnnotation na = new NormalAnnotation(qualifiedType, pS); na.declarationSourceEnd = pE; - arg.sourceStart = pS; - arg.sourceEnd = pE; - na.memberValuePairs = new MemberValuePair[] {(MemberValuePair) arg}; + na.memberValuePairs = new MemberValuePair[args.length]; + for (int i = 0; i < args.length; i++) { + args[i].sourceStart = pS; + args[i].sourceEnd = pE; + na.memberValuePairs[i] = (MemberValuePair) args[i]; + } setGeneratedBy(na.memberValuePairs[0], source); setGeneratedBy(na.memberValuePairs[0].value, source); na.memberValuePairs[0].value.sourceStart = pS; @@ -2016,6 +2023,14 @@ static Annotation[] addAnnotation(ASTNode source, Annotation[] originalAnnotatio return newAnnotationArray; } + private static boolean arrayHasOnlyElementsOfType(Object[] array, Class clazz) { + for (Object element : array) { + if (!clazz.isInstance(element)) + return false; + } + return true; + } + /** * Generates a new statement that checks if the given local variable is null, and if so, throws a specified exception with the * variable name as message. diff --git a/src/core/lombok/eclipse/handlers/HandleBuilder.java b/src/core/lombok/eclipse/handlers/HandleBuilder.java index 20bec50e28..b8e88522b2 100755 --- a/src/core/lombok/eclipse/handlers/HandleBuilder.java +++ b/src/core/lombok/eclipse/handlers/HandleBuilder.java @@ -376,32 +376,9 @@ private static final char[] prefixWith(char[] prefix, char[] name) { thrownExceptions = md.thrownExceptions; nameOfStaticBuilderMethod = md.selector; if (replaceNameInBuilderClassName) { - char[] token; - if (md.returnType instanceof QualifiedTypeReference) { - char[][] tokens = ((QualifiedTypeReference) md.returnType).tokens; - token = tokens[tokens.length - 1]; - } else if (md.returnType instanceof SingleTypeReference) { - token = ((SingleTypeReference) md.returnType).token; - if (!(md.returnType instanceof ParameterizedSingleTypeReference) && typeParams != null) { - for (TypeParameter tp : typeParams) { - if (Arrays.equals(tp.name, token)) { - annotationNode.addError("@Builder requires specifying 'builderClassName' if used on methods with a type parameter as return type."); - return; - } - } - } - } else { - annotationNode.addError("Unexpected kind of return type on annotated method. Specify 'builderClassName' to solve this problem."); + char[] token = returnTypeToBuilderClassName(annotationNode, md, typeParams); + if (token == null) return; - } - - if (Character.isLowerCase(token[0])) { - char[] newToken = new char[token.length]; - System.arraycopy(token, 1, newToken, 1, token.length - 1); - newToken[0] = Character.toTitleCase(token[0]); - token = newToken; - } - builderClassName = builderClassName.replace("*", new String(token)); } } else { @@ -550,6 +527,35 @@ private static final char[] prefixWith(char[] prefix, char[] name) { } } } + + static char[] returnTypeToBuilderClassName(EclipseNode annotationNode, MethodDeclaration md, TypeParameter[] typeParams) { + char[] token; + if (md.returnType instanceof QualifiedTypeReference) { + char[][] tokens = ((QualifiedTypeReference) md.returnType).tokens; + token = tokens[tokens.length - 1]; + } else if (md.returnType instanceof SingleTypeReference) { + token = ((SingleTypeReference) md.returnType).token; + if (!(md.returnType instanceof ParameterizedSingleTypeReference) && typeParams != null) { + for (TypeParameter tp : typeParams) { + if (Arrays.equals(tp.name, token)) { + annotationNode.addError("@Builder requires specifying 'builderClassName' if used on methods with a type parameter as return type."); + return null; + } + } + } + } else { + annotationNode.addError("Unexpected kind of return type on annotated method. Specify 'builderClassName' to solve this problem."); + return null; + } + + if (Character.isLowerCase(token[0])) { + char[] newToken = new char[token.length]; + System.arraycopy(token, 1, newToken, 1, token.length - 1); + newToken[0] = Character.toTitleCase(token[0]); + token = newToken; + } + return token; + } private static final char[] BUILDER_TEMP_VAR = {'b', 'u', 'i', 'l', 'd', 'e', 'r'}; private MethodDeclaration generateToBuilderMethod(CheckerFrameworkVersion cfv, boolean isStatic, String methodName, String builderClassName, EclipseNode type, TypeParameter[] typeParams, List builderFields, boolean fluent, ASTNode source, AccessLevel access, String prefix) { diff --git a/src/core/lombok/eclipse/handlers/HandleJacksonized.java b/src/core/lombok/eclipse/handlers/HandleJacksonized.java new file mode 100644 index 0000000000..90ee758291 --- /dev/null +++ b/src/core/lombok/eclipse/handlers/HandleJacksonized.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2013-2020 The Project Lombok Authors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package lombok.eclipse.handlers; + +import static lombok.core.handlers.HandlerUtil.handleExperimentalFlagUsage; +import static lombok.eclipse.handlers.EclipseHandlerUtil.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jdt.internal.compiler.ast.ASTNode; +import org.eclipse.jdt.internal.compiler.ast.Annotation; +import org.eclipse.jdt.internal.compiler.ast.ClassLiteralAccess; +import org.eclipse.jdt.internal.compiler.ast.MemberValuePair; +import org.eclipse.jdt.internal.compiler.ast.MethodDeclaration; +import org.eclipse.jdt.internal.compiler.ast.StringLiteral; +import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration; +import org.eclipse.jdt.internal.compiler.ast.TypeReference; +import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants; +import org.mangosdk.spi.ProviderFor; + +import lombok.Builder; +import lombok.ConfigurationKeys; +import lombok.core.AnnotationValues; +import lombok.core.HandlerPriority; +import lombok.core.AST.Kind; +import lombok.core.handlers.HandlerUtil; +import lombok.eclipse.Eclipse; +import lombok.eclipse.EclipseAnnotationHandler; +import lombok.eclipse.EclipseNode; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; + +/** + * This (ecj) handler deals with {@code @Jacksonized} modifying the (already + * generated) {@code @Builder} or {@code @SuperBuilder} to conform to Jackson's + * needs for builders. + */ +@ProviderFor(EclipseAnnotationHandler.class) @HandlerPriority(-512) // Above Handle(Super)Builder's level (builders must be already generated). +public class HandleJacksonized extends EclipseAnnotationHandler { + + private static final char[][] JSON_POJO_BUILDER_ANNOTATION = Eclipse.fromQualifiedName("com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder"); + private static final char[][] JSON_DESERIALIZE_ANNOTATION = Eclipse.fromQualifiedName("com.fasterxml.jackson.databind.annotation.JsonDeserialize"); + + @Override public void handle(AnnotationValues annotation, Annotation ast, EclipseNode annotationNode) { + handleExperimentalFlagUsage(annotationNode, ConfigurationKeys.JACKSONIZED_FLAG_USAGE, "@Jacksonized"); + + EclipseNode annotatedNode = annotationNode.up(); + + EclipseNode tdNode; + if (annotatedNode.getKind() != Kind.TYPE) + tdNode = annotatedNode.up(); // @Jacksonized on a constructor or a static factory method. + else + tdNode = annotatedNode; // @Jacksonized on the class. + TypeDeclaration td = (TypeDeclaration) tdNode.get(); + + EclipseNode builderAnnotationNode = findAnnotation(Builder.class, annotatedNode); + EclipseNode superBuilderAnnotationNode = findAnnotation(SuperBuilder.class, annotatedNode); + if (builderAnnotationNode == null && superBuilderAnnotationNode == null) { + annotationNode.addWarning("@Jacksonized requires @Builder or @SuperBuilder for it to mean anything."); + return; + } + + if (builderAnnotationNode != null && superBuilderAnnotationNode != null) { + annotationNode.addError("@Jacksonized cannot process both @Builder and @SuperBuilder on the same class."); + return; + } + + boolean isAbstract = (td.modifiers & ClassFileConstants.AccAbstract) != 0; + if (isAbstract) { + annotationNode.addError("Builders on abstract classes cannot be @Jacksonized (the builder would never be used)."); + return; + } + + AnnotationValues builderAnnotation = builderAnnotationNode != null ? createAnnotation(Builder.class, builderAnnotationNode) : null; + AnnotationValues superBuilderAnnotation = superBuilderAnnotationNode != null ? createAnnotation(SuperBuilder.class, superBuilderAnnotationNode) : null; + + String setPrefix = builderAnnotation != null ? builderAnnotation.getInstance().setterPrefix() : superBuilderAnnotation.getInstance().setterPrefix(); + String buildMethodName = builderAnnotation != null ? builderAnnotation.getInstance().buildMethodName() : superBuilderAnnotation.getInstance().buildMethodName(); + + // Now lets find the generated builder class. + EclipseNode builderClassNode = null; + TypeDeclaration builderClass = null; + String builderClassName = getBuilderClassName(ast, annotationNode, annotatedNode, td, builderAnnotation); + for (EclipseNode member : tdNode.down()) { + ASTNode astNode = member.get(); + if (astNode instanceof TypeDeclaration && Arrays.equals(((TypeDeclaration)astNode).name, builderClassName.toCharArray())) { + builderClassNode = member; + builderClass = (TypeDeclaration) astNode; + break; + } + } + + if (builderClass == null) { + annotationNode.addError("Could not find @(Super)Builder's generated builder class for @Jacksonized processing. If there are other compiler errors, fix them first."); + return; + } + + // Insert @JsonDeserialize on annotated class. + if (hasAnnotation("com.fasterxml.jackson.databind.annotation.JsonDeserialize", tdNode)) { + annotationNode.addError("@JsonDeserialize already exists on class. Either delete @JsonDeserialize, or remove @Jacksonized and manually configure Jackson."); + return; + } + long p = (long) ast.sourceStart << 32 | ast.sourceEnd; + TypeReference builderClassExpression = namePlusTypeParamsToTypeReference(builderClassNode, null, p); + ClassLiteralAccess builderClassLiteralAccess = new ClassLiteralAccess(td.sourceEnd, builderClassExpression); + MemberValuePair builderMvp = new MemberValuePair("builder".toCharArray(), td.sourceStart, td.sourceEnd, builderClassLiteralAccess); + td.annotations = addAnnotation(td, td.annotations, JSON_DESERIALIZE_ANNOTATION, builderMvp); + + // Copy annotations from the class to the builder class. + Annotation[] copyableAnnotations = findJacksonAnnotationsOnClass(td, tdNode); + builderClass.annotations = copyAnnotations(builderClass, builderClass.annotations, copyableAnnotations); + + // Insert @JsonPOJOBuilder on the builder class. + StringLiteral withPrefixLiteral = new StringLiteral(setPrefix.toCharArray(), builderClass.sourceStart, builderClass.sourceEnd, 0); + MemberValuePair withPrefixMvp = new MemberValuePair("withPrefix".toCharArray(), builderClass.sourceStart, builderClass.sourceEnd, withPrefixLiteral); + StringLiteral buildMethodNameLiteral = new StringLiteral(buildMethodName.toCharArray(), builderClass.sourceStart, builderClass.sourceEnd, 0); + MemberValuePair buildMethodNameMvp = new MemberValuePair("buildMethodName".toCharArray(), builderClass.sourceStart, builderClass.sourceEnd, buildMethodNameLiteral); + builderClass.annotations = addAnnotation(builderClass, builderClass.annotations, JSON_POJO_BUILDER_ANNOTATION, withPrefixMvp, buildMethodNameMvp); + + // @SuperBuilder? Make it package-private! + if (superBuilderAnnotationNode != null) + builderClass.modifiers = builderClass.modifiers & ~ClassFileConstants.AccPrivate; + } + + private String getBuilderClassName(Annotation ast, EclipseNode annotationNode, EclipseNode annotatedNode, TypeDeclaration td, AnnotationValues builderAnnotation) { + String builderClassName = builderAnnotation != null ? + builderAnnotation.getInstance().builderClassName() : null; + if (builderClassName == null || builderClassName.isEmpty()) { + builderClassName = annotationNode.getAst().readConfiguration(ConfigurationKeys.BUILDER_CLASS_NAME); + if (builderClassName == null || builderClassName.isEmpty()) + builderClassName = "*Builder"; + + MethodDeclaration fillParametersFrom = annotatedNode.get() instanceof MethodDeclaration ? (MethodDeclaration) annotatedNode.get() : null; + char[] replacement; + if (fillParametersFrom != null) { + // @Builder on a method: Use name of return type for builder class name. + replacement = HandleBuilder.returnTypeToBuilderClassName(annotationNode, fillParametersFrom, fillParametersFrom.typeParameters); + } else { + // @Builder on class or constructor: Use the class name. + replacement = td.name; + } + builderClassName = builderClassName.replace("*", new String(replacement)); + } + + if (builderAnnotation == null) + builderClassName += "Impl"; // For @SuperBuilder, all Jackson annotations must be put on the BuilderImpl class. + + return builderClassName; + } + + private static final Annotation[] EMPTY_ANNOTATIONS_ARRAY = new Annotation[0]; + + private static Annotation[] findJacksonAnnotationsOnClass(TypeDeclaration td, EclipseNode node) { + if (td.annotations == null) return EMPTY_ANNOTATIONS_ARRAY; + + List result = new ArrayList(); + for (Annotation annotation : td.annotations) { + TypeReference typeRef = annotation.type; + if (typeRef != null && typeRef.getTypeName() != null) { + for (String bn : HandlerUtil.JACKSON_COPY_TO_BUILDER_ANNOTATIONS) { + if (typeMatches(bn, node, typeRef)) { + result.add(annotation); + break; + } + } + } + } + return result.toArray(EMPTY_ANNOTATIONS_ARRAY); + } +} diff --git a/src/core/lombok/extern/jackson/Jacksonized.java b/src/core/lombok/extern/jackson/Jacksonized.java new file mode 100644 index 0000000000..cf6678da08 --- /dev/null +++ b/src/core/lombok/extern/jackson/Jacksonized.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2020 The Project Lombok Authors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package lombok.extern.jackson; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.*; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import lombok.Builder; +import lombok.experimental.SuperBuilder; + +/** + * The {@code @Jacksonized} annotation is an add-on annotation for + * {@code @}{@link Builder} and {@code @}{@link SuperBuilder}. It automatically + * configures the generated builder class to be used by Jackson's + * deserialization. It only has an effect if present at a context where there is + * also a {@code @Builder} or a {@code @SuperBuilder}; a warning is emitted + * otherwise. + *

+ * In particular, the annotation does the following: + *

    + *
  • Configure Jackson to use the builder for deserialization using + * {@code @JsonDeserialize(builder=Foobar.FoobarBuilder[Impl].class)} + * on the class (where Foobar is the name of the annotated class).
  • + *
  • Copy Jackson-related configuration annotations (like + * {@code @JsonIgnoreProperties}) from the class to the builder class. This is + * necessary so that Jackson recognizes them when using the builder.
  • + *
  • Insert {@code @JsonPOJOBuilder(withPrefix="")} on the generated builder + * class to override Jackson's default prefix "with". If you configured a + * different prefix in lombok using {@code setterPrefix}, this value is used. If + * you changed the name of the {@code build()} method using using + * {@code buildMethodName}, this is also made known to Jackson.
  • + *
  • For {@code @SuperBuilder}, make the builder implementation class + * package-private.
  • + *
+ * This annotation does not change the behavior of the generated builder. + * A {@code @Jacksonized} {@code @SuperBuilder} remains fully compatible to + * regular {@code @SuperBuilder}s. + */ +@Target({TYPE, METHOD, CONSTRUCTOR}) +@Retention(SOURCE) +public @interface Jacksonized { +} diff --git a/src/core/lombok/javac/handlers/HandleBuilder.java b/src/core/lombok/javac/handlers/HandleBuilder.java index 2f000546b2..91a74d62bc 100644 --- a/src/core/lombok/javac/handlers/HandleBuilder.java +++ b/src/core/lombok/javac/handlers/HandleBuilder.java @@ -149,7 +149,7 @@ static class BuilderFieldData { if (!checkName("builderClassName", builderClassName, annotationNode)) return; } - deleteAnnotationIfNeccessary(annotationNode, Builder.class, "lombok.experimental.Builder"); + // Do not delete the Builder annotation here, we need it for @Jacksonized. JavacNode parent = annotationNode.up(); @@ -257,38 +257,9 @@ static class BuilderFieldData { returnType = cloneType(tdParent.getTreeMaker(), returnType, ast, annotationNode.getContext()); } if (replaceNameInBuilderClassName) { - String replStr = null; - if (returnType instanceof JCFieldAccess) { - replStr = ((JCFieldAccess) returnType).name.toString(); - } else if (returnType instanceof JCIdent) { - Name n = ((JCIdent) returnType).name; - - for (JCTypeParameter tp : typeParams) { - if (tp.name.equals(n)) { - annotationNode.addError("@Builder requires specifying 'builderClassName' if used on methods with a type parameter as return type."); - return; - } - } - replStr = n.toString(); - } else if (returnType instanceof JCPrimitiveTypeTree) { - replStr = returnType.toString(); - if (Character.isLowerCase(replStr.charAt(0))) { - replStr = Character.toTitleCase(replStr.charAt(0)) + replStr.substring(1); - } - } else if (returnType instanceof JCTypeApply) { - JCExpression clazz = ((JCTypeApply) returnType).clazz; - if (clazz instanceof JCFieldAccess) { - replStr = ((JCFieldAccess) clazz).name.toString(); - } else if (clazz instanceof JCIdent) { - replStr = ((JCIdent) clazz).name.toString(); - } - } - - if (replStr == null || replStr.isEmpty()) { - // This shouldn't happen. - System.err.println("Lombok bug ID#20140614-1651: javac HandleBuilder: return type to name conversion failed: " + returnType.getClass()); - replStr = td.name.toString(); - } + String replStr = returnTypeToBuilderClassName(annotationNode, td, returnType, typeParams); + if (replStr == null) + return; builderClassName = builderClassName.replace("*", replStr); replaceNameInBuilderClassName = false; } @@ -507,6 +478,42 @@ static class BuilderFieldData { } } } + + static String returnTypeToBuilderClassName(JavacNode annotationNode, JCClassDecl td, JCExpression returnType, List typeParams) { + String replStr = null; + if (returnType instanceof JCFieldAccess) { + replStr = ((JCFieldAccess) returnType).name.toString(); + } else if (returnType instanceof JCIdent) { + Name n = ((JCIdent) returnType).name; + + for (JCTypeParameter tp : typeParams) { + if (tp.name.equals(n)) { + annotationNode.addError("@Builder requires specifying 'builderClassName' if used on methods with a type parameter as return type."); + return null; + } + } + replStr = n.toString(); + } else if (returnType instanceof JCPrimitiveTypeTree) { + replStr = returnType.toString(); + if (Character.isLowerCase(replStr.charAt(0))) { + replStr = Character.toTitleCase(replStr.charAt(0)) + replStr.substring(1); + } + } else if (returnType instanceof JCTypeApply) { + JCExpression clazz = ((JCTypeApply) returnType).clazz; + if (clazz instanceof JCFieldAccess) { + replStr = ((JCFieldAccess) clazz).name.toString(); + } else if (clazz instanceof JCIdent) { + replStr = ((JCIdent) clazz).name.toString(); + } + } + + if (replStr == null || replStr.isEmpty()) { + // This shouldn't happen. + System.err.println("Lombok bug ID#20140614-1651: javac HandleBuilder: return type to name conversion failed: " + returnType.getClass()); + replStr = td.name.toString(); + } + return replStr; + } private static String unpack(JCExpression expr) { StringBuilder sb = new StringBuilder(); diff --git a/src/core/lombok/javac/handlers/HandleBuilderRemove.java b/src/core/lombok/javac/handlers/HandleBuilderRemove.java new file mode 100644 index 0000000000..6e59b40fb9 --- /dev/null +++ b/src/core/lombok/javac/handlers/HandleBuilderRemove.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2020 The Project Lombok Authors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package lombok.javac.handlers; + +import static lombok.javac.handlers.JavacHandlerUtil.*; + +import org.mangosdk.spi.ProviderFor; + +import com.sun.tools.javac.tree.JCTree.JCAnnotation; + +import lombok.Builder; +import lombok.core.AlreadyHandledAnnotations; +import lombok.core.AnnotationValues; +import lombok.core.HandlerPriority; +import lombok.javac.JavacAnnotationHandler; +import lombok.javac.JavacNode; + +@ProviderFor(JavacAnnotationHandler.class) +@HandlerPriority(65536) +@AlreadyHandledAnnotations +public class HandleBuilderRemove extends JavacAnnotationHandler { + @Override public void handle(AnnotationValues annotation, JCAnnotation ast, JavacNode annotationNode) { + deleteAnnotationIfNeccessary(annotationNode, Builder.class, "lombok.experimental.Builder"); + } +} diff --git a/src/core/lombok/javac/handlers/HandleJacksonized.java b/src/core/lombok/javac/handlers/HandleJacksonized.java new file mode 100644 index 0000000000..aff0bf6340 --- /dev/null +++ b/src/core/lombok/javac/handlers/HandleJacksonized.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2020 The Project Lombok Authors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package lombok.javac.handlers; + +import static lombok.core.handlers.HandlerUtil.handleExperimentalFlagUsage; +import static lombok.javac.handlers.JavacHandlerUtil.*; + +import org.mangosdk.spi.ProviderFor; + +import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.JCTree.JCAnnotation; +import com.sun.tools.javac.tree.JCTree.JCClassDecl; +import com.sun.tools.javac.tree.JCTree.JCExpression; +import com.sun.tools.javac.tree.JCTree.JCFieldAccess; +import com.sun.tools.javac.tree.JCTree.JCMethodDecl; +import com.sun.tools.javac.tree.JCTree.JCTypeApply; +import com.sun.tools.javac.tree.JCTree.JCTypeParameter; +import com.sun.tools.javac.util.List; +import com.sun.tools.javac.util.ListBuffer; + +import lombok.Builder; +import lombok.ConfigurationKeys; +import lombok.core.AST.Kind; +import lombok.core.AnnotationValues; +import lombok.core.HandlerPriority; +import lombok.core.handlers.HandlerUtil; +import lombok.experimental.SuperBuilder; +import lombok.extern.jackson.Jacksonized; +import lombok.javac.JavacAnnotationHandler; +import lombok.javac.JavacNode; +import lombok.javac.JavacTreeMaker; + +/** + * This (javac) handler deals with {@code @Jacksonized} modifying the (already + * generated) {@code @Builder} or {@code @SuperBuilder} to conform to Jackson's + * needs for builders. + */ +@ProviderFor(JavacAnnotationHandler.class) +@HandlerPriority(-512) // Above Handle(Super)Builder's level (builders must be already generated). +public class HandleJacksonized extends JavacAnnotationHandler { + + @Override public void handle(AnnotationValues annotation, JCAnnotation ast, JavacNode annotationNode) { + handleExperimentalFlagUsage(annotationNode, ConfigurationKeys.JACKSONIZED_FLAG_USAGE, "@Jacksonized"); + + JavacNode annotatedNode = annotationNode.up(); + deleteAnnotationIfNeccessary(annotationNode, Jacksonized.class); + + JavacNode tdNode; + if (annotatedNode.getKind() != Kind.TYPE) + tdNode = annotatedNode.up(); // @Jacksonized on a constructor or a static factory method. + else + tdNode = annotatedNode; // @Jacksonized on the class. + JCClassDecl td = (JCClassDecl) tdNode.get(); + + JavacNode builderAnnotationNode = findAnnotation(Builder.class, annotatedNode); + JavacNode superBuilderAnnotationNode = findAnnotation(SuperBuilder.class, annotatedNode); + if (builderAnnotationNode == null && superBuilderAnnotationNode == null) { + annotationNode.addWarning("@Jacksonized requires @Builder or @SuperBuilder for it to mean anything."); + return; + } + + if (builderAnnotationNode != null && superBuilderAnnotationNode != null) { + annotationNode.addError("@Jacksonized cannot process both @Builder and @SuperBuilder on the same class."); + return; + } + + boolean isAbstract = (td.mods.flags & Flags.ABSTRACT) != 0; + if (isAbstract) { + annotationNode.addError("Builders on abstract classes cannot be @Jacksonized (the builder would never be used)."); + return; + } + + AnnotationValues builderAnnotation = builderAnnotationNode != null ? + createAnnotation(Builder.class, builderAnnotationNode) : + null; + AnnotationValues superBuilderAnnotation = superBuilderAnnotationNode != null ? + createAnnotation(SuperBuilder.class, superBuilderAnnotationNode) : + null; + + String setPrefix = builderAnnotation != null ? + builderAnnotation.getInstance().setterPrefix() : + superBuilderAnnotation.getInstance().setterPrefix(); + String buildMethodName = builderAnnotation != null ? + builderAnnotation.getInstance().buildMethodName() : + superBuilderAnnotation.getInstance().buildMethodName(); + + JavacTreeMaker maker = annotatedNode.getTreeMaker(); + + // Now lets find the generated builder class. + String builderClassName = getBuilderClassName(ast, annotationNode, annotatedNode, td, builderAnnotation, maker); + + JCClassDecl builderClass = null; + for (JCTree member : td.getMembers()) { + if (member instanceof JCClassDecl && ((JCClassDecl) member).getSimpleName().contentEquals(builderClassName)) { + builderClass = (JCClassDecl) member; + break; + } + } + + if (builderClass == null) { + annotationNode.addError("Could not find @(Super)Builder's generated builder class for @Jacksonized processing. If there are other compiler errors, fix them first."); + return; + } + + // Insert @JsonDeserialize on annotated class. + if (hasAnnotation("com.fasterxml.jackson.databind.annotation.JsonDeserialize", tdNode)) { + annotationNode.addError("@JsonDeserialize already exists on class. Either delete @JsonDeserialize, or remove @Jacksonized and manually configure Jackson."); + return; + } + JCExpression jsonDeserializeType = chainDots(annotatedNode, "com", "fasterxml", "jackson", "databind", "annotation", "JsonDeserialize"); + JCExpression builderClassExpression = namePlusTypeParamsToTypeReference(maker, tdNode, annotationNode.toName(builderClassName), false, List.nil()); + JCFieldAccess builderClassReference = maker.Select(builderClassExpression, annotatedNode.toName("class")); + JCExpression assign = maker.Assign(maker.Ident(annotationNode.toName("builder")), builderClassReference); + JCAnnotation annotationJsonDeserialize = maker.Annotation(jsonDeserializeType, List.of(assign)); + td.mods.annotations = td.mods.annotations.append(annotationJsonDeserialize); + + // Copy annotations from the class to the builder class. + List copyableAnnotations = findJacksonAnnotationsOnClass(tdNode); + List copiedAnnotations = copyAnnotations(copyableAnnotations); + builderClass.mods.annotations = builderClass.mods.annotations.appendList(copiedAnnotations); + + // Insert @JsonPOJOBuilder on the builder class. + JCExpression jsonPOJOBuilderType = chainDots(annotatedNode, "com", "fasterxml", "jackson", "databind", "annotation", "JsonPOJOBuilder"); + JCExpression withPrefixExpr = maker.Assign(maker.Ident(annotationNode.toName("withPrefix")), maker.Literal(setPrefix)); + JCExpression buildMethodNameExpr = maker.Assign(maker.Ident(annotationNode.toName("buildMethodName")), maker.Literal(buildMethodName)); + JCAnnotation annotationJsonPOJOBuilder = maker.Annotation(jsonPOJOBuilderType, List.of(withPrefixExpr, buildMethodNameExpr)); + builderClass.mods.annotations = builderClass.mods.annotations.append(annotationJsonPOJOBuilder); + + // @SuperBuilder? Make it package-private! + if (superBuilderAnnotationNode != null) + builderClass.mods.flags = builderClass.mods.flags & ~Flags.PRIVATE; + + } + + private String getBuilderClassName(JCAnnotation ast, JavacNode annotationNode, JavacNode annotatedNode, JCClassDecl td, AnnotationValues builderAnnotation, JavacTreeMaker maker) { + String builderClassName = builderAnnotation != null ? + builderAnnotation.getInstance().builderClassName() : null; + if (builderClassName == null || builderClassName.isEmpty()) { + builderClassName = annotationNode.getAst().readConfiguration(ConfigurationKeys.BUILDER_CLASS_NAME); + if (builderClassName == null || builderClassName.isEmpty()) + builderClassName = "*Builder"; + + JCMethodDecl fillParametersFrom = annotatedNode.get() instanceof JCMethodDecl ? (JCMethodDecl)annotatedNode.get() : null; + String replacement; + if (fillParametersFrom != null && !fillParametersFrom.getName().toString().equals("")) { + // @Builder on a method: Use name of return type for builder class name. + JCExpression returnType = fillParametersFrom.restype; + List typeParams = fillParametersFrom.typarams; + if (returnType instanceof JCTypeApply) { + returnType = cloneType(maker, returnType, ast, annotationNode.getContext()); + } + replacement = HandleBuilder.returnTypeToBuilderClassName(annotationNode, td, returnType, typeParams); + } else { + // @Builder on class or constructor: Use the class name. + replacement = td.name.toString(); + } + builderClassName = builderClassName.replace("*", replacement); + } + + if (builderAnnotation == null) + builderClassName += "Impl"; // For @SuperBuilder, all Jackson annotations must be put on the BuilderImpl class. + + return builderClassName; + } + + private static List findJacksonAnnotationsOnClass(JavacNode node) { + ListBuffer result = new ListBuffer(); + for (JavacNode child : node.down()) { + if (child.getKind() == Kind.ANNOTATION) { + JCAnnotation annotation = (JCAnnotation) child.get(); + for (String bn : HandlerUtil.JACKSON_COPY_TO_BUILDER_ANNOTATIONS) { + if (typeMatches(bn, node, annotation.annotationType)) { + result.append(annotation); + break; + } + } + } + } + return result.toList(); + } +} diff --git a/src/core/lombok/javac/handlers/HandleSuperBuilder.java b/src/core/lombok/javac/handlers/HandleSuperBuilder.java index 5f4f3c1d07..cc70b33316 100644 --- a/src/core/lombok/javac/handlers/HandleSuperBuilder.java +++ b/src/core/lombok/javac/handlers/HandleSuperBuilder.java @@ -99,7 +99,7 @@ public void handle(AnnotationValues annotation, JCAnnotation ast, handleExperimentalFlagUsage(annotationNode, ConfigurationKeys.SUPERBUILDER_FLAG_USAGE, "@SuperBuilder"); CheckerFrameworkVersion cfv = getCheckerFrameworkVersion(annotationNode); SuperBuilder superbuilderAnnotation = annotation.getInstance(); - deleteAnnotationIfNeccessary(annotationNode, SuperBuilder.class); + // Do not delete the SuperBuilder annotation here, we need it for @Jacksonized. String builderMethodName = superbuilderAnnotation.builderMethodName(); String buildMethodName = superbuilderAnnotation.buildMethodName(); diff --git a/src/core/lombok/javac/handlers/HandleSuperBuilderRemove.java b/src/core/lombok/javac/handlers/HandleSuperBuilderRemove.java new file mode 100644 index 0000000000..cca69729fd --- /dev/null +++ b/src/core/lombok/javac/handlers/HandleSuperBuilderRemove.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2020 The Project Lombok Authors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package lombok.javac.handlers; + +import static lombok.javac.handlers.JavacHandlerUtil.*; + +import org.mangosdk.spi.ProviderFor; + +import com.sun.tools.javac.tree.JCTree.JCAnnotation; + +import lombok.core.AlreadyHandledAnnotations; +import lombok.core.AnnotationValues; +import lombok.core.HandlerPriority; +import lombok.experimental.SuperBuilder; +import lombok.javac.JavacAnnotationHandler; +import lombok.javac.JavacNode; + +@ProviderFor(JavacAnnotationHandler.class) +@HandlerPriority(65536) +@AlreadyHandledAnnotations +public class HandleSuperBuilderRemove extends JavacAnnotationHandler { + @Override public void handle(AnnotationValues annotation, JCAnnotation ast, JavacNode annotationNode) { + deleteAnnotationIfNeccessary(annotationNode, SuperBuilder.class); + } +} diff --git a/test/stubs/com/fasterxml/jackson/annotation/JsonIgnoreProperties.java b/test/stubs/com/fasterxml/jackson/annotation/JsonIgnoreProperties.java new file mode 100644 index 0000000000..c90ef91477 --- /dev/null +++ b/test/stubs/com/fasterxml/jackson/annotation/JsonIgnoreProperties.java @@ -0,0 +1,12 @@ +package com.fasterxml.jackson.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface JsonIgnoreProperties { + public boolean ignoreUnknown() default false; +} diff --git a/test/stubs/com/fasterxml/jackson/databind/annotation/JsonDeserialize.java b/test/stubs/com/fasterxml/jackson/databind/annotation/JsonDeserialize.java new file mode 100644 index 0000000000..0964f82c4d --- /dev/null +++ b/test/stubs/com/fasterxml/jackson/databind/annotation/JsonDeserialize.java @@ -0,0 +1,12 @@ +package com.fasterxml.jackson.databind.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.TYPE, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface JsonDeserialize { + public Class builder() default Void.class; +} diff --git a/test/stubs/com/fasterxml/jackson/databind/annotation/JsonPOJOBuilder.java b/test/stubs/com/fasterxml/jackson/databind/annotation/JsonPOJOBuilder.java new file mode 100644 index 0000000000..4906dc58e7 --- /dev/null +++ b/test/stubs/com/fasterxml/jackson/databind/annotation/JsonPOJOBuilder.java @@ -0,0 +1,10 @@ +package com.fasterxml.jackson.databind.annotation; + +import java.lang.annotation.*; + +@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface JsonPOJOBuilder { + public String buildMethodName() default "build"; + public String withPrefix() default "with"; +} diff --git a/test/transform/resource/after-delombok/BuilderDefaultsWarnings.java b/test/transform/resource/after-delombok/BuilderDefaultsWarnings.java index 3c84c71bba..ae865570d4 100644 --- a/test/transform/resource/after-delombok/BuilderDefaultsWarnings.java +++ b/test/transform/resource/after-delombok/BuilderDefaultsWarnings.java @@ -1,3 +1,5 @@ +import lombok.Builder; +@Builder public class BuilderDefaultsWarnings { long x = System.currentTimeMillis(); final int y = 5; @@ -78,6 +80,7 @@ public static BuilderDefaultsWarnings.BuilderDefaultsWarningsBuilder builder() { } class NoBuilderButHasDefaults { private final long z = 5; + @Builder public NoBuilderButHasDefaults() { } @java.lang.SuppressWarnings("all") diff --git a/test/transform/resource/after-delombok/BuilderSingularNoAuto.java b/test/transform/resource/after-delombok/BuilderSingularNoAuto.java index ba6ddc52f0..4c9de17461 100644 --- a/test/transform/resource/after-delombok/BuilderSingularNoAuto.java +++ b/test/transform/resource/after-delombok/BuilderSingularNoAuto.java @@ -1,4 +1,5 @@ import java.util.List; +@lombok.Builder class BuilderSingularNoAuto { private List things; private List widgets; diff --git a/test/transform/resource/after-delombok/BuilderSingularNoAutoWithSetterPrefix.java b/test/transform/resource/after-delombok/BuilderSingularNoAutoWithSetterPrefix.java index 857348d3f6..48b396ebb9 100644 --- a/test/transform/resource/after-delombok/BuilderSingularNoAutoWithSetterPrefix.java +++ b/test/transform/resource/after-delombok/BuilderSingularNoAutoWithSetterPrefix.java @@ -1,4 +1,5 @@ import java.util.List; +@lombok.Builder(setterPrefix = "with") class BuilderSingularNoAutoWithSetterPrefix { private List things; private List widgets; diff --git a/test/transform/resource/after-delombok/JacksonizedBuilderComplex.java b/test/transform/resource/after-delombok/JacksonizedBuilderComplex.java new file mode 100644 index 0000000000..30f6b9af9e --- /dev/null +++ b/test/transform/resource/after-delombok/JacksonizedBuilderComplex.java @@ -0,0 +1,55 @@ +//CONF: lombok.builder.className = Test*Name +import java.util.List; +@com.fasterxml.jackson.databind.annotation.JsonDeserialize(builder = JacksonizedBuilderComplex.TestVoidName.class) +class JacksonizedBuilderComplex { + private static void testVoidWithGenerics(T number, int arg2, String arg3, JacksonizedBuilderComplex selfRef) { + } + @java.lang.SuppressWarnings("all") + @com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "with", buildMethodName = "execute") + public static class TestVoidName { + @java.lang.SuppressWarnings("all") + private T number; + @java.lang.SuppressWarnings("all") + private int arg2; + @java.lang.SuppressWarnings("all") + private String arg3; + @java.lang.SuppressWarnings("all") + private JacksonizedBuilderComplex selfRef; + @java.lang.SuppressWarnings("all") + TestVoidName() { + } + @java.lang.SuppressWarnings("all") + public JacksonizedBuilderComplex.TestVoidName withNumber(final T number) { + this.number = number; + return this; + } + @java.lang.SuppressWarnings("all") + public JacksonizedBuilderComplex.TestVoidName withArg2(final int arg2) { + this.arg2 = arg2; + return this; + } + @java.lang.SuppressWarnings("all") + public JacksonizedBuilderComplex.TestVoidName withArg3(final String arg3) { + this.arg3 = arg3; + return this; + } + @java.lang.SuppressWarnings("all") + public JacksonizedBuilderComplex.TestVoidName withSelfRef(final JacksonizedBuilderComplex selfRef) { + this.selfRef = selfRef; + return this; + } + @java.lang.SuppressWarnings("all") + public void execute() { + JacksonizedBuilderComplex.testVoidWithGenerics(this.number, this.arg2, this.arg3, this.selfRef); + } + @java.lang.Override + @java.lang.SuppressWarnings("all") + public java.lang.String toString() { + return "JacksonizedBuilderComplex.TestVoidName(number=" + this.number + ", arg2=" + this.arg2 + ", arg3=" + this.arg3 + ", selfRef=" + this.selfRef + ")"; + } + } + @java.lang.SuppressWarnings("all") + public static JacksonizedBuilderComplex.TestVoidName builder() { + return new JacksonizedBuilderComplex.TestVoidName(); + } +} \ No newline at end of file diff --git a/test/transform/resource/after-delombok/JacksonizedBuilderSimple.java b/test/transform/resource/after-delombok/JacksonizedBuilderSimple.java new file mode 100644 index 0000000000..bd79df9071 --- /dev/null +++ b/test/transform/resource/after-delombok/JacksonizedBuilderSimple.java @@ -0,0 +1,50 @@ +import java.util.List; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +@JsonIgnoreProperties(ignoreUnknown = true) +@com.fasterxml.jackson.databind.annotation.JsonDeserialize(builder = JacksonizedBuilderSimple.JacksonizedBuilderSimpleBuilder.class) +class JacksonizedBuilderSimple { + private final int noshow = 0; + private final int yes; + private List also; + private int $butNotMe; + @java.lang.SuppressWarnings("all") + JacksonizedBuilderSimple(final int yes, final List also) { + this.yes = yes; + this.also = also; + } + @java.lang.SuppressWarnings("all") + @JsonIgnoreProperties(ignoreUnknown = true) + @com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "", buildMethodName = "build") + protected static class JacksonizedBuilderSimpleBuilder { + @java.lang.SuppressWarnings("all") + private int yes; + @java.lang.SuppressWarnings("all") + private List also; + @java.lang.SuppressWarnings("all") + JacksonizedBuilderSimpleBuilder() { + } + @java.lang.SuppressWarnings("all") + public JacksonizedBuilderSimple.JacksonizedBuilderSimpleBuilder yes(final int yes) { + this.yes = yes; + return this; + } + @java.lang.SuppressWarnings("all") + public JacksonizedBuilderSimple.JacksonizedBuilderSimpleBuilder also(final List also) { + this.also = also; + return this; + } + @java.lang.SuppressWarnings("all") + public JacksonizedBuilderSimple build() { + return new JacksonizedBuilderSimple(this.yes, this.also); + } + @java.lang.Override + @java.lang.SuppressWarnings("all") + public java.lang.String toString() { + return "JacksonizedBuilderSimple.JacksonizedBuilderSimpleBuilder(yes=" + this.yes + ", also=" + this.also + ")"; + } + } + @java.lang.SuppressWarnings("all") + protected static JacksonizedBuilderSimple.JacksonizedBuilderSimpleBuilder builder() { + return new JacksonizedBuilderSimple.JacksonizedBuilderSimpleBuilder(); + } +} diff --git a/test/transform/resource/after-delombok/JacksonizedSuperBuilderSimple.java b/test/transform/resource/after-delombok/JacksonizedSuperBuilderSimple.java new file mode 100644 index 0000000000..7fd3f8dca6 --- /dev/null +++ b/test/transform/resource/after-delombok/JacksonizedSuperBuilderSimple.java @@ -0,0 +1,55 @@ +public class JacksonizedSuperBuilderSimple { + @com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) + @com.fasterxml.jackson.databind.annotation.JsonDeserialize(builder = JacksonizedSuperBuilderSimple.Parent.ParentBuilderImpl.class) + public static class Parent { + int field1; + @java.lang.SuppressWarnings("all") + public static abstract class ParentBuilder> { + @java.lang.SuppressWarnings("all") + private int field1; + @java.lang.SuppressWarnings("all") + protected abstract B self(); + @java.lang.SuppressWarnings("all") + public abstract C build(); + @java.lang.SuppressWarnings("all") + public B field1(final int field1) { + this.field1 = field1; + return self(); + } + @java.lang.Override + @java.lang.SuppressWarnings("all") + public java.lang.String toString() { + return "JacksonizedSuperBuilderSimple.Parent.ParentBuilder(field1=" + this.field1 + ")"; + } + } + @java.lang.SuppressWarnings("all") + @com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) + @com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "", buildMethodName = "build") + static final class ParentBuilderImpl extends JacksonizedSuperBuilderSimple.Parent.ParentBuilder { + @java.lang.SuppressWarnings("all") + private ParentBuilderImpl() { + } + @java.lang.Override + @java.lang.SuppressWarnings("all") + protected JacksonizedSuperBuilderSimple.Parent.ParentBuilderImpl self() { + return this; + } + @java.lang.Override + @java.lang.SuppressWarnings("all") + public JacksonizedSuperBuilderSimple.Parent build() { + return new JacksonizedSuperBuilderSimple.Parent(this); + } + } + @java.lang.SuppressWarnings("all") + protected Parent(final JacksonizedSuperBuilderSimple.Parent.ParentBuilder b) { + this.field1 = b.field1; + } + @java.lang.SuppressWarnings("all") + public static JacksonizedSuperBuilderSimple.Parent.ParentBuilder builder() { + return new JacksonizedSuperBuilderSimple.Parent.ParentBuilderImpl(); + } + } + public static void test() { + Parent x = Parent.builder().field1(5).build(); + } +} diff --git a/test/transform/resource/after-delombok/JacksonizedSuperBuilderWithJsonDeserialize.java b/test/transform/resource/after-delombok/JacksonizedSuperBuilderWithJsonDeserialize.java new file mode 100644 index 0000000000..df52db33f9 --- /dev/null +++ b/test/transform/resource/after-delombok/JacksonizedSuperBuilderWithJsonDeserialize.java @@ -0,0 +1,48 @@ +@lombok.experimental.SuperBuilder +@com.fasterxml.jackson.databind.annotation.JsonDeserialize +public class JacksonizedSuperBuilderWithJsonDeserialize { + int field1; + @java.lang.SuppressWarnings("all") + public static abstract class JacksonizedSuperBuilderWithJsonDeserializeBuilder> { + @java.lang.SuppressWarnings("all") + private int field1; + @java.lang.SuppressWarnings("all") + protected abstract B self(); + @java.lang.SuppressWarnings("all") + public abstract C build(); + @java.lang.SuppressWarnings("all") + public B field1(final int field1) { + this.field1 = field1; + return self(); + } + @java.lang.Override + @java.lang.SuppressWarnings("all") + public java.lang.String toString() { + return "JacksonizedSuperBuilderWithJsonDeserialize.JacksonizedSuperBuilderWithJsonDeserializeBuilder(field1=" + this.field1 + ")"; + } + } + @java.lang.SuppressWarnings("all") + private static final class JacksonizedSuperBuilderWithJsonDeserializeBuilderImpl extends JacksonizedSuperBuilderWithJsonDeserialize.JacksonizedSuperBuilderWithJsonDeserializeBuilder { + @java.lang.SuppressWarnings("all") + private JacksonizedSuperBuilderWithJsonDeserializeBuilderImpl() { + } + @java.lang.Override + @java.lang.SuppressWarnings("all") + protected JacksonizedSuperBuilderWithJsonDeserialize.JacksonizedSuperBuilderWithJsonDeserializeBuilderImpl self() { + return this; + } + @java.lang.Override + @java.lang.SuppressWarnings("all") + public JacksonizedSuperBuilderWithJsonDeserialize build() { + return new JacksonizedSuperBuilderWithJsonDeserialize(this); + } + } + @java.lang.SuppressWarnings("all") + protected JacksonizedSuperBuilderWithJsonDeserialize(final JacksonizedSuperBuilderWithJsonDeserialize.JacksonizedSuperBuilderWithJsonDeserializeBuilder b) { + this.field1 = b.field1; + } + @java.lang.SuppressWarnings("all") + public static JacksonizedSuperBuilderWithJsonDeserialize.JacksonizedSuperBuilderWithJsonDeserializeBuilder builder() { + return new JacksonizedSuperBuilderWithJsonDeserialize.JacksonizedSuperBuilderWithJsonDeserializeBuilderImpl(); + } +} \ No newline at end of file diff --git a/test/transform/resource/after-ecj/JacksonizedBuilderComplex.java b/test/transform/resource/after-ecj/JacksonizedBuilderComplex.java new file mode 100644 index 0000000000..8a06b34b88 --- /dev/null +++ b/test/transform/resource/after-ecj/JacksonizedBuilderComplex.java @@ -0,0 +1,44 @@ +import java.util.List; +import lombok.Builder; +import lombok.extern.jackson.Jacksonized; +@com.fasterxml.jackson.databind.annotation.JsonDeserialize(builder = JacksonizedBuilderComplex.TestVoidName.class) class JacksonizedBuilderComplex { + public static @java.lang.SuppressWarnings("all") @com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "with",buildMethodName = "execute") class TestVoidName { + private @java.lang.SuppressWarnings("all") T number; + private @java.lang.SuppressWarnings("all") int arg2; + private @java.lang.SuppressWarnings("all") String arg3; + private @java.lang.SuppressWarnings("all") JacksonizedBuilderComplex selfRef; + @java.lang.SuppressWarnings("all") TestVoidName() { + super(); + } + public @java.lang.SuppressWarnings("all") JacksonizedBuilderComplex.TestVoidName withNumber(final T number) { + this.number = number; + return this; + } + public @java.lang.SuppressWarnings("all") JacksonizedBuilderComplex.TestVoidName withArg2(final int arg2) { + this.arg2 = arg2; + return this; + } + public @java.lang.SuppressWarnings("all") JacksonizedBuilderComplex.TestVoidName withArg3(final String arg3) { + this.arg3 = arg3; + return this; + } + public @java.lang.SuppressWarnings("all") JacksonizedBuilderComplex.TestVoidName withSelfRef(final JacksonizedBuilderComplex selfRef) { + this.selfRef = selfRef; + return this; + } + public @java.lang.SuppressWarnings("all") void execute() { + JacksonizedBuilderComplex.testVoidWithGenerics(this.number, this.arg2, this.arg3, this.selfRef); + } + public @java.lang.Override @java.lang.SuppressWarnings("all") java.lang.String toString() { + return (((((((("JacksonizedBuilderComplex.TestVoidName(number=" + this.number) + ", arg2=") + this.arg2) + ", arg3=") + this.arg3) + ", selfRef=") + this.selfRef) + ")"); + } + } + JacksonizedBuilderComplex() { + super(); + } + private static @Jacksonized @Builder(buildMethodName = "execute",setterPrefix = "with") void testVoidWithGenerics(T number, int arg2, String arg3, JacksonizedBuilderComplex selfRef) { + } + public static @java.lang.SuppressWarnings("all") JacksonizedBuilderComplex.TestVoidName builder() { + return new JacksonizedBuilderComplex.TestVoidName(); + } +} diff --git a/test/transform/resource/after-ecj/JacksonizedBuilderSimple.java b/test/transform/resource/after-ecj/JacksonizedBuilderSimple.java new file mode 100644 index 0000000000..2e0a4e0c51 --- /dev/null +++ b/test/transform/resource/after-ecj/JacksonizedBuilderSimple.java @@ -0,0 +1,38 @@ +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +@lombok.extern.jackson.Jacksonized @JsonIgnoreProperties(ignoreUnknown = true) @lombok.Builder(access = lombok.AccessLevel.PROTECTED) @com.fasterxml.jackson.databind.annotation.JsonDeserialize(builder = JacksonizedBuilderSimple.JacksonizedBuilderSimpleBuilder.class) class JacksonizedBuilderSimple { + protected static @java.lang.SuppressWarnings("all") @JsonIgnoreProperties(ignoreUnknown = true) @com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "",buildMethodName = "build") class JacksonizedBuilderSimpleBuilder { + private @java.lang.SuppressWarnings("all") int yes; + private @java.lang.SuppressWarnings("all") List also; + @java.lang.SuppressWarnings("all") JacksonizedBuilderSimpleBuilder() { + super(); + } + public @java.lang.SuppressWarnings("all") JacksonizedBuilderSimple.JacksonizedBuilderSimpleBuilder yes(final int yes) { + this.yes = yes; + return this; + } + public @java.lang.SuppressWarnings("all") JacksonizedBuilderSimple.JacksonizedBuilderSimpleBuilder also(final List also) { + this.also = also; + return this; + } + public @java.lang.SuppressWarnings("all") JacksonizedBuilderSimple build() { + return new JacksonizedBuilderSimple(this.yes, this.also); + } + public @java.lang.Override @java.lang.SuppressWarnings("all") java.lang.String toString() { + return (((("JacksonizedBuilderSimple.JacksonizedBuilderSimpleBuilder(yes=" + this.yes) + ", also=") + this.also) + ")"); + } + } + private final int noshow = 0; + private final int yes; + private List also; + private int $butNotMe; + @java.lang.SuppressWarnings("all") JacksonizedBuilderSimple(final int yes, final List also) { + super(); + this.yes = yes; + this.also = also; + } + protected static @java.lang.SuppressWarnings("all") JacksonizedBuilderSimple.JacksonizedBuilderSimpleBuilder builder() { + return new JacksonizedBuilderSimple.JacksonizedBuilderSimpleBuilder(); + } +} diff --git a/test/transform/resource/after-ecj/JacksonizedSuperBuilderSimple.java b/test/transform/resource/after-ecj/JacksonizedSuperBuilderSimple.java new file mode 100644 index 0000000000..8227162a50 --- /dev/null +++ b/test/transform/resource/after-ecj/JacksonizedSuperBuilderSimple.java @@ -0,0 +1,44 @@ +public class JacksonizedSuperBuilderSimple { + public static @lombok.extern.jackson.Jacksonized @lombok.experimental.SuperBuilder @com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) @com.fasterxml.jackson.databind.annotation.JsonDeserialize(builder = JacksonizedSuperBuilderSimple.Parent.ParentBuilderImpl.class) class Parent { + public static abstract @java.lang.SuppressWarnings("all") class ParentBuilder> { + private @java.lang.SuppressWarnings("all") int field1; + public ParentBuilder() { + super(); + } + protected abstract @java.lang.SuppressWarnings("all") B self(); + public abstract @java.lang.SuppressWarnings("all") C build(); + public @java.lang.SuppressWarnings("all") B field1(final int field1) { + this.field1 = field1; + return self(); + } + public @java.lang.Override @java.lang.SuppressWarnings("all") java.lang.String toString() { + return (("JacksonizedSuperBuilderSimple.Parent.ParentBuilder(field1=" + this.field1) + ")"); + } + } + static final @java.lang.SuppressWarnings("all") @com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) @com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "",buildMethodName = "build") class ParentBuilderImpl extends JacksonizedSuperBuilderSimple.Parent.ParentBuilder { + private ParentBuilderImpl() { + super(); + } + protected @java.lang.Override @java.lang.SuppressWarnings("all") JacksonizedSuperBuilderSimple.Parent.ParentBuilderImpl self() { + return this; + } + public @java.lang.Override @java.lang.SuppressWarnings("all") JacksonizedSuperBuilderSimple.Parent build() { + return new JacksonizedSuperBuilderSimple.Parent(this); + } + } + int field1; + protected @java.lang.SuppressWarnings("all") Parent(final JacksonizedSuperBuilderSimple.Parent.ParentBuilder b) { + super(); + this.field1 = b.field1; + } + public static @java.lang.SuppressWarnings("all") JacksonizedSuperBuilderSimple.Parent.ParentBuilder builder() { + return new JacksonizedSuperBuilderSimple.Parent.ParentBuilderImpl(); + } + } + public JacksonizedSuperBuilderSimple() { + super(); + } + public static void test() { + Parent x = Parent.builder().field1(5).build(); + } +} \ No newline at end of file diff --git a/test/transform/resource/after-ecj/JacksonizedSuperBuilderWithJsonDeserialize.java b/test/transform/resource/after-ecj/JacksonizedSuperBuilderWithJsonDeserialize.java new file mode 100644 index 0000000000..a5677275ba --- /dev/null +++ b/test/transform/resource/after-ecj/JacksonizedSuperBuilderWithJsonDeserialize.java @@ -0,0 +1,36 @@ +public @lombok.extern.jackson.Jacksonized @lombok.experimental.SuperBuilder @com.fasterxml.jackson.databind.annotation.JsonDeserialize class JacksonizedSuperBuilderWithJsonDeserialize { + public static abstract @java.lang.SuppressWarnings("all") class JacksonizedSuperBuilderWithJsonDeserializeBuilder> { + private @java.lang.SuppressWarnings("all") int field1; + public JacksonizedSuperBuilderWithJsonDeserializeBuilder() { + super(); + } + protected abstract @java.lang.SuppressWarnings("all") B self(); + public abstract @java.lang.SuppressWarnings("all") C build(); + public @java.lang.SuppressWarnings("all") B field1(final int field1) { + this.field1 = field1; + return self(); + } + public @java.lang.Override @java.lang.SuppressWarnings("all") java.lang.String toString() { + return (("JacksonizedSuperBuilderWithJsonDeserialize.JacksonizedSuperBuilderWithJsonDeserializeBuilder(field1=" + this.field1) + ")"); + } + } + private static final @java.lang.SuppressWarnings("all") class JacksonizedSuperBuilderWithJsonDeserializeBuilderImpl extends JacksonizedSuperBuilderWithJsonDeserialize.JacksonizedSuperBuilderWithJsonDeserializeBuilder { + private JacksonizedSuperBuilderWithJsonDeserializeBuilderImpl() { + super(); + } + protected @java.lang.Override @java.lang.SuppressWarnings("all") JacksonizedSuperBuilderWithJsonDeserialize.JacksonizedSuperBuilderWithJsonDeserializeBuilderImpl self() { + return this; + } + public @java.lang.Override @java.lang.SuppressWarnings("all") JacksonizedSuperBuilderWithJsonDeserialize build() { + return new JacksonizedSuperBuilderWithJsonDeserialize(this); + } + } + int field1; + protected @java.lang.SuppressWarnings("all") JacksonizedSuperBuilderWithJsonDeserialize(final JacksonizedSuperBuilderWithJsonDeserialize.JacksonizedSuperBuilderWithJsonDeserializeBuilder b) { + super(); + this.field1 = b.field1; + } + public static @java.lang.SuppressWarnings("all") JacksonizedSuperBuilderWithJsonDeserialize.JacksonizedSuperBuilderWithJsonDeserializeBuilder builder() { + return new JacksonizedSuperBuilderWithJsonDeserialize.JacksonizedSuperBuilderWithJsonDeserializeBuilderImpl(); + } +} \ No newline at end of file diff --git a/test/transform/resource/before/JacksonizedBuilderComplex.java b/test/transform/resource/before/JacksonizedBuilderComplex.java new file mode 100644 index 0000000000..dec1cd3bb1 --- /dev/null +++ b/test/transform/resource/before/JacksonizedBuilderComplex.java @@ -0,0 +1,10 @@ +//CONF: lombok.builder.className = Test*Name +import java.util.List; +import lombok.Builder; +import lombok.extern.jackson.Jacksonized; + +class JacksonizedBuilderComplex { + @Jacksonized + @Builder(buildMethodName = "execute", setterPrefix = "with") + private static void testVoidWithGenerics(T number, int arg2, String arg3, JacksonizedBuilderComplex selfRef) {} +} diff --git a/test/transform/resource/before/JacksonizedBuilderSimple.java b/test/transform/resource/before/JacksonizedBuilderSimple.java new file mode 100644 index 0000000000..6da015ecaf --- /dev/null +++ b/test/transform/resource/before/JacksonizedBuilderSimple.java @@ -0,0 +1,12 @@ +import java.util.List; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@lombok.extern.jackson.Jacksonized +@JsonIgnoreProperties(ignoreUnknown = true) +@lombok.Builder(access = lombok.AccessLevel.PROTECTED) +class JacksonizedBuilderSimple { + private final int noshow = 0; + private final int yes; + private List also; + private int $butNotMe; +} diff --git a/test/transform/resource/before/JacksonizedSuperBuilderSimple.java b/test/transform/resource/before/JacksonizedSuperBuilderSimple.java new file mode 100644 index 0000000000..1e3bd0fc7d --- /dev/null +++ b/test/transform/resource/before/JacksonizedSuperBuilderSimple.java @@ -0,0 +1,12 @@ +public class JacksonizedSuperBuilderSimple { + @lombok.extern.jackson.Jacksonized + @lombok.experimental.SuperBuilder + @com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) + public static class Parent { + int field1; + } + + public static void test() { + Parent x = Parent.builder().field1(5).build(); + } +} diff --git a/test/transform/resource/before/JacksonizedSuperBuilderWithJsonDeserialize.java b/test/transform/resource/before/JacksonizedSuperBuilderWithJsonDeserialize.java new file mode 100644 index 0000000000..0c80d1a6c5 --- /dev/null +++ b/test/transform/resource/before/JacksonizedSuperBuilderWithJsonDeserialize.java @@ -0,0 +1,6 @@ +@lombok.extern.jackson.Jacksonized +@lombok.experimental.SuperBuilder +@com.fasterxml.jackson.databind.annotation.JsonDeserialize +public class JacksonizedSuperBuilderWithJsonDeserialize { + int field1; +} diff --git a/test/transform/resource/messages-delombok/JacksonizedSuperBuilderWithJsonDeserialize.java.messages b/test/transform/resource/messages-delombok/JacksonizedSuperBuilderWithJsonDeserialize.java.messages new file mode 100644 index 0000000000..5c69304fed --- /dev/null +++ b/test/transform/resource/messages-delombok/JacksonizedSuperBuilderWithJsonDeserialize.java.messages @@ -0,0 +1 @@ +1 @JsonDeserialize already exists on class. Either delete @JsonDeserialize, or remove @Jacksonized and manually configure Jackson. \ No newline at end of file diff --git a/test/transform/resource/messages-ecj/JacksonizedSuperBuilderWithJsonDeserialize.java.messages b/test/transform/resource/messages-ecj/JacksonizedSuperBuilderWithJsonDeserialize.java.messages new file mode 100644 index 0000000000..5c69304fed --- /dev/null +++ b/test/transform/resource/messages-ecj/JacksonizedSuperBuilderWithJsonDeserialize.java.messages @@ -0,0 +1 @@ +1 @JsonDeserialize already exists on class. Either delete @JsonDeserialize, or remove @Jacksonized and manually configure Jackson. \ No newline at end of file diff --git a/website/templates/features/experimental/Jacksonized.html b/website/templates/features/experimental/Jacksonized.html new file mode 100644 index 0000000000..fd2bfe68ae --- /dev/null +++ b/website/templates/features/experimental/Jacksonized.html @@ -0,0 +1,53 @@ +<#import "../_features.html" as f> + +<@f.scaffold title="@Jacksonized" logline="Make Jackson use your builders."> + <@f.history> +

+ @Jacksonized was introduced as experimental feature in lombok v1.18.14. +

+ + + <@f.overview> +

+ The @Jacksonized annotation is an add-on annotation for @Builder and @SuperBuilder. + It automatically configures the generated builder class to be used by Jackson's deserialization. + It only has an effect if present at a context where there is also a @Builder or a @SuperBuilder; a warning is emitted otherwise. +

+ Without @Jacksonized, you would have to customize your builder class(es). + With @Jacksonized, you can simply write something like this to let Jackson use the generated builder:

+@Jacksonized @Builder
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class JacksonExample {
+	private List<Foo> foos;
+}
+
+

+ This annotation is especially useful when deserializing into immutable (sub-)classes that only use @SuperBuilder to create instances. + With @Jacksonized, you do not have to put the complex @SuperBuilder class header into your code just to configure it for Jackson. +

+ This annotation does not change the behavior of the generated builder. + A @Jacksonized @SuperBuilder remains fully compatible to regular @SuperBuilders. +

+ + + <@f.smallPrint> +

+ In particular, the annotation does the following: +

    +
  • + Configure Jackson to use the builder for deserialization using @JsonDeserialize(builder=Foobar.FoobarBuilder[Impl].class)) on the class (where Foobar is the name of the annotated class). + (An error is emitted if such an annotation already exists.) +
  • + Copy Jackson-related configuration annotations (like @JsonIgnoreProperties) from the class to the builder class. + This is necessary so that Jackson recognizes them when using the builder. +
  • + Insert @JsonPOJOBuilder(withPrefix="") on the generated builder class to override Jackson's default prefix "with". + If you configured a different prefix in lombok using setterPrefix, this value is used. + If you changed the name of the build() method using using buildMethodName, this is also made known to Jackson. +
  • + For @SuperBuilder, make the builder implementation class package-private. +
  • +
+

+ + diff --git a/website/templates/features/experimental/index.html b/website/templates/features/experimental/index.html index b158d381f9..dc7870cf52 100644 --- a/website/templates/features/experimental/index.html +++ b/website/templates/features/experimental/index.html @@ -71,6 +71,10 @@ <@main.feature title="@Tolerate" href="Tolerate"> Skip, jump, and forget! Make lombok disregard an existing method or constructor. + + <@main.feature title="@Jacksonized" href="Jacksonized"> + Make Jackson use your builders. + <@f.confKeys>