From 4a41472fbc82f13248260cf17721dd843d18ccfc Mon Sep 17 00:00:00 2001 From: Sergey Rukin Date: Mon, 17 Jun 2024 21:42:00 +0500 Subject: [PATCH 1/3] [#3437] Add @JpaAssociationSync annotation --- src/core/lombok/ConfigurationKeys.java | 7 + src/core/lombok/core/AnnotationValues.java | 4 + .../configuration/CapitalizationStrategy.java | 25 +- .../lombok/core/handlers/HandlerUtil.java | 42 +- .../lombok/eclipse/EclipseASTVisitor.java | 132 ++++ .../eclipse/handlers/EclipseHandlerUtil.java | 14 + .../handlers/HandleJpaAssociationSync.java | 562 ++++++++++++++++ .../experimental/JpaAssociationSync.java | 133 ++++ src/core/lombok/javac/JavacASTVisitor.java | 132 ++++ src/core/lombok/javac/JavacNode.java | 6 +- .../handlers/HandleJpaAssociationSync.java | 603 ++++++++++++++++++ .../javac/handlers/JavacHandlerUtil.java | 13 +- src/utils/lombok/javac/JavacTreeMaker.java | 3 + 13 files changed, 1670 insertions(+), 6 deletions(-) create mode 100644 src/core/lombok/eclipse/handlers/HandleJpaAssociationSync.java create mode 100644 src/core/lombok/experimental/JpaAssociationSync.java create mode 100644 src/core/lombok/javac/handlers/HandleJpaAssociationSync.java diff --git a/src/core/lombok/ConfigurationKeys.java b/src/core/lombok/ConfigurationKeys.java index b19dccfc98..261ed1e34e 100644 --- a/src/core/lombok/ConfigurationKeys.java +++ b/src/core/lombok/ConfigurationKeys.java @@ -743,4 +743,11 @@ private ConfigurationKeys() {} * If set, any usage of {@code @StandardException} results in a warning / error. */ public static final ConfigurationKey STANDARD_EXCEPTION_FLAG_USAGE = new ConfigurationKey("lombok.standardException.flagUsage", "Emit a warning or error if @StandardException is used.") {}; + + /** + * lombok configuration: {@code lombok.jpaAssociationSync.flagUsage} = {@code WARNING} | {@code ERROR}. + * + * If set, any usage of {@code @JpaAssociationSync} results in a warning / error. + */ + public static final ConfigurationKey JPA_ASSOCIATION_SYNC_FLAG_USAGE = new ConfigurationKey("lombok.jpaAssociationSync.flagUsage", "Emit a warning or error if @JpaAssociationSync is used.") {}; } diff --git a/src/core/lombok/core/AnnotationValues.java b/src/core/lombok/core/AnnotationValues.java index 390e9b71b3..01d80be92f 100644 --- a/src/core/lombok/core/AnnotationValues.java +++ b/src/core/lombok/core/AnnotationValues.java @@ -571,4 +571,8 @@ public boolean isMarking() { for (AnnotationValue v : values.values()) if (v.isExplicit) return false; return true; } + + public Class getType() { + return this.type; + } } diff --git a/src/core/lombok/core/configuration/CapitalizationStrategy.java b/src/core/lombok/core/configuration/CapitalizationStrategy.java index affd357646..945611450a 100644 --- a/src/core/lombok/core/configuration/CapitalizationStrategy.java +++ b/src/core/lombok/core/configuration/CapitalizationStrategy.java @@ -25,28 +25,47 @@ public enum CapitalizationStrategy { BASIC { @Override public String capitalize(String in) { - if (in.length() == 0) return in; + if (in.isEmpty()) return in; char first = in.charAt(0); if (!Character.isLowerCase(first)) return in; boolean useUpperCase = in.length() > 2 && (Character.isTitleCase(in.charAt(1)) || Character.isUpperCase(in.charAt(1))); return (useUpperCase ? Character.toUpperCase(first) : Character.toTitleCase(first)) + in.substring(1); } + + @Override public String uncapitalize(String in) { + if (in.isEmpty()) return in; + char first = in.charAt(0); + if (!Character.isUpperCase(first)) return in; + boolean useLowerCase = in.length() > 2 && + (Character.isTitleCase(in.charAt(1)) || Character.isLowerCase(in.charAt(1))); + return (useLowerCase ? Character.toLowerCase(first) : Character.toTitleCase(first)) + in.substring(1); + } }, BEANSPEC { @Override public String capitalize(String in) { - if (in.length() == 0) return in; + if (in.isEmpty()) return in; char first = in.charAt(0); if (!Character.isLowerCase(first) || (in.length() > 1 && Character.isUpperCase(in.charAt(1)))) return in; boolean useUpperCase = in.length() > 2 && Character.isTitleCase(in.charAt(1)); return (useUpperCase ? Character.toUpperCase(first) : Character.toTitleCase(first)) + in.substring(1); } + + @Override public String uncapitalize(String in) { + if (in.isEmpty()) return in; + char first = in.charAt(0); + if (!Character.isUpperCase(first) || (in.length() > 1 && Character.isLowerCase(in.charAt(1)))) return in; + boolean useLowerCase = in.length() > 2 && Character.isTitleCase(in.charAt(1)); + return (useLowerCase ? Character.isLowerCase(first) : Character.toTitleCase(first)) + in.substring(1); + } }, ; public static CapitalizationStrategy defaultValue() { return BASIC; } - + public abstract String capitalize(String in); + + public abstract String uncapitalize(String in); } diff --git a/src/core/lombok/core/handlers/HandlerUtil.java b/src/core/lombok/core/handlers/HandlerUtil.java index 06bac16fdd..8816b3486b 100644 --- a/src/core/lombok/core/handlers/HandlerUtil.java +++ b/src/core/lombok/core/handlers/HandlerUtil.java @@ -883,7 +883,47 @@ public static String camelCaseToConstant(String fieldName) { } return b.toString(); } - + + /** + * @param node Any node (used to fetch config of capitalization strategy). + * @param str any string. + * @return string with uppercased first char. + */ + public static String capitalize(LombokNode node, String str) { + CapitalizationStrategy capitalizationStrategy = node.getAst().readConfigurationOr(ConfigurationKeys.ACCESSORS_JAVA_BEANS_SPEC_CAPITALIZATION, CapitalizationStrategy.defaultValue()); + return capitalize(str, capitalizationStrategy); + } + + /** + * @param str any string. + * @return string with uppercased first char. + */ + public static String capitalize(String str, CapitalizationStrategy capitalizationStrategy) { + if (str == null || str.isEmpty()) return str; + + return capitalizationStrategy.capitalize(str); + } + + /** + * @param node Any node (used to fetch config of capitalization strategy). + * @param str any string. + * @return string with lowercased first char. + */ + public static String uncapitalize(LombokNode node, String str) { + CapitalizationStrategy capitalizationStrategy = node.getAst().readConfigurationOr(ConfigurationKeys.ACCESSORS_JAVA_BEANS_SPEC_CAPITALIZATION, CapitalizationStrategy.defaultValue()); + return uncapitalize(str, capitalizationStrategy); + } + + /** + * @param str any string. + * @return string with lowercased first char. + */ + public static String uncapitalize(String str, CapitalizationStrategy capitalizationStrategy) { + if (str == null || str.isEmpty()) return str; + + return capitalizationStrategy.uncapitalize(str); + } + /** Matches any of the 8 primitive wrapper names, such as {@code Boolean}. */ private static final Pattern PRIMITIVE_WRAPPER_TYPE_NAME_PATTERN = Pattern.compile("^(?:java\\.lang\\.)?(?:Boolean|Byte|Short|Integer|Long|Float|Double|Character)$"); diff --git a/src/core/lombok/eclipse/EclipseASTVisitor.java b/src/core/lombok/eclipse/EclipseASTVisitor.java index 0bd668bc04..2168e6f6a6 100644 --- a/src/core/lombok/eclipse/EclipseASTVisitor.java +++ b/src/core/lombok/eclipse/EclipseASTVisitor.java @@ -465,5 +465,137 @@ public boolean isDeferUntilPostDiet() { } } + /** + * A default implementation of the {@link EclipseASTVisitor} interface. + * This class provides empty method implementations for all the methods in the {@link EclipseASTVisitor} interface. + * Subclasses can override the methods they are interested in. + */ + public static abstract class Default implements EclipseASTVisitor { + @Override + public void visitCompilationUnit(EclipseNode top, CompilationUnitDeclaration unit) { + + } + + @Override + public void endVisitCompilationUnit(EclipseNode top, CompilationUnitDeclaration unit) { + + } + + @Override + public void visitType(EclipseNode typeNode, TypeDeclaration type) { + + } + + @Override + public void visitAnnotationOnType(TypeDeclaration type, EclipseNode annotationNode, Annotation annotation) { + + } + + @Override + public void endVisitType(EclipseNode typeNode, TypeDeclaration type) { + + } + + @Override + public void visitField(EclipseNode fieldNode, FieldDeclaration field) { + + } + + @Override + public void visitAnnotationOnField(FieldDeclaration field, EclipseNode annotationNode, Annotation annotation) { + + } + + @Override + public void endVisitField(EclipseNode fieldNode, FieldDeclaration field) { + + } + + @Override + public void visitInitializer(EclipseNode initializerNode, Initializer initializer) { + + } + + @Override + public void endVisitInitializer(EclipseNode initializerNode, Initializer initializer) { + + } + + @Override + public void visitMethod(EclipseNode methodNode, AbstractMethodDeclaration method) { + + } + + @Override + public void visitAnnotationOnMethod(AbstractMethodDeclaration method, EclipseNode annotationNode, Annotation annotation) { + + } + + @Override + public void endVisitMethod(EclipseNode methodNode, AbstractMethodDeclaration method) { + + } + + @Override + public void visitMethodArgument(EclipseNode argNode, Argument arg, AbstractMethodDeclaration method) { + + } + + @Override + public void visitAnnotationOnMethodArgument(Argument arg, AbstractMethodDeclaration method, EclipseNode annotationNode, Annotation annotation) { + + } + + @Override + public void endVisitMethodArgument(EclipseNode argNode, Argument arg, AbstractMethodDeclaration method) { + + } + + @Override + public void visitLocal(EclipseNode localNode, LocalDeclaration local) { + + } + + @Override + public void visitAnnotationOnLocal(LocalDeclaration local, EclipseNode annotationNode, Annotation annotation) { + + } + + @Override + public void endVisitLocal(EclipseNode localNode, LocalDeclaration local) { + + } + + @Override + public void visitTypeUse(EclipseNode typeUseNode, TypeReference typeUse) { + + } + + @Override + public void visitAnnotationOnTypeUse(TypeReference typeUse, EclipseNode annotationNode, Annotation annotation) { + + } + + @Override + public void endVisitTypeUse(EclipseNode typeUseNode, TypeReference typeUse) { + + } + + @Override + public void visitStatement(EclipseNode statementNode, Statement statement) { + + } + + @Override + public void endVisitStatement(EclipseNode statementNode, Statement statement) { + + } + + @Override + public boolean isDeferUntilPostDiet() { + return false; + } + } + boolean isDeferUntilPostDiet(); } diff --git a/src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java b/src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java index af1aa0fd82..a2ea308651 100644 --- a/src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java +++ b/src/core/lombok/eclipse/handlers/EclipseHandlerUtil.java @@ -2992,4 +2992,18 @@ public static boolean hasParsedBody(EclipseNode method) { } return false; } + + public static List getGenericTypes(TypeReference fieldType) { + List result = new ArrayList(); + + TypeReference[][] typeArguments = fieldType.getTypeArguments(); + if (typeArguments != null) { + for (TypeReference[] arg : typeArguments) { + if (arg == null) continue; + result.addAll(Arrays.asList(arg)); + } + } + + return result; + } } diff --git a/src/core/lombok/eclipse/handlers/HandleJpaAssociationSync.java b/src/core/lombok/eclipse/handlers/HandleJpaAssociationSync.java new file mode 100644 index 0000000000..2a3e1ea3bd --- /dev/null +++ b/src/core/lombok/eclipse/handlers/HandleJpaAssociationSync.java @@ -0,0 +1,562 @@ +/* + * Copyright (C) 2024 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.*; +import static lombok.eclipse.Eclipse.ECLIPSE_DO_NOT_TOUCH_FLAG; +import static lombok.eclipse.handlers.EclipseHandlerUtil.*; + +import lombok.ConfigurationKeys; +import lombok.core.AST; +import lombok.core.AnnotationValues; +import lombok.eclipse.EclipseASTVisitor; +import lombok.eclipse.EclipseAnnotationHandler; +import lombok.eclipse.EclipseNode; +import lombok.experimental.JpaAssociationSync; +import lombok.spi.Provides; +import org.eclipse.jdt.internal.compiler.ast.*; +import org.eclipse.jdt.internal.compiler.impl.Constant; +import org.eclipse.jdt.internal.compiler.lookup.*; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Handles the {@code lombok.experimental.JpaAssociationSync} annotation for eclipse. + */ +@Provides +public class HandleJpaAssociationSync extends EclipseAnnotationHandler { + private static final String NO_JPA_PACKAGE_IN_CLASSPATH_ERROR = "@JpaAssociationSync requires JPA in classpath"; + private static final String NODE_NOT_SUPPORTED_ERROR = "@JpaAssociationSync is only supported on a class or a field."; + + private static final String ONE_TO_MANY_CLASS_NAME = "OneToMany"; + private static final String MANY_TO_ONE_CLASS_NAME = "ManyToOne"; + private static final String ONE_TO_ONE_CLASS_NAME = "OneToOne"; + private static final String MANY_TO_MANY_CLASS_NAME = "ManyToMany"; + + private static final String MAPPED_BY_METHOD_NAME = "mappedBy"; + + private static final char[] ADD_METHOD_PREFIX = "add".toCharArray(); + private static final char[] REMOVE_METHOD_PREFIX = "remove".toCharArray(); + private static final char[] UPDATE_METHOD_PREFIX = "update".toCharArray(); + + @Override + public void handle(AnnotationValues annotation, Annotation ast, EclipseNode annotationNode) { + handleExperimentalFlagUsage(annotationNode, ConfigurationKeys.JPA_ASSOCIATION_SYNC_FLAG_USAGE, "@JpaAssociationSync"); + + EclipseNode node = annotationNode.up(); + EclipseNode sourceNode = annotationNode; + + String jpaPackageName = getJpaPackageName(); + + if (jpaPackageName == null) { + sourceNode.addError(NO_JPA_PACKAGE_IN_CLASSPATH_ERROR); + return; + } + + switch (node.getKind()) { + case FIELD: + generateAssociationSyncMethodsForField(node, sourceNode, jpaPackageName); + break; + case TYPE: + generateAssociationSyncMethodsForType(node, sourceNode, jpaPackageName); + break; + } + } + + private void generateAssociationSyncMethodsForType(EclipseNode typeNode, + final EclipseNode sourceNode, + final String jpaPackageName) { + if (!isClass(typeNode)) { + sourceNode.addError(NODE_NOT_SUPPORTED_ERROR); + return; + } + + typeNode.traverse(new EclipseASTVisitor.Default() { + @Override + public void visitField(EclipseNode fieldNode, FieldDeclaration field) { + if (hasAnnotation(JpaAssociationSync.class, fieldNode)) { + return; + } + + generateAssociationSyncMethodsForField(fieldNode, sourceNode, jpaPackageName); + } + }); + } + + private void generateAssociationSyncMethodsForField(EclipseNode fieldNode, + EclipseNode sourceNode, + String jpaPackageName) { + if (fieldNode.getKind() != AST.Kind.FIELD) { + sourceNode.addError(NODE_NOT_SUPPORTED_ERROR); + return; + } + + EclipseNode classNode = fieldNode.up(); + + Class oneToManyClass + = findAnnotationClass(jpaPackageName + "." + ONE_TO_MANY_CLASS_NAME); + Class oneToOneClass + = findAnnotationClass(jpaPackageName + "." + ONE_TO_ONE_CLASS_NAME); + Class manyToManyClass + = findAnnotationClass(jpaPackageName + "." + MANY_TO_MANY_CLASS_NAME); + + List> annotationClasses = new ArrayList>(3); + annotationClasses.add(oneToManyClass); + annotationClasses.add(oneToOneClass); + annotationClasses.add(manyToManyClass); + + List associationDetailsList + = getAssociationDetailsList(fieldNode, sourceNode, annotationClasses); + + for (AssociationDetails associationDetails : associationDetailsList) { + if (associationDetails.getType() != null) { + if (associationDetails.getType().equals(oneToManyClass)) { + generateOneToManyAssociationSyncMethodsForField(classNode, fieldNode, sourceNode, associationDetails); + } else if (associationDetails.getType().equals(oneToOneClass)) { + generateOneToOneAssociationSyncMethodsForField(classNode, fieldNode, sourceNode, associationDetails); + } else if (associationDetails.getType().equals(manyToManyClass)) { + generateManyToManyAssociationSyncMethodsForField(classNode, fieldNode, sourceNode, associationDetails); + } + } + } + } + + private void generateManyToManyAssociationSyncMethodsForField(EclipseNode classNode, + EclipseNode fieldNode, + EclipseNode sourceNode, + AssociationDetails associationDetails) { + injectMethod( + classNode, + createMethodForManyToManyAssociation(classNode, fieldNode, sourceNode, associationDetails, ADD_METHOD_PREFIX) + ); + + injectMethod( + classNode, + createMethodForManyToManyAssociation(classNode, fieldNode, sourceNode, associationDetails, REMOVE_METHOD_PREFIX) + ); + } + + private void generateOneToOneAssociationSyncMethodsForField(EclipseNode classNode, + EclipseNode fieldNode, + EclipseNode sourceNode, + AssociationDetails associationDetails) { + injectMethod( + classNode, + createMethodForOneToOneAssociation(classNode, fieldNode, sourceNode, associationDetails, UPDATE_METHOD_PREFIX) + ); + } + + private void generateOneToManyAssociationSyncMethodsForField(EclipseNode classNode, + EclipseNode fieldNode, + EclipseNode sourceNode, + AssociationDetails associationDetails) { + injectMethod( + classNode, + createMethodForOneToManyAssociation(classNode, fieldNode, sourceNode, associationDetails, ADD_METHOD_PREFIX) + ); + + injectMethod( + classNode, + createMethodForOneToManyAssociation(classNode, fieldNode, sourceNode, associationDetails, REMOVE_METHOD_PREFIX) + ); + } + + private MethodDeclaration createMethodForManyToManyAssociation(EclipseNode classNode, + EclipseNode fieldNode, + EclipseNode sourceNode, + AssociationDetails associationDetails, + char[] methodPrefix) { + ASTNode source = sourceNode.get(); + FieldDeclaration fieldDecl = (FieldDeclaration) fieldNode.get(); + int pS = source.sourceStart, pE = source.sourceEnd; + long p = (long) pS << 32 | pE; + + TypeReference paramType = getFieldTypeOrGenericType(fieldDecl); + char[] paramName = associationDetails.paramName != null + ? associationDetails.paramName.toCharArray() + : uncapitalize(sourceNode, String.valueOf(paramType.getLastToken())).toCharArray(); + Argument param = createParameter(paramName, p, paramType, 0); + param.sourceStart = pS; param.sourceEnd = pE; + + List statements = new ArrayList(); + statements.add( + executeMethod( + fieldNode, + sourceNode, + methodPrefix, + new Expression[] { new SingleNameReference(param.name, p) } + ) + ); + statements.add( + executeMethod( + executeMethod( + new SingleNameReference(param.name, p), + buildAccessorName( + sourceNode, + "get", + associationDetails.inverseSideAnnotation != null + ? associationDetails.owningSideFieldName + : associationDetails.inverseSideFieldName + ).toCharArray(), + new Expression[] {} + ), + methodPrefix, + new Expression[] { new ThisReference(pS, pE) } + ) + ); + + return createMethod(classNode, sourceNode, param, statements.toArray(new Statement[0]), methodPrefix); + } + + private MethodDeclaration createMethodForOneToOneAssociation(EclipseNode classNode, + EclipseNode fieldNode, + EclipseNode sourceNode, + AssociationDetails associationDetails, + char[] methodPrefix) { + ASTNode source = sourceNode.get(); + FieldDeclaration fieldDecl = (FieldDeclaration) fieldNode.get(); + int pS = source.sourceStart, pE = source.sourceEnd; + long p = (long) pS << 32 | pE; + + TypeReference paramType = getFieldTypeOrGenericType(fieldDecl); + char[] paramName = associationDetails.paramName != null + ? associationDetails.paramName.toCharArray() + : uncapitalize(sourceNode, String.valueOf(paramType.getLastToken())).toCharArray(); + Argument param = createParameter(paramName, p, paramType, 0); + param.sourceStart = pS; param.sourceEnd = pE; + + List statements = new ArrayList(); + + List ifStatements = new ArrayList(); + + List if1Statements = new ArrayList();; + if1Statements.add( + executeMethod( + fieldNode, + sourceNode, + buildAccessorName( + sourceNode, + "set", + associationDetails.inverseSideAnnotation != null + ? associationDetails.owningSideFieldName + : associationDetails.inverseSideFieldName + ).toCharArray(), + new Expression[]{ new NullLiteral(pS, pE) } + ) + ); + Block if1Block = new Block(0); + if1Block.statements = if1Statements.toArray(new Statement[0]); + + ifStatements.add( + new IfStatement( + new EqualExpression( + createFieldAccessor(fieldNode, FieldAccess.ALWAYS_FIELD, sourceNode.get()), + new NullLiteral(pS, pE), + Constant.NOT_EQUAL + ), + if1Block, + pS, pE + ) + ); + Block ifBlock = new Block(0); + ifBlock.statements = ifStatements.toArray(new Statement[0]); + + List elseStatements = new ArrayList(); + elseStatements.add( + executeMethod( + new SingleNameReference(param.name, p), + buildAccessorName( + sourceNode, + "set", + associationDetails.inverseSideAnnotation != null + ? associationDetails.owningSideFieldName + : associationDetails.inverseSideFieldName + ).toCharArray(), + new Expression[]{ new ThisReference(pS, pE) } + ) + ); + Block elseBlock = new Block(0); + elseBlock.statements = elseStatements.toArray(new Statement[0]); + + statements.add( + new IfStatement( + new EqualExpression( + new SingleNameReference(param.name, p), + new NullLiteral(pS, pE), + Constant.EQUAL_EQUAL + ), + ifBlock, + elseBlock, + pS, pE + ) + ); + + statements.add( + new Assignment( + createFieldAccessor(fieldNode, FieldAccess.ALWAYS_FIELD, sourceNode.get()), + new SingleNameReference(param.name, p), + pE + ) + ); + + return createMethod(classNode, sourceNode, param, statements.toArray(new Statement[0]), methodPrefix); + } + + private MethodDeclaration createMethodForOneToManyAssociation(EclipseNode classNode, + EclipseNode fieldNode, + EclipseNode sourceNode, + AssociationDetails associationDetails, + char[] methodPrefix) { + ASTNode source = sourceNode.get(); + FieldDeclaration fieldDecl = (FieldDeclaration) fieldNode.get(); + int pS = source.sourceStart, pE = source.sourceEnd; + long p = (long) pS << 32 | pE; + + TypeReference paramType = getFieldTypeOrGenericType(fieldDecl); + char[] paramName = associationDetails.paramName != null + ? associationDetails.paramName.toCharArray() + : uncapitalize(sourceNode, String.valueOf(paramType.getLastToken())).toCharArray(); + Argument param = createParameter(paramName, p, paramType, 0); + param.sourceStart = pS; param.sourceEnd = pE; + + List statements = new ArrayList(); + statements.add( + executeMethod( + fieldNode, + sourceNode, + methodPrefix, + new Expression[] { new SingleNameReference(param.name, p) } + ) + ); + statements.add( + executeMethod( + new SingleNameReference(param.name, p), + buildAccessorName(sourceNode, "set", associationDetails.owningSideFieldName).toCharArray(), + new Expression[] { + Arrays.equals(methodPrefix, ADD_METHOD_PREFIX) + ? new ThisReference(pS, pE) + : new NullLiteral(pS, pE) + } + ) + ); + + return createMethod(classNode, sourceNode, param, statements.toArray(new Statement[0]), methodPrefix); + } + + private MethodDeclaration createMethod(EclipseNode classNode, + EclipseNode sourceNode, + Argument param, + Statement[] statements, + char[] methodPrefix) { + MethodDeclaration methodDecl = new MethodDeclaration(((TypeDeclaration) classNode.get()).compilationResult); + + ASTNode source = sourceNode.get(); + int pS = source.sourceStart, pE = source.sourceEnd; + + methodDecl.modifiers = Modifier.PUBLIC; + methodDecl.returnType = TypeReference.baseTypeReference(TypeIds.T_void, 0); + methodDecl.returnType.sourceStart = pS; methodDecl.returnType.sourceEnd = pE; + methodDecl.arguments = new Argument[] { param }; + methodDecl.selector = buildAccessorName(sourceNode, String.valueOf(methodPrefix), String.valueOf(param.name)).toCharArray(); + methodDecl.binding = null; + methodDecl.thrownExceptions = null; + methodDecl.typeParameters = null; + methodDecl.bits |= ECLIPSE_DO_NOT_TOUCH_FLAG; + methodDecl.statements = statements; + + methodDecl.traverse(new SetGeneratedByVisitor(source), ((TypeDeclaration) classNode.get()).scope); + + return methodDecl; + } + + private Expression executeMethod(Expression field, + char[] methodName, + Expression[] arguments) { + MessageSend messageSend = new MessageSend(); + messageSend.receiver = field; + messageSend.selector = methodName; + messageSend.arguments = arguments; + + return messageSend; + } + + private Expression executeMethod(EclipseNode fieldNode, + EclipseNode sourceNode, + char[] methodName, + Expression[] arguments) { + MessageSend messageSend = new MessageSend(); + messageSend.receiver = createFieldAccessor(fieldNode, FieldAccess.ALWAYS_FIELD, sourceNode.get()); + messageSend.selector = methodName; + messageSend.arguments = arguments; + + return messageSend; + } + + private Argument createParameter(char[] paramName, + long pos, + TypeReference paramType, + int modifiers) { + return new Argument(paramName, pos, paramType, modifiers); + } + + private TypeReference getFieldTypeOrGenericType(FieldDeclaration fieldDeclaration) { + TypeReference fieldType = fieldDeclaration.type; + + if (fieldType instanceof ParameterizedQualifiedTypeReference + || fieldType instanceof ParameterizedSingleTypeReference) { + return copyType( + getGenericTypes(fieldType).get(0) + ); + } + + return copyType(fieldType); + } + + private List getAssociationDetailsList(EclipseNode fieldNode, + EclipseNode sourceNode, + List> annotationClasses) { + TypeReference fieldType = getFieldTypeOrGenericType((FieldDeclaration) fieldNode.get()); + + AnnotationValues extraSettings = fieldNode.findAnnotation(JpaAssociationSync.Extra.class); + + List> associationAnnotations = getAssociationAnnotations(fieldNode, annotationClasses); + + List associationDetailsListBuffer = new ArrayList(); + for (AnnotationValues annotation : associationAnnotations) { + if (AssociationDetails.isOwningSide(annotation)) { + if (extraSettings != null + && !extraSettings.getAsString("inverseSideFieldName").isEmpty()) { + AssociationDetails associationDetails = new AssociationDetails(); + + associationDetails.owningSideAnnotation = annotation; + + associationDetails.owningSideFieldName = uncapitalize(sourceNode, String.valueOf(fieldType)); + associationDetails.inverseSideFieldName = extraSettings.getAsString("inverseSideFieldName"); + + associationDetails.paramName = + !extraSettings.getAsString("paramName").isEmpty() + ? extraSettings.getAsString("paramName") + : null; + + associationDetailsListBuffer.add(associationDetails); + } + } else { + AssociationDetails associationDetails = new AssociationDetails(); + + associationDetails.inverseSideAnnotation = annotation; + + associationDetails.owningSideFieldName = AssociationDetails.getOwningSideFieldName(annotation); + associationDetails.inverseSideFieldName = uncapitalize(sourceNode, String.valueOf(fieldType)); + + associationDetails.paramName = + extraSettings != null && !extraSettings.getAsString("paramName").isEmpty() + ? extraSettings.getAsString("paramName") + : null; + + associationDetailsListBuffer.add(associationDetails); + } + } + + return associationDetailsListBuffer; + } + + private List> getAssociationAnnotations(EclipseNode fieldNode, + List> annotationClasses) { + List> annotations = new ArrayList>(); + + for (Class annotationClass : annotationClasses) { + if (annotationClass != null) { + AnnotationValues annotation = fieldNode.findAnnotation(annotationClass); + + if (annotation != null) { + annotations.add(annotation); + } + } + } + + return annotations; + } + + private Class findAnnotationClass(String className) { + try { + return Class.forName(className).asSubclass(java.lang.annotation.Annotation.class); + } catch (ClassNotFoundException e) { + return null; + } + } + + private String getJpaPackageName() { + try { + Class.forName("javax.persistence.Entity"); + return "javax.persistence"; + } catch (ClassNotFoundException e0) { + try { + Class.forName("jakarta.persistence.Entity"); + return "jakarta.persistence"; + } catch (ClassNotFoundException e1) { + return null; + } + } + } + + private static class AssociationDetails { + AnnotationValues owningSideAnnotation; + AnnotationValues inverseSideAnnotation; + + String owningSideFieldName; + String inverseSideFieldName; + + String paramName; + + AssociationDetails() { + } + + AssociationDetails(String owningSideFieldName, String inverseSideFieldName) { + this.owningSideFieldName = owningSideFieldName; + this.inverseSideFieldName = inverseSideFieldName; + } + + Class getType() { + if (owningSideAnnotation != null) + return owningSideAnnotation.getType(); + if (inverseSideAnnotation != null) + return inverseSideAnnotation.getType(); + + return null; + } + + static String getOwningSideFieldName(AnnotationValues inverseSideAnnotation) { + return inverseSideAnnotation.getAsString(MAPPED_BY_METHOD_NAME); + } + + static boolean isOwningSide(AnnotationValues annotation) { + return annotation.getAsString(MAPPED_BY_METHOD_NAME).isEmpty(); + } + + static boolean isInverseSide(AnnotationValues annotation) { + return !isOwningSide(annotation); + } + } +} diff --git a/src/core/lombok/experimental/JpaAssociationSync.java b/src/core/lombok/experimental/JpaAssociationSync.java new file mode 100644 index 0000000000..679aa253da --- /dev/null +++ b/src/core/lombok/experimental/JpaAssociationSync.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2024 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.experimental; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * {@code @JpaAssociationSync} is a Lombok annotation used to facilitate the synchronization + * of JPA associations, particularly in the context of bidirectional relationships. + *

+ * This annotation can be applied at both the class and field level. When applied, it generates + * helper methods to manage JPA associations, ensuring that both sides of a bidirectional + * relationship remain consistent. + *

+ *

+ * The methods is generated according to Vlad Mihalcea article + *

+ * + *

Example 1

+ *
+ * @Entity
+ * public class ExampleEntity {
+ *     @JpaAssociationSync
+ *     @OneToMany(mappedBy = "exampleEntities")
+ *     private List<RelatedEntity> relatedEntity;
+ *
+ *     @JpaAssociationSync
+ *     @JpaAssociationSync.Extra(paramName = "someRelatedField")
+ *     @OneToOne(mappedBy = "exampleEntity")
+ *     private AnotherRelatedEntity anotherRelatedEntity;
+ *
+ *     // other fields, getters, and setters
+ * }
+ * 
+ * + *

Example 2

+ *
+ * @JpaAssociationSync
+ * @Entity
+ * public class ExampleEntity {
+ *     @OneToOne(mappedBy = "exampleEntity")
+ *     private RelatedEntity relatedEntity;
+ *
+ *     // other fields, getters, and setters
+ * }
+ * 
+ * + * @see lombok.experimental.JpaAssociationSync.Extra + * + * @see jakarta.persistence.OneToMany + * @see jakarta.persistence.OneToOne + * @see jakarta.persistence.ManyToMany + * @see javax.persistence.OneToMany + * @see javax.persistence.OneToOne + * @see javax.persistence.ManyToMany + */ +@Target({ElementType.TYPE, ElementType.FIELD}) +@Retention(RetentionPolicy.SOURCE) +public @interface JpaAssociationSync { + + /** + * {@code @JpaAssociationSync.Extra} is a nested annotation that provides additional + * configuration options for the {@code @JpaAssociationSync} annotation when applied at the field level. + *

+ * This annotation allows for specifying the parameter name for generated methods and the name of + * the field on the inverse side of a bidirectional relationship. + *

+ * + *

Parameters

+ *
    + *
  • paramName: Specifies the name of the parameter used in the generated methods. + * If not specified, the default is the name of the field's type.
  • + *
  • inverseSideFieldName: Necessary on the owning side of bidirectional {@code @OneToOne} or + * {@code @ManyToMany} associations. Indicates the name of the field on the inverse side of the relationship. + * On the inverse side, this parameter is redundant if the {@code mappedBy} attribute is used on the + * association annotation.
  • + *
+ * + *

Example

+ *
+     * @JpaAssociationSync
+     * @Entity
+     * public class ExampleEntity {
+     *     @JpaAssociationSync.Extra(paramName = "entity", inverseSideFieldName = "exampleEntities")
+     *     @ManyToMany
+     *     private Set<RelatedEntity> relatedEntities;
+     *
+     *     // other fields, getters, and setters
+     * }
+     * 
+ */ + @Target({ElementType.FIELD}) + @Retention(RetentionPolicy.SOURCE) + public @interface Extra { + /** + * @return Tells lombok the name of the parameter for jpa association sync methods, + * by default - the name of the type. Also affects the method names + */ + String paramName() default ""; + + /** + * Necessary only on the owning side of bidirectional {@code @OneToOne} or {@code @ManyToMany} associations. + * On the inverse side the similar parameter is redundant, + * because the parameter {@code mappedBy} on the association annotation is enough. + * + * @return Tells lombok the name of the field on the inverse side. + */ + String inverseSideFieldName() default ""; + } + +} diff --git a/src/core/lombok/javac/JavacASTVisitor.java b/src/core/lombok/javac/JavacASTVisitor.java index e8fd295c5a..ebe038a6de 100644 --- a/src/core/lombok/javac/JavacASTVisitor.java +++ b/src/core/lombok/javac/JavacASTVisitor.java @@ -316,4 +316,136 @@ private String printFlags(long f) { print("", statement.getClass()); } } + + /** + * A default implementation of the {@link JavacASTVisitor} interface. + * This class provides empty method implementations for all the methods in the {@link JavacASTVisitor} interface. + * Subclasses can override the methods they are interested in. + */ + public static abstract class Default implements JavacASTVisitor { + @Override + public void setTrees(Trees trees) { + + } + + @Override + public void visitCompilationUnit(JavacNode top, JCCompilationUnit unit) { + + } + + @Override + public void endVisitCompilationUnit(JavacNode top, JCCompilationUnit unit) { + + } + + @Override + public void visitType(JavacNode typeNode, JCClassDecl type) { + + } + + @Override + public void visitAnnotationOnType(JCClassDecl type, JavacNode annotationNode, JCAnnotation annotation) { + + } + + @Override + public void endVisitType(JavacNode typeNode, JCClassDecl type) { + + } + + @Override + public void visitField(JavacNode fieldNode, JCVariableDecl field) { + + } + + @Override + public void visitAnnotationOnField(JCVariableDecl field, JavacNode annotationNode, JCAnnotation annotation) { + + } + + @Override + public void endVisitField(JavacNode fieldNode, JCVariableDecl field) { + + } + + @Override + public void visitInitializer(JavacNode initializerNode, JCBlock initializer) { + + } + + @Override + public void endVisitInitializer(JavacNode initializerNode, JCBlock initializer) { + + } + + @Override + public void visitMethod(JavacNode methodNode, JCMethodDecl method) { + + } + + @Override + public void visitAnnotationOnMethod(JCMethodDecl method, JavacNode annotationNode, JCAnnotation annotation) { + + } + + @Override + public void endVisitMethod(JavacNode methodNode, JCMethodDecl method) { + + } + + @Override + public void visitMethodArgument(JavacNode argumentNode, JCVariableDecl argument, JCMethodDecl method) { + + } + + @Override + public void visitAnnotationOnMethodArgument(JCVariableDecl argument, JCMethodDecl method, JavacNode annotationNode, JCAnnotation annotation) { + + } + + @Override + public void endVisitMethodArgument(JavacNode argumentNode, JCVariableDecl argument, JCMethodDecl method) { + + } + + @Override + public void visitLocal(JavacNode localNode, JCVariableDecl local) { + + } + + @Override + public void visitAnnotationOnLocal(JCVariableDecl local, JavacNode annotationNode, JCAnnotation annotation) { + + } + + @Override + public void endVisitLocal(JavacNode localNode, JCVariableDecl local) { + + } + + @Override + public void visitTypeUse(JavacNode typeUseNode, JCTree typeUse) { + + } + + @Override + public void visitAnnotationOnTypeUse(JCTree typeUse, JavacNode annotationNode, JCAnnotation annotation) { + + } + + @Override + public void endVisitTypeUse(JavacNode typeUseNode, JCTree typeUse) { + + } + + @Override + public void visitStatement(JavacNode statementNode, JCTree statement) { + + } + + @Override + public void endVisitStatement(JavacNode statementNode, JCTree statement) { + + } + } } diff --git a/src/core/lombok/javac/JavacNode.java b/src/core/lombok/javac/JavacNode.java index 3e180d8445..6e6b0ff451 100644 --- a/src/core/lombok/javac/JavacNode.java +++ b/src/core/lombok/javac/JavacNode.java @@ -272,7 +272,11 @@ public void addWarning(String message, DiagnosticPosition pos) { } @Override public AnnotationValues findAnnotation(Class type) { - JavacNode annotation = JavacHandlerUtil.findAnnotation(type, this, true); + return findAnnotation(type, true); + } + + public AnnotationValues findAnnotation(Class type, boolean delete) { + JavacNode annotation = JavacHandlerUtil.findAnnotation(type, this, delete); if (annotation == null) return null; return JavacHandlerUtil.createAnnotation(type, annotation); } diff --git a/src/core/lombok/javac/handlers/HandleJpaAssociationSync.java b/src/core/lombok/javac/handlers/HandleJpaAssociationSync.java new file mode 100644 index 0000000000..1ac6e75493 --- /dev/null +++ b/src/core/lombok/javac/handlers/HandleJpaAssociationSync.java @@ -0,0 +1,603 @@ +/* + * Copyright (C) 2024 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.*; +import static lombok.javac.Javac.*; +import static lombok.javac.JavacAugments.JCTree_keepPosition; +import static lombok.javac.handlers.JavacHandlerUtil.*; + +import com.sun.tools.javac.code.Flags; +import com.sun.tools.javac.code.Type; +import com.sun.tools.javac.tree.JCTree; +import com.sun.tools.javac.tree.JCTree.JCBlock; +import com.sun.tools.javac.tree.JCTree.JCExpression; +import com.sun.tools.javac.tree.JCTree.JCMethodDecl; +import com.sun.tools.javac.tree.JCTree.JCStatement; +import com.sun.tools.javac.tree.JCTree.JCTypeParameter; +import com.sun.tools.javac.tree.JCTree.JCVariableDecl; +import com.sun.tools.javac.tree.JCTree.JCExpressionStatement; +import com.sun.tools.javac.tree.JCTree.JCIdent; +import com.sun.tools.javac.tree.JCTree.JCTypeApply; +import com.sun.tools.javac.tree.JCTree.JCAnnotation; +import com.sun.tools.javac.util.List; +import com.sun.tools.javac.util.ListBuffer; +import com.sun.tools.javac.util.Name; + +import lombok.ConfigurationKeys; +import lombok.core.AST; +import lombok.core.AnnotationValues; +import lombok.experimental.JpaAssociationSync; +import lombok.javac.JavacASTVisitor; +import lombok.javac.JavacAnnotationHandler; +import lombok.javac.JavacNode; +import lombok.javac.JavacTreeMaker; +import lombok.spi.Provides; + +import java.lang.annotation.Annotation; + +/** + * Handles the {@code lombok.experimental.JpaAssociationSync} annotation for javac. + */ +@Provides +public class HandleJpaAssociationSync extends JavacAnnotationHandler { + private static final String NO_JPA_PACKAGE_IN_CLASSPATH_ERROR = "@JpaAssociationSync requires JPA in classpath"; + private static final String NODE_NOT_SUPPORTED_ERROR = "@JpaAssociationSync is only supported on a class or a field."; + + private static final String ONE_TO_MANY_CLASS_NAME = "OneToMany"; + private static final String MANY_TO_ONE_CLASS_NAME = "ManyToOne"; + private static final String ONE_TO_ONE_CLASS_NAME = "OneToOne"; + private static final String MANY_TO_MANY_CLASS_NAME = "ManyToMany"; + + private static final String MAPPED_BY_METHOD_NAME = "mappedBy"; + + private static final String ADD_METHOD_PREFIX = "add"; + private static final String REMOVE_METHOD_PREFIX = "remove"; + private static final String UPDATE_METHOD_PREFIX = "update"; + + @Override + public void handle(AnnotationValues annotation, JCAnnotation ast, JavacNode annotationNode) { + handleExperimentalFlagUsage(annotationNode, ConfigurationKeys.JPA_ASSOCIATION_SYNC_FLAG_USAGE, "@JpaAssociationSync"); + deleteAnnotationIfNeccessary(annotationNode, JpaAssociationSync.class); + + JavacNode node = annotationNode.up(); + JavacNode sourceNode = annotationNode; + JavacTreeMaker treeMaker = annotationNode.getTreeMaker(); + + String jpaPackageName = getJpaPackageName(); + + if (jpaPackageName == null) { + sourceNode.addError(NO_JPA_PACKAGE_IN_CLASSPATH_ERROR); + return; + } + + switch (node.getKind()) { + case FIELD: + generateAssociationSyncMethodsForField(node, sourceNode, treeMaker, jpaPackageName); + break; + case TYPE: + generateAssociationSyncMethodsForType(node, sourceNode, treeMaker, jpaPackageName); + break; + } + } + + private void generateAssociationSyncMethodsForType(JavacNode typeNode, + final JavacNode sourceNode, + final JavacTreeMaker treeMaker, + final String jpaPackageName) { + if (!isClass(typeNode)) { + sourceNode.addError(NODE_NOT_SUPPORTED_ERROR); + return; + } + + typeNode.traverse(new JavacASTVisitor.Default() { + @Override + public void visitField(JavacNode fieldNode, JCVariableDecl field) { + if (hasAnnotation(JpaAssociationSync.class, fieldNode)) { + return; + } + + generateAssociationSyncMethodsForField(fieldNode, sourceNode, treeMaker, jpaPackageName); + } + }); + } + + private void generateAssociationSyncMethodsForField(JavacNode fieldNode, + JavacNode sourceNode, + JavacTreeMaker treeMaker, + String jpaPackageName) { + if (fieldNode.getKind() != AST.Kind.FIELD) { + sourceNode.addError(NODE_NOT_SUPPORTED_ERROR); + return; + } + + JavacNode classNode = fieldNode.up(); + + Class oneToManyClass + = findAnnotationClass(jpaPackageName + "." + ONE_TO_MANY_CLASS_NAME); + Class oneToOneClass + = findAnnotationClass(jpaPackageName + "." + ONE_TO_ONE_CLASS_NAME); + Class manyToManyClass + = findAnnotationClass(jpaPackageName + "." + MANY_TO_MANY_CLASS_NAME); + + List> annotationClasses = List.of(oneToManyClass, oneToOneClass, manyToManyClass); + + List associationDetailsList + = getAssociationDetailsList(fieldNode, sourceNode, treeMaker, annotationClasses); + + for (AssociationDetails associationDetails : associationDetailsList) { + if (associationDetails.getType() != null) { + if (associationDetails.getType().equals(oneToManyClass)) { + generateOneToManyAssociationSyncMethodsForField(classNode, fieldNode, sourceNode, treeMaker, associationDetails); + } else if (associationDetails.getType().equals(oneToOneClass)) { + generateOneToOneAssociationSyncMethodsForField(classNode, fieldNode, sourceNode, treeMaker, associationDetails); + } else if (associationDetails.getType().equals(manyToManyClass)) { + generateManyToManyAssociationSyncMethodsForField(classNode, fieldNode, sourceNode, treeMaker, associationDetails); + } + } + } + } + + private void generateManyToManyAssociationSyncMethodsForField(JavacNode classNode, + JavacNode fieldNode, + JavacNode sourceNode, + JavacTreeMaker treeMaker, + AssociationDetails associationDetails) { + injectMethod( + classNode, + createMethodForManyToManyAssociation(fieldNode, sourceNode, treeMaker, associationDetails, ADD_METHOD_PREFIX) + ); + + injectMethod( + classNode, + createMethodForManyToManyAssociation(fieldNode, sourceNode, treeMaker, associationDetails, REMOVE_METHOD_PREFIX) + ); + } + + private void generateOneToOneAssociationSyncMethodsForField(JavacNode classNode, + JavacNode fieldNode, + JavacNode sourceNode, + JavacTreeMaker treeMaker, + AssociationDetails associationDetails) { + injectMethod( + classNode, + createMethodForOneToOneAssociation(fieldNode, sourceNode, treeMaker, associationDetails, UPDATE_METHOD_PREFIX) + ); + } + + private void generateOneToManyAssociationSyncMethodsForField(JavacNode classNode, + JavacNode fieldNode, + JavacNode sourceNode, + JavacTreeMaker treeMaker, + AssociationDetails associationDetails) { + injectMethod( + classNode, + createMethodForOneToManyAssociation(fieldNode, sourceNode, treeMaker, associationDetails, ADD_METHOD_PREFIX) + ); + + injectMethod( + classNode, + createMethodForOneToManyAssociation(fieldNode, sourceNode, treeMaker, associationDetails, REMOVE_METHOD_PREFIX) + ); + } + + private JCMethodDecl createMethodForManyToManyAssociation(JavacNode fieldNode, + JavacNode sourceNode, + JavacTreeMaker treeMaker, + AssociationDetails associationDetails, + String methodPrefix) { + JCVariableDecl fieldDecl = (JCVariableDecl) fieldNode.get(); + + JCIdent paramType = getFieldTypeOrGenericType(fieldDecl, sourceNode, treeMaker); + Name paramName = associationDetails.paramName != null + ? sourceNode.toName(associationDetails.paramName) + : sourceNode.toName(uncapitalize(sourceNode, paramType.getName().toString())); + JCVariableDecl param = createParameter(treeMaker, paramName, paramType); + + ListBuffer parameters = new ListBuffer(); + parameters.append(param); + + ListBuffer statements = new ListBuffer(); + statements.append( + executeMethod( + treeMaker, + createFieldAccessor(treeMaker, fieldNode, FieldAccess.ALWAYS_FIELD), + sourceNode.toName(methodPrefix), + List.of( + treeMaker.Ident(param.name) + ) + ) + ); + statements.append( + executeMethod( + treeMaker, + treeMaker.Apply( + List.nil(), + treeMaker.Select( + treeMaker.Ident(param.name), + sourceNode.toName( + buildAccessorName( + sourceNode, + "get", + associationDetails.inverseSideAnnotation != null + ? associationDetails.owningSideFieldName + : associationDetails.inverseSideFieldName + ) + ) + ), + List.nil() + ), + sourceNode.toName(methodPrefix), + List.of( + treeMaker.Ident(sourceNode.toName("this")) + ) + ) + ); + + JCBlock methodBody = treeMaker.Block(0, statements.toList()); + + JCMethodDecl methodDecl = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC), + sourceNode.toName(methodPrefix + capitalize(sourceNode, param.name.toString())), + treeMaker.Type(createVoidType(sourceNode.getSymbolTable(), CTC_VOID)), + List.nil(), + parameters.toList(), + List.nil(), + methodBody, + null + ); + + return recursiveSetGeneratedBy(methodDecl, sourceNode); + } + + private JCMethodDecl createMethodForOneToOneAssociation(JavacNode fieldNode, + JavacNode sourceNode, + JavacTreeMaker treeMaker, + AssociationDetails associationDetails, + String methodPrefix) { + JCVariableDecl fieldDecl = (JCVariableDecl) fieldNode.get(); + + JCIdent paramType = getFieldTypeOrGenericType(fieldDecl, sourceNode, treeMaker); + Name paramName = associationDetails.paramName != null + ? sourceNode.toName(associationDetails.paramName) + : sourceNode.toName(uncapitalize(sourceNode, paramType.getName().toString())); + JCVariableDecl param = createParameter(treeMaker, paramName, paramType); + + ListBuffer parameters = new ListBuffer(); + parameters.append(param); + + ListBuffer statements = new ListBuffer(); + + ListBuffer ifStatements = new ListBuffer(); + + ListBuffer if1Statements = new ListBuffer(); + if1Statements.append( + executeMethod( + treeMaker, + createFieldAccessor(treeMaker, fieldNode, FieldAccess.ALWAYS_FIELD), + sourceNode.toName( + buildAccessorName( + sourceNode, + "set", + associationDetails.inverseSideAnnotation != null + ? associationDetails.owningSideFieldName + : associationDetails.inverseSideFieldName + ) + ), + List.of( + treeMaker.NullLiteral() + ) + ) + ); + + ifStatements.append( + treeMaker.If( + treeMaker.Binary( + CTC_NOT_EQUAL, + createFieldAccessor(treeMaker, fieldNode, FieldAccess.ALWAYS_FIELD), + treeMaker.NullLiteral() + ), + treeMaker.Block(0, if1Statements.toList()), + null + ) + ); + + ListBuffer elseStatements = new ListBuffer(); + elseStatements.append( + executeMethod( + treeMaker, + treeMaker.Ident(param.name), + sourceNode.toName( + buildAccessorName( + sourceNode, + "set", + associationDetails.inverseSideAnnotation != null + ? associationDetails.owningSideFieldName + : associationDetails.inverseSideFieldName + ) + ), + List.of( + treeMaker.Ident(fieldNode.toName("this")) + ) + ) + ); + + statements.append( + treeMaker.If( + treeMaker.Binary(CTC_EQUAL, treeMaker.Ident(param.name), treeMaker.NullLiteral()), + treeMaker.Block(0, ifStatements.toList()), + treeMaker.Block(0, elseStatements.toList()) + ) + ); + statements.append( + treeMaker.Exec( + treeMaker.Assign( + createFieldAccessor(treeMaker, fieldNode, FieldAccess.ALWAYS_FIELD), + treeMaker.Ident(param.name) + ) + ) + ); + + JCBlock methodBody = treeMaker.Block(0, statements.toList()); + + JCMethodDecl methodDecl = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC), + sourceNode.toName(methodPrefix + capitalize(sourceNode, param.name.toString())), + treeMaker.Type(createVoidType(sourceNode.getSymbolTable(), CTC_VOID)), + List.nil(), + parameters.toList(), + List.nil(), + methodBody, + null + ); + + return recursiveSetGeneratedBy(methodDecl, sourceNode); + } + + private JCMethodDecl createMethodForOneToManyAssociation(JavacNode fieldNode, + JavacNode sourceNode, + JavacTreeMaker treeMaker, + AssociationDetails associationDetails, + String methodPrefix) { + JCVariableDecl fieldDecl = (JCVariableDecl) fieldNode.get(); + + JCIdent paramType = getFieldTypeOrGenericType(fieldDecl, sourceNode, treeMaker); + Name paramName = associationDetails.paramName != null + ? sourceNode.toName(associationDetails.paramName) + : sourceNode.toName(uncapitalize(sourceNode, paramType.getName().toString())); + JCVariableDecl param = createParameter(treeMaker, paramName, paramType); + + ListBuffer parameters = new ListBuffer(); + parameters.append(param); + + ListBuffer statements = new ListBuffer(); + statements.append( + executeMethod( + treeMaker, + createFieldAccessor(treeMaker, fieldNode, FieldAccess.ALWAYS_FIELD), + sourceNode.toName(methodPrefix), + List.of( + treeMaker.Ident(param.name) + ) + ) + ); + statements.append( + executeMethod( + treeMaker, + treeMaker.Ident(param.name), + sourceNode.toName( + buildAccessorName(sourceNode, "set", associationDetails.owningSideFieldName) + ), + List.of( + methodPrefix.equals(ADD_METHOD_PREFIX) + ? treeMaker.Ident(sourceNode.toName("this")) + : treeMaker.NullLiteral() + ) + ) + ); + + JCBlock methodBody = treeMaker.Block(0, statements.toList()); + + JCMethodDecl methodDecl = treeMaker.MethodDef( + treeMaker.Modifiers(Flags.PUBLIC), + sourceNode.toName(methodPrefix + capitalize(sourceNode, param.name.toString())), + treeMaker.Type(createVoidType(sourceNode.getSymbolTable(), CTC_VOID)), + List.nil(), + parameters.toList(), + List.nil(), + methodBody, + null + ); + + return recursiveSetGeneratedBy(methodDecl, sourceNode); + } + + private JCVariableDecl createParameter(JavacTreeMaker treeMaker, + Name paramName, + JCIdent paramType) { + return treeMaker.VarDef( + treeMaker.Modifiers(Flags.PARAMETER), + paramName, + paramType, + null + ); + } + + private JCExpressionStatement executeMethod(JavacTreeMaker treeMaker, + JCExpression field, + Name methodName, + List arguments) { + return treeMaker.Exec( + treeMaker.Apply( + List.nil(), + treeMaker.Select( + field, + methodName + ), + arguments + ) + ); + } + + private JCIdent getFieldTypeOrGenericType(JCVariableDecl fieldDecl, + JavacNode sourceNode, + JavacTreeMaker treeMaker) { + JCExpression fieldType = fieldDecl.vartype; + + if (fieldType instanceof JCTypeApply) { + return (JCIdent) cloneType( + treeMaker, + getGenericTypes((JCTypeApply) fieldType).get(0), + sourceNode + ); + } + + return (JCIdent) cloneType(treeMaker, fieldType, sourceNode); + } + + private List getAssociationDetailsList(JavacNode fieldNode, + JavacNode sourceNode, + JavacTreeMaker treeMaker, + List> annotationClasses) { + JCIdent fieldType = getFieldTypeOrGenericType((JCVariableDecl) fieldNode.get(), sourceNode, treeMaker); + + AnnotationValues extraSettings = fieldNode.findAnnotation(JpaAssociationSync.Extra.class); + + List> associationAnnotations = getAssociationAnnotations(fieldNode, annotationClasses); + + ListBuffer associationDetailsListBuffer = new ListBuffer(); + for (AnnotationValues annotation : associationAnnotations) { + if (AssociationDetails.isOwningSide(annotation)) { + if (extraSettings != null + && !extraSettings.getAsString("inverseSideFieldName").isEmpty()) { + AssociationDetails associationDetails = new AssociationDetails(); + + associationDetails.owningSideAnnotation = annotation; + + associationDetails.owningSideFieldName = uncapitalize(sourceNode, fieldType.getName().toString()); + associationDetails.inverseSideFieldName = extraSettings.getAsString("inverseSideFieldName"); + + associationDetails.paramName = + !extraSettings.getAsString("paramName").isEmpty() + ? extraSettings.getAsString("paramName") + : null; + + associationDetailsListBuffer.append(associationDetails); + } + } else { + AssociationDetails associationDetails = new AssociationDetails(); + + associationDetails.inverseSideAnnotation = annotation; + + associationDetails.owningSideFieldName = AssociationDetails.getOwningSideFieldName(annotation); + associationDetails.inverseSideFieldName = uncapitalize(sourceNode, fieldType.getName().toString()); + + associationDetails.paramName = + extraSettings != null && !extraSettings.getAsString("paramName").isEmpty() + ? extraSettings.getAsString("paramName") + : null; + + associationDetailsListBuffer.append(associationDetails); + } + } + + return associationDetailsListBuffer.toList(); + } + + private List> getAssociationAnnotations(JavacNode fieldNode, + List> annotationClasses) { + ListBuffer> annotations = new ListBuffer>(); + + for (Class annotationClass : annotationClasses) { + if (annotationClass != null) { + AnnotationValues annotation = fieldNode.findAnnotation(annotationClass, false); + + if (annotation != null) { + annotations.append(annotation); + } + } + } + + return annotations.toList(); + } + + private Class findAnnotationClass(String className) { + try { + return Class.forName(className).asSubclass(Annotation.class); + } catch (ClassNotFoundException e) { + return null; + } + } + + private String getJpaPackageName() { + try { + Class.forName("javax.persistence.Entity"); + return "javax.persistence"; + } catch (ClassNotFoundException e0) { + try { + Class.forName("jakarta.persistence.Entity"); + return "jakarta.persistence"; + } catch (ClassNotFoundException e1) { + return null; + } + } + } + + private static class AssociationDetails { + AnnotationValues owningSideAnnotation; + AnnotationValues inverseSideAnnotation; + + String owningSideFieldName; + String inverseSideFieldName; + + String paramName; + + AssociationDetails() { + } + + AssociationDetails(String owningSideFieldName, String inverseSideFieldName) { + this.owningSideFieldName = owningSideFieldName; + this.inverseSideFieldName = inverseSideFieldName; + } + + Class getType() { + if (owningSideAnnotation != null) + return owningSideAnnotation.getType(); + if (inverseSideAnnotation != null) + return inverseSideAnnotation.getType(); + + return null; + } + + static String getOwningSideFieldName(AnnotationValues inverseSideAnnotation) { + return inverseSideAnnotation.getAsString(MAPPED_BY_METHOD_NAME); + } + + static boolean isOwningSide(AnnotationValues annotation) { + return annotation.getAsString(MAPPED_BY_METHOD_NAME).isEmpty(); + } + + static boolean isInverseSide(AnnotationValues annotation) { + return !isOwningSide(annotation); + } + } +} diff --git a/src/core/lombok/javac/handlers/JavacHandlerUtil.java b/src/core/lombok/javac/handlers/JavacHandlerUtil.java index 38658bb622..2e4a751220 100644 --- a/src/core/lombok/javac/handlers/JavacHandlerUtil.java +++ b/src/core/lombok/javac/handlers/JavacHandlerUtil.java @@ -1361,7 +1361,7 @@ static void enter(ClassSymbol from, Symbol toEnter) { } catch (Exception e) {} } } - + /** * Adds the given new method declaration to the provided type AST Node. * Can also inject constructors. @@ -2482,4 +2482,15 @@ private static void applyAnnotationToVarDecl(JavacNode typeNode, JCVariableDecl arg.mods.annotations = arg.mods.annotations == null ? List.of(m) : arg.mods.annotations.prepend(m); } } + + public static List getGenericTypes(JCTypeApply fieldType) { + ListBuffer result = new ListBuffer(); + + List typeArguments = fieldType.arguments; + for (JCExpression arg : typeArguments) { + result.append((JCIdent) arg); + } + + return result.toList(); + } } diff --git a/src/utils/lombok/javac/JavacTreeMaker.java b/src/utils/lombok/javac/JavacTreeMaker.java index 04f0e208aa..468a3a9fc3 100644 --- a/src/utils/lombok/javac/JavacTreeMaker.java +++ b/src/utils/lombok/javac/JavacTreeMaker.java @@ -809,6 +809,9 @@ public JCIdent Ident(Name idname) { public JCLiteral Literal(TypeTag tag, Object value) { return invoke(Literal, tag.value, value); } + public JCLiteral NullLiteral() { + return Literal(Javac.CTC_BOT, null); + } //javac versions: 6-8 //typetag = [6-7] int [8] TypeTag From 57a16576473966a061e9320a50efa132b2a6dde2 Mon Sep 17 00:00:00 2001 From: Sergey Rukin Date: Mon, 17 Jun 2024 21:42:45 +0500 Subject: [PATCH 2/3] [#3437] Add tests for @JpaAssociationSync annotation --- test/stubs/jakarta/persistence/Entity.java | 14 ++++ .../stubs/jakarta/persistence/ManyToMany.java | 12 +++ test/stubs/jakarta/persistence/ManyToOne.java | 11 +++ test/stubs/jakarta/persistence/OneToMany.java | 12 +++ test/stubs/jakarta/persistence/OneToOne.java | 12 +++ .../JpaAssociationSyncManyToMany.java | 30 +++++++ .../JpaAssociationSyncManyToManyInverse.java | 30 +++++++ .../JpaAssociationSyncOnClass.java | 84 +++++++++++++++++++ ...paAssociationSyncOnClassWithParamName.java | 84 +++++++++++++++++++ .../JpaAssociationSyncOneToMany.java | 30 +++++++ .../JpaAssociationSyncOneToOne.java | 30 +++++++ .../JpaAssociationSyncOneToOneInverse.java | 30 +++++++ .../JpaAssociationSyncManyToMany.java | 30 +++++++ .../JpaAssociationSyncManyToManyInverse.java | 30 +++++++ .../after-ecj/JpaAssociationSyncOnClass.java | 81 ++++++++++++++++++ ...paAssociationSyncOnClassWithParamName.java | 81 ++++++++++++++++++ .../JpaAssociationSyncOneToMany.java | 30 +++++++ .../after-ecj/JpaAssociationSyncOneToOne.java | 35 ++++++++ .../JpaAssociationSyncOneToOneInverse.java | 35 ++++++++ .../before/JpaAssociationSyncManyToMany.java | 17 ++++ .../JpaAssociationSyncManyToManyInverse.java | 18 ++++ .../before/JpaAssociationSyncOnClass.java | 43 ++++++++++ ...paAssociationSyncOnClassWithParamName.java | 46 ++++++++++ .../before/JpaAssociationSyncOneToMany.java | 17 ++++ .../before/JpaAssociationSyncOneToOne.java | 17 ++++ .../JpaAssociationSyncOneToOneInverse.java | 18 ++++ 26 files changed, 877 insertions(+) create mode 100644 test/stubs/jakarta/persistence/Entity.java create mode 100644 test/stubs/jakarta/persistence/ManyToMany.java create mode 100644 test/stubs/jakarta/persistence/ManyToOne.java create mode 100644 test/stubs/jakarta/persistence/OneToMany.java create mode 100644 test/stubs/jakarta/persistence/OneToOne.java create mode 100644 test/transform/resource/after-delombok/JpaAssociationSyncManyToMany.java create mode 100644 test/transform/resource/after-delombok/JpaAssociationSyncManyToManyInverse.java create mode 100644 test/transform/resource/after-delombok/JpaAssociationSyncOnClass.java create mode 100644 test/transform/resource/after-delombok/JpaAssociationSyncOnClassWithParamName.java create mode 100644 test/transform/resource/after-delombok/JpaAssociationSyncOneToMany.java create mode 100644 test/transform/resource/after-delombok/JpaAssociationSyncOneToOne.java create mode 100644 test/transform/resource/after-delombok/JpaAssociationSyncOneToOneInverse.java create mode 100644 test/transform/resource/after-ecj/JpaAssociationSyncManyToMany.java create mode 100644 test/transform/resource/after-ecj/JpaAssociationSyncManyToManyInverse.java create mode 100644 test/transform/resource/after-ecj/JpaAssociationSyncOnClass.java create mode 100644 test/transform/resource/after-ecj/JpaAssociationSyncOnClassWithParamName.java create mode 100644 test/transform/resource/after-ecj/JpaAssociationSyncOneToMany.java create mode 100644 test/transform/resource/after-ecj/JpaAssociationSyncOneToOne.java create mode 100644 test/transform/resource/after-ecj/JpaAssociationSyncOneToOneInverse.java create mode 100644 test/transform/resource/before/JpaAssociationSyncManyToMany.java create mode 100644 test/transform/resource/before/JpaAssociationSyncManyToManyInverse.java create mode 100644 test/transform/resource/before/JpaAssociationSyncOnClass.java create mode 100644 test/transform/resource/before/JpaAssociationSyncOnClassWithParamName.java create mode 100644 test/transform/resource/before/JpaAssociationSyncOneToMany.java create mode 100644 test/transform/resource/before/JpaAssociationSyncOneToOne.java create mode 100644 test/transform/resource/before/JpaAssociationSyncOneToOneInverse.java diff --git a/test/stubs/jakarta/persistence/Entity.java b/test/stubs/jakarta/persistence/Entity.java new file mode 100644 index 0000000000..2a77a5f315 --- /dev/null +++ b/test/stubs/jakarta/persistence/Entity.java @@ -0,0 +1,14 @@ +package jakarta.persistence; + +import java.lang.annotation.Target; +import java.lang.annotation.Retention; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.RetentionPolicy; + +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface Entity { + String name() default ""; +} diff --git a/test/stubs/jakarta/persistence/ManyToMany.java b/test/stubs/jakarta/persistence/ManyToMany.java new file mode 100644 index 0000000000..06960aa39d --- /dev/null +++ b/test/stubs/jakarta/persistence/ManyToMany.java @@ -0,0 +1,12 @@ +package jakarta.persistence; + +import java.lang.annotation.Target; +import java.lang.annotation.Retention; +import java.lang.annotation.ElementType; +import java.lang.annotation.RetentionPolicy; + +@Target({ElementType.METHOD, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ManyToMany { + String mappedBy() default ""; +} diff --git a/test/stubs/jakarta/persistence/ManyToOne.java b/test/stubs/jakarta/persistence/ManyToOne.java new file mode 100644 index 0000000000..d45edeabe0 --- /dev/null +++ b/test/stubs/jakarta/persistence/ManyToOne.java @@ -0,0 +1,11 @@ +package jakarta.persistence; + +import java.lang.annotation.Target; +import java.lang.annotation.Retention; +import java.lang.annotation.ElementType; +import java.lang.annotation.RetentionPolicy; + +@Target({ElementType.METHOD, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface ManyToOne { +} diff --git a/test/stubs/jakarta/persistence/OneToMany.java b/test/stubs/jakarta/persistence/OneToMany.java new file mode 100644 index 0000000000..f49ff72832 --- /dev/null +++ b/test/stubs/jakarta/persistence/OneToMany.java @@ -0,0 +1,12 @@ +package jakarta.persistence; + +import java.lang.annotation.Target; +import java.lang.annotation.Retention; +import java.lang.annotation.ElementType; +import java.lang.annotation.RetentionPolicy; + +@Target({ElementType.METHOD, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface OneToMany { + String mappedBy() default ""; +} diff --git a/test/stubs/jakarta/persistence/OneToOne.java b/test/stubs/jakarta/persistence/OneToOne.java new file mode 100644 index 0000000000..b51d292fbe --- /dev/null +++ b/test/stubs/jakarta/persistence/OneToOne.java @@ -0,0 +1,12 @@ +package jakarta.persistence; + +import java.lang.annotation.Target; +import java.lang.annotation.Retention; +import java.lang.annotation.ElementType; +import java.lang.annotation.RetentionPolicy; + +@Target({ElementType.METHOD, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface OneToOne { + String mappedBy() default ""; +} diff --git a/test/transform/resource/after-delombok/JpaAssociationSyncManyToMany.java b/test/transform/resource/after-delombok/JpaAssociationSyncManyToMany.java new file mode 100644 index 0000000000..f6248eea2a --- /dev/null +++ b/test/transform/resource/after-delombok/JpaAssociationSyncManyToMany.java @@ -0,0 +1,30 @@ + +@jakarta.persistence.Entity(name = "Post") +class Post { + @jakarta.persistence.ManyToMany(mappedBy = "posts") + private java.util.Set tags = new java.util.HashSet<>(); + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public void addTag(Tag tag) { + this.tags.add(tag); + tag.getPosts().add(this); + } + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public void removeTag(Tag tag) { + this.tags.remove(tag); + tag.getPosts().remove(this); + } +} + +@jakarta.persistence.Entity(name = "Tag") +class Tag { + @jakarta.persistence.ManyToMany + private java.util.Set posts = new java.util.HashSet<>(); + + public java.util.Set getPosts() { + return this.posts; + } +} diff --git a/test/transform/resource/after-delombok/JpaAssociationSyncManyToManyInverse.java b/test/transform/resource/after-delombok/JpaAssociationSyncManyToManyInverse.java new file mode 100644 index 0000000000..d0e3376c0f --- /dev/null +++ b/test/transform/resource/after-delombok/JpaAssociationSyncManyToManyInverse.java @@ -0,0 +1,30 @@ + +@jakarta.persistence.Entity(name = "Post") +class Post { + @jakarta.persistence.ManyToMany + private java.util.Set tags = new java.util.HashSet<>(); + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public void addTag(Tag tag) { + this.tags.add(tag); + tag.getPosts().add(this); + } + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public void removeTag(Tag tag) { + this.tags.remove(tag); + tag.getPosts().remove(this); + } +} + +@jakarta.persistence.Entity(name = "Tag") +class Tag { + @jakarta.persistence.ManyToMany(mappedBy = "tags") + private java.util.Set posts = new java.util.HashSet<>(); + + public java.util.Set getPosts() { + return this.posts; + } +} diff --git a/test/transform/resource/after-delombok/JpaAssociationSyncOnClass.java b/test/transform/resource/after-delombok/JpaAssociationSyncOnClass.java new file mode 100644 index 0000000000..5e425da2d9 --- /dev/null +++ b/test/transform/resource/after-delombok/JpaAssociationSyncOnClass.java @@ -0,0 +1,84 @@ + +@jakarta.persistence.Entity(name = "Post") +class Post { + @jakarta.persistence.OneToMany(mappedBy = "post") + private java.util.List comments = new java.util.ArrayList<>(); + + @jakarta.persistence.OneToOne(mappedBy = "post") + private PostDetails details; + + @jakarta.persistence.ManyToMany(mappedBy = "posts") + private java.util.Set tags = new java.util.HashSet<>(); + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public void addPostComment(PostComment postComment) { + this.comments.add(postComment); + postComment.setPost(this); + } + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public void removePostComment(PostComment postComment) { + this.comments.remove(postComment); + postComment.setPost(null); + } + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public void updatePostDetails(PostDetails postDetails) { + if (postDetails == null) { + if (this.details != null) { + this.details.setPost(null); + } + } else { + postDetails.setPost(this); + } + + this.details = postDetails; + } + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public void addTag(Tag tag) { + this.tags.add(tag); + tag.getPosts().add(this); + } + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public void removeTag(Tag tag) { + this.tags.remove(tag); + tag.getPosts().remove(this); + } +} + +@jakarta.persistence.Entity(name = "PostComment") +class PostComment { + @jakarta.persistence.ManyToOne + private Post post; + + public void setPost(final Post post) { + this.post = post; + } +} + +@jakarta.persistence.Entity(name = "PostDetails") +class PostDetails { + @jakarta.persistence.OneToOne + private Post post; + + public void setPost(final Post post) { + this.post = post; + } +} + +@jakarta.persistence.Entity(name = "Tag") +class Tag { + @jakarta.persistence.ManyToMany + private java.util.Set posts = new java.util.HashSet<>(); + + public java.util.Set getPosts() { + return this.posts; + } +} diff --git a/test/transform/resource/after-delombok/JpaAssociationSyncOnClassWithParamName.java b/test/transform/resource/after-delombok/JpaAssociationSyncOnClassWithParamName.java new file mode 100644 index 0000000000..d9841600a8 --- /dev/null +++ b/test/transform/resource/after-delombok/JpaAssociationSyncOnClassWithParamName.java @@ -0,0 +1,84 @@ + +@jakarta.persistence.Entity(name = "Post") +class Post { + @jakarta.persistence.OneToMany(mappedBy = "post") + private java.util.List comments = new java.util.ArrayList<>(); + + @jakarta.persistence.OneToOne(mappedBy = "post") + private PostDetails details; + + @jakarta.persistence.ManyToMany(mappedBy = "posts") + private java.util.Set tags = new java.util.HashSet<>(); + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public void addAnotherName(PostComment anotherName) { + this.comments.add(anotherName); + anotherName.setPost(this); + } + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public void removeAnotherName(PostComment anotherName) { + this.comments.remove(anotherName); + anotherName.setPost(null); + } + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public void updateAnotherName1(PostDetails anotherName1) { + if (anotherName1 == null) { + if (this.details != null) { + this.details.setPost(null); + } + } else { + anotherName1.setPost(this); + } + + this.details = anotherName1; + } + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public void addAnotherName2(Tag anotherName2) { + this.tags.add(anotherName2); + anotherName2.getPosts().add(this); + } + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public void removeAnotherName2(Tag anotherName2) { + this.tags.remove(anotherName2); + anotherName2.getPosts().remove(this); + } +} + +@jakarta.persistence.Entity(name = "PostComment") +class PostComment { + @jakarta.persistence.ManyToOne + private Post post; + + public void setPost(final Post post) { + this.post = post; + } +} + +@jakarta.persistence.Entity(name = "PostDetails") +class PostDetails { + @jakarta.persistence.OneToOne + private Post post; + + public void setPost(final Post post) { + this.post = post; + } +} + +@jakarta.persistence.Entity(name = "Tag") +class Tag { + @jakarta.persistence.ManyToMany + private java.util.Set posts = new java.util.HashSet<>(); + + public java.util.Set getPosts() { + return this.posts; + } +} diff --git a/test/transform/resource/after-delombok/JpaAssociationSyncOneToMany.java b/test/transform/resource/after-delombok/JpaAssociationSyncOneToMany.java new file mode 100644 index 0000000000..c685c8d49f --- /dev/null +++ b/test/transform/resource/after-delombok/JpaAssociationSyncOneToMany.java @@ -0,0 +1,30 @@ + +@jakarta.persistence.Entity(name = "Post") +class Post { + @jakarta.persistence.OneToMany(mappedBy = "post") + private java.util.List comments = new java.util.ArrayList<>(); + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public void addPostComment(PostComment postComment) { + this.comments.add(postComment); + postComment.setPost(this); + } + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public void removePostComment(PostComment postComment) { + this.comments.remove(postComment); + postComment.setPost(null); + } +} + +@jakarta.persistence.Entity(name = "PostComment") +class PostComment { + @jakarta.persistence.ManyToOne + private Post post; + + public void setPost(final Post post) { + this.post = post; + } +} diff --git a/test/transform/resource/after-delombok/JpaAssociationSyncOneToOne.java b/test/transform/resource/after-delombok/JpaAssociationSyncOneToOne.java new file mode 100644 index 0000000000..e33a37479f --- /dev/null +++ b/test/transform/resource/after-delombok/JpaAssociationSyncOneToOne.java @@ -0,0 +1,30 @@ + +@jakarta.persistence.Entity(name = "Post") +class Post { + @jakarta.persistence.OneToOne(mappedBy = "post") + private PostDetails details; + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public void updatePostDetails(PostDetails postDetails) { + if (postDetails == null) { + if (this.details != null) { + this.details.setPost(null); + } + } else { + postDetails.setPost(this); + } + + this.details = postDetails; + } +} + +@jakarta.persistence.Entity(name = "PostDetails") +class PostDetails { + @jakarta.persistence.OneToOne + private Post post; + + public void setPost(final Post post) { + this.post = post; + } +} diff --git a/test/transform/resource/after-delombok/JpaAssociationSyncOneToOneInverse.java b/test/transform/resource/after-delombok/JpaAssociationSyncOneToOneInverse.java new file mode 100644 index 0000000000..bef34e6739 --- /dev/null +++ b/test/transform/resource/after-delombok/JpaAssociationSyncOneToOneInverse.java @@ -0,0 +1,30 @@ + +@jakarta.persistence.Entity(name = "Post") +class Post { + @jakarta.persistence.OneToOne + private PostDetails details; + + @java.lang.SuppressWarnings("all") + @lombok.Generated + public void updatePostDetails(PostDetails postDetails) { + if (postDetails == null) { + if (this.details != null) { + this.details.setPost(null); + } + } else { + postDetails.setPost(this); + } + + this.details = postDetails; + } +} + +@jakarta.persistence.Entity(name = "PostDetails") +class PostDetails { + @jakarta.persistence.OneToOne(mappedBy = "details") + private Post post; + + public void setPost(final Post post) { + this.post = post; + } +} diff --git a/test/transform/resource/after-ecj/JpaAssociationSyncManyToMany.java b/test/transform/resource/after-ecj/JpaAssociationSyncManyToMany.java new file mode 100644 index 0000000000..92f3028abc --- /dev/null +++ b/test/transform/resource/after-ecj/JpaAssociationSyncManyToMany.java @@ -0,0 +1,30 @@ + +@jakarta.persistence.Entity(name = "Post") class Post { + private @lombok.experimental.JpaAssociationSync @jakarta.persistence.ManyToMany(mappedBy = "posts") java.util.Set tags = new java.util.HashSet<>(); + + Post() { + super(); + } + + public @java.lang.SuppressWarnings("all") @lombok.Generated void addTag(Tag tag) { + this.tags.add(tag); + tag.getPosts().add(this); + } + + public @java.lang.SuppressWarnings("all") @lombok.Generated void removeTag(Tag tag) { + this.tags.remove(tag); + tag.getPosts().remove(this); + } +} + +@jakarta.persistence.Entity(name = "Tag") class Tag { + private @jakarta.persistence.ManyToMany java.util.Set posts = new java.util.HashSet<>(); + + Tag() { + super(); + } + + public java.util.Set getPosts() { + return this.posts; + } +} diff --git a/test/transform/resource/after-ecj/JpaAssociationSyncManyToManyInverse.java b/test/transform/resource/after-ecj/JpaAssociationSyncManyToManyInverse.java new file mode 100644 index 0000000000..8b2dc38561 --- /dev/null +++ b/test/transform/resource/after-ecj/JpaAssociationSyncManyToManyInverse.java @@ -0,0 +1,30 @@ + +@jakarta.persistence.Entity(name = "Post") class Post { + private @lombok.experimental.JpaAssociationSync @lombok.experimental.JpaAssociationSync.Extra(inverseSideFieldName = "posts") @jakarta.persistence.ManyToMany java.util.Set tags = new java.util.HashSet<>(); + + Post() { + super(); + } + + public @java.lang.SuppressWarnings("all") @lombok.Generated void addTag(Tag tag) { + this.tags.add(tag); + tag.getPosts().add(this); + } + + public @java.lang.SuppressWarnings("all") @lombok.Generated void removeTag(Tag tag) { + this.tags.remove(tag); + tag.getPosts().remove(this); + } +} + +@jakarta.persistence.Entity(name = "Tag") class Tag { + private @jakarta.persistence.ManyToMany(mappedBy = "tags") java.util.Set posts = new java.util.HashSet<>(); + + Tag() { + super(); + } + + public java.util.Set getPosts() { + return this.posts; + } +} diff --git a/test/transform/resource/after-ecj/JpaAssociationSyncOnClass.java b/test/transform/resource/after-ecj/JpaAssociationSyncOnClass.java new file mode 100644 index 0000000000..ee3ec91f0c --- /dev/null +++ b/test/transform/resource/after-ecj/JpaAssociationSyncOnClass.java @@ -0,0 +1,81 @@ + +@lombok.experimental.JpaAssociationSync @jakarta.persistence.Entity(name = "Post") class Post { + private @jakarta.persistence.OneToMany(mappedBy = "post") java.util.List comments = new java.util.ArrayList<>(); + private @jakarta.persistence.OneToOne(mappedBy = "post") PostDetails details; + private @jakarta.persistence.ManyToMany(mappedBy = "posts") java.util.Set tags = new java.util.HashSet<>(); + + Post() { + super(); + } + + public @java.lang.SuppressWarnings("all") @lombok.Generated void addPostComment(PostComment postComment) { + this.comments.add(postComment); + postComment.setPost(this); + } + + public @java.lang.SuppressWarnings("all") @lombok.Generated void removePostComment(PostComment postComment) { + this.comments.remove(postComment); + postComment.setPost(null); + } + + public @java.lang.SuppressWarnings("all") @lombok.Generated void updatePostDetails(PostDetails postDetails) { + if ((postDetails == null)) + { + if ((this.details != null)) + { + this.details.setPost(null); + } + } + else + { + postDetails.setPost(this); + } + this.details = postDetails; + } + + public @java.lang.SuppressWarnings("all") @lombok.Generated void addTag(Tag tag) { + this.tags.add(tag); + tag.getPosts().add(this); + } + + public @java.lang.SuppressWarnings("all") @lombok.Generated void removeTag(Tag tag) { + this.tags.remove(tag); + tag.getPosts().remove(this); + } +} + +@jakarta.persistence.Entity(name = "PostComment") class PostComment { + private @jakarta.persistence.ManyToOne Post post; + + PostComment() { + super(); + } + + public void setPost(final Post post) { + this.post = post; + } +} + +@jakarta.persistence.Entity(name = "PostDetails") class PostDetails { + private @jakarta.persistence.OneToOne Post post; + + PostDetails() { + super(); + } + + public void setPost(final Post post) { + this.post = post; + } +} + +@jakarta.persistence.Entity(name = "Tag") class Tag { + private @jakarta.persistence.ManyToMany java.util.Set posts = new java.util.HashSet<>(); + + Tag() { + super(); + } + + public java.util.Set getPosts() { + return this.posts; + } +} diff --git a/test/transform/resource/after-ecj/JpaAssociationSyncOnClassWithParamName.java b/test/transform/resource/after-ecj/JpaAssociationSyncOnClassWithParamName.java new file mode 100644 index 0000000000..3bccbbe91c --- /dev/null +++ b/test/transform/resource/after-ecj/JpaAssociationSyncOnClassWithParamName.java @@ -0,0 +1,81 @@ + +@lombok.experimental.JpaAssociationSync @jakarta.persistence.Entity(name = "Post") class Post { + private @lombok.experimental.JpaAssociationSync.Extra(paramName = "anotherName") @jakarta.persistence.OneToMany(mappedBy = "post") java.util.List comments = new java.util.ArrayList<>(); + private @lombok.experimental.JpaAssociationSync.Extra(paramName = "anotherName1") @jakarta.persistence.OneToOne(mappedBy = "post") PostDetails details; + private @lombok.experimental.JpaAssociationSync.Extra(paramName = "anotherName2") @jakarta.persistence.ManyToMany(mappedBy = "posts") java.util.Set tags = new java.util.HashSet<>(); + + Post() { + super(); + } + + public @java.lang.SuppressWarnings("all") @lombok.Generated void addAnotherName(PostComment anotherName) { + this.comments.add(anotherName); + anotherName.setPost(this); + } + + public @java.lang.SuppressWarnings("all") @lombok.Generated void removeAnotherName(PostComment anotherName) { + this.comments.remove(anotherName); + anotherName.setPost(null); + } + + public @java.lang.SuppressWarnings("all") @lombok.Generated void updateAnotherName1(PostDetails anotherName1) { + if ((anotherName1 == null)) + { + if ((this.details != null)) + { + this.details.setPost(null); + } + } + else + { + anotherName1.setPost(this); + } + this.details = anotherName1; + } + + public @java.lang.SuppressWarnings("all") @lombok.Generated void addAnotherName2(Tag anotherName2) { + this.tags.add(anotherName2); + anotherName2.getPosts().add(this); + } + + public @java.lang.SuppressWarnings("all") @lombok.Generated void removeAnotherName2(Tag anotherName2) { + this.tags.remove(anotherName2); + anotherName2.getPosts().remove(this); + } +} + +@jakarta.persistence.Entity(name = "PostComment") class PostComment { + private @jakarta.persistence.ManyToOne Post post; + + PostComment() { + super(); + } + + public void setPost(final Post post) { + this.post = post; + } +} + +@jakarta.persistence.Entity(name = "PostDetails") class PostDetails { + private @jakarta.persistence.OneToOne Post post; + + PostDetails() { + super(); + } + + public void setPost(final Post post) { + this.post = post; + } +} + +@jakarta.persistence.Entity(name = "Tag") class Tag { + private @jakarta.persistence.ManyToMany java.util.Set posts = new java.util.HashSet<>(); + + Tag() { + super(); + } + + public java.util.Set getPosts() { + return this.posts; + } +} diff --git a/test/transform/resource/after-ecj/JpaAssociationSyncOneToMany.java b/test/transform/resource/after-ecj/JpaAssociationSyncOneToMany.java new file mode 100644 index 0000000000..e92824cd49 --- /dev/null +++ b/test/transform/resource/after-ecj/JpaAssociationSyncOneToMany.java @@ -0,0 +1,30 @@ + +@jakarta.persistence.Entity(name = "Post") class Post { + private @lombok.experimental.JpaAssociationSync @jakarta.persistence.OneToMany(mappedBy = "post") java.util.List comments = new java.util.ArrayList<>(); + + Post() { + super(); + } + + public @java.lang.SuppressWarnings("all") @lombok.Generated void addPostComment(PostComment postComment) { + this.comments.add(postComment); + postComment.setPost(this); + } + + public @java.lang.SuppressWarnings("all") @lombok.Generated void removePostComment(PostComment postComment) { + this.comments.remove(postComment); + postComment.setPost(null); + } +} + +@jakarta.persistence.Entity(name = "PostComment") class PostComment { + private @jakarta.persistence.ManyToOne Post post; + + PostComment() { + super(); + } + + public void setPost(final Post post) { + this.post = post; + } +} diff --git a/test/transform/resource/after-ecj/JpaAssociationSyncOneToOne.java b/test/transform/resource/after-ecj/JpaAssociationSyncOneToOne.java new file mode 100644 index 0000000000..32f961231c --- /dev/null +++ b/test/transform/resource/after-ecj/JpaAssociationSyncOneToOne.java @@ -0,0 +1,35 @@ + +@jakarta.persistence.Entity(name = "Post") class Post { + private @lombok.experimental.JpaAssociationSync @jakarta.persistence.OneToOne(mappedBy = "post") PostDetails details; + + Post() { + super(); + } + + public @java.lang.SuppressWarnings("all") @lombok.Generated void updatePostDetails(PostDetails postDetails) { + if ((postDetails == null)) + { + if ((this.details != null)) + { + this.details.setPost(null); + } + } + else + { + postDetails.setPost(this); + } + this.details = postDetails; + } +} + +@jakarta.persistence.Entity(name = "PostDetails") class PostDetails { + private @jakarta.persistence.OneToOne Post post; + + PostDetails() { + super(); + } + + public void setPost(final Post post) { + this.post = post; + } +} diff --git a/test/transform/resource/after-ecj/JpaAssociationSyncOneToOneInverse.java b/test/transform/resource/after-ecj/JpaAssociationSyncOneToOneInverse.java new file mode 100644 index 0000000000..583ca9f7f4 --- /dev/null +++ b/test/transform/resource/after-ecj/JpaAssociationSyncOneToOneInverse.java @@ -0,0 +1,35 @@ + +@jakarta.persistence.Entity(name = "Post") class Post { + private @lombok.experimental.JpaAssociationSync @lombok.experimental.JpaAssociationSync.Extra(inverseSideFieldName = "post") @jakarta.persistence.OneToOne PostDetails details; + + Post() { + super(); + } + + public @java.lang.SuppressWarnings("all") @lombok.Generated void updatePostDetails(PostDetails postDetails) { + if ((postDetails == null)) + { + if ((this.details != null)) + { + this.details.setPost(null); + } + } + else + { + postDetails.setPost(this); + } + this.details = postDetails; + } +} + +@jakarta.persistence.Entity(name = "PostDetails") class PostDetails { + private @jakarta.persistence.OneToOne(mappedBy = "details") Post post; + + PostDetails() { + super(); + } + + public void setPost(final Post post) { + this.post = post; + } +} diff --git a/test/transform/resource/before/JpaAssociationSyncManyToMany.java b/test/transform/resource/before/JpaAssociationSyncManyToMany.java new file mode 100644 index 0000000000..ce6941befc --- /dev/null +++ b/test/transform/resource/before/JpaAssociationSyncManyToMany.java @@ -0,0 +1,17 @@ + +@jakarta.persistence.Entity(name = "Post") +class Post { + @lombok.experimental.JpaAssociationSync + @jakarta.persistence.ManyToMany(mappedBy = "posts") + private java.util.Set tags = new java.util.HashSet<>(); +} + +@jakarta.persistence.Entity(name = "Tag") +class Tag { + @jakarta.persistence.ManyToMany + private java.util.Set posts = new java.util.HashSet<>(); + + public java.util.Set getPosts() { + return this.posts; + } +} diff --git a/test/transform/resource/before/JpaAssociationSyncManyToManyInverse.java b/test/transform/resource/before/JpaAssociationSyncManyToManyInverse.java new file mode 100644 index 0000000000..5a598ebc05 --- /dev/null +++ b/test/transform/resource/before/JpaAssociationSyncManyToManyInverse.java @@ -0,0 +1,18 @@ + +@jakarta.persistence.Entity(name = "Post") +class Post { + @lombok.experimental.JpaAssociationSync + @lombok.experimental.JpaAssociationSync.Extra(inverseSideFieldName = "posts") + @jakarta.persistence.ManyToMany + private java.util.Set tags = new java.util.HashSet<>(); +} + +@jakarta.persistence.Entity(name = "Tag") +class Tag { + @jakarta.persistence.ManyToMany(mappedBy = "tags") + private java.util.Set posts = new java.util.HashSet<>(); + + public java.util.Set getPosts() { + return this.posts; + } +} diff --git a/test/transform/resource/before/JpaAssociationSyncOnClass.java b/test/transform/resource/before/JpaAssociationSyncOnClass.java new file mode 100644 index 0000000000..da0a5862da --- /dev/null +++ b/test/transform/resource/before/JpaAssociationSyncOnClass.java @@ -0,0 +1,43 @@ + +@lombok.experimental.JpaAssociationSync +@jakarta.persistence.Entity(name = "Post") +class Post { + @jakarta.persistence.OneToMany(mappedBy = "post") + private java.util.List comments = new java.util.ArrayList<>(); + + @jakarta.persistence.OneToOne(mappedBy = "post") + private PostDetails details; + + @jakarta.persistence.ManyToMany(mappedBy = "posts") + private java.util.Set tags = new java.util.HashSet<>(); +} + +@jakarta.persistence.Entity(name = "PostComment") +class PostComment { + @jakarta.persistence.ManyToOne + private Post post; + + public void setPost(final Post post) { + this.post = post; + } +} + +@jakarta.persistence.Entity(name = "PostDetails") +class PostDetails { + @jakarta.persistence.OneToOne + private Post post; + + public void setPost(final Post post) { + this.post = post; + } +} + +@jakarta.persistence.Entity(name = "Tag") +class Tag { + @jakarta.persistence.ManyToMany + private java.util.Set posts = new java.util.HashSet<>(); + + public java.util.Set getPosts() { + return this.posts; + } +} diff --git a/test/transform/resource/before/JpaAssociationSyncOnClassWithParamName.java b/test/transform/resource/before/JpaAssociationSyncOnClassWithParamName.java new file mode 100644 index 0000000000..d66a34b6b0 --- /dev/null +++ b/test/transform/resource/before/JpaAssociationSyncOnClassWithParamName.java @@ -0,0 +1,46 @@ + +@lombok.experimental.JpaAssociationSync +@jakarta.persistence.Entity(name = "Post") +class Post { + @lombok.experimental.JpaAssociationSync.Extra(paramName = "anotherName") + @jakarta.persistence.OneToMany(mappedBy = "post") + private java.util.List comments = new java.util.ArrayList<>(); + + @lombok.experimental.JpaAssociationSync.Extra(paramName = "anotherName1") + @jakarta.persistence.OneToOne(mappedBy = "post") + private PostDetails details; + + @lombok.experimental.JpaAssociationSync.Extra(paramName = "anotherName2") + @jakarta.persistence.ManyToMany(mappedBy = "posts") + private java.util.Set tags = new java.util.HashSet<>(); +} + +@jakarta.persistence.Entity(name = "PostComment") +class PostComment { + @jakarta.persistence.ManyToOne + private Post post; + + public void setPost(final Post post) { + this.post = post; + } +} + +@jakarta.persistence.Entity(name = "PostDetails") +class PostDetails { + @jakarta.persistence.OneToOne + private Post post; + + public void setPost(final Post post) { + this.post = post; + } +} + +@jakarta.persistence.Entity(name = "Tag") +class Tag { + @jakarta.persistence.ManyToMany + private java.util.Set posts = new java.util.HashSet<>(); + + public java.util.Set getPosts() { + return this.posts; + } +} diff --git a/test/transform/resource/before/JpaAssociationSyncOneToMany.java b/test/transform/resource/before/JpaAssociationSyncOneToMany.java new file mode 100644 index 0000000000..9e47ee5df9 --- /dev/null +++ b/test/transform/resource/before/JpaAssociationSyncOneToMany.java @@ -0,0 +1,17 @@ + +@jakarta.persistence.Entity(name = "Post") +class Post { + @lombok.experimental.JpaAssociationSync + @jakarta.persistence.OneToMany(mappedBy = "post") + private java.util.List comments = new java.util.ArrayList<>(); +} + +@jakarta.persistence.Entity(name = "PostComment") +class PostComment { + @jakarta.persistence.ManyToOne + private Post post; + + public void setPost(final Post post) { + this.post = post; + } +} diff --git a/test/transform/resource/before/JpaAssociationSyncOneToOne.java b/test/transform/resource/before/JpaAssociationSyncOneToOne.java new file mode 100644 index 0000000000..aaf71203a5 --- /dev/null +++ b/test/transform/resource/before/JpaAssociationSyncOneToOne.java @@ -0,0 +1,17 @@ + +@jakarta.persistence.Entity(name = "Post") +class Post { + @lombok.experimental.JpaAssociationSync + @jakarta.persistence.OneToOne(mappedBy = "post") + private PostDetails details; +} + +@jakarta.persistence.Entity(name = "PostDetails") +class PostDetails { + @jakarta.persistence.OneToOne + private Post post; + + public void setPost(final Post post) { + this.post = post; + } +} diff --git a/test/transform/resource/before/JpaAssociationSyncOneToOneInverse.java b/test/transform/resource/before/JpaAssociationSyncOneToOneInverse.java new file mode 100644 index 0000000000..39f7c09686 --- /dev/null +++ b/test/transform/resource/before/JpaAssociationSyncOneToOneInverse.java @@ -0,0 +1,18 @@ + +@jakarta.persistence.Entity(name = "Post") +class Post { + @lombok.experimental.JpaAssociationSync + @lombok.experimental.JpaAssociationSync.Extra(inverseSideFieldName = "post") + @jakarta.persistence.OneToOne + private PostDetails details; +} + +@jakarta.persistence.Entity(name = "PostDetails") +class PostDetails { + @jakarta.persistence.OneToOne(mappedBy = "details") + private Post post; + + public void setPost(final Post post) { + this.post = post; + } +} From 23b652f063d99529c3403d032dd1247fc9ff0668 Mon Sep 17 00:00:00 2001 From: Sergey Rukin Date: Mon, 17 Jun 2024 21:46:57 +0500 Subject: [PATCH 3/3] update AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index a8c9bcc459..2b1a8eeb86 100755 --- a/AUTHORS +++ b/AUTHORS @@ -52,6 +52,7 @@ Roel Spilker Roland Praml Rostislav Krasny <45571812+rosti-il@users.noreply.github.com> Samuel Pereira +Sergey Rukin Sasha Koning Szymon Pacanowski Taiki Sugawara