package org.springframework.roo.addon.javabean.addon; import static org.springframework.roo.model.JavaType.BOOLEAN_PRIMITIVE; import static org.springframework.roo.model.JavaType.INT_PRIMITIVE; import static org.springframework.roo.model.JavaType.OBJECT; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.List; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.builder.ToStringBuilder; import org.springframework.roo.addon.javabean.annotations.RooEquals; import org.springframework.roo.classpath.PhysicalTypeIdentifierNamingUtils; import org.springframework.roo.classpath.PhysicalTypeMetadata; import org.springframework.roo.classpath.details.FieldMetadata; import org.springframework.roo.classpath.details.MethodMetadata; import org.springframework.roo.classpath.details.MethodMetadataBuilder; import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType; import org.springframework.roo.classpath.details.comments.CommentStructure; import org.springframework.roo.classpath.details.comments.CommentStructure.CommentLocation; import org.springframework.roo.classpath.details.comments.JavadocComment; import org.springframework.roo.classpath.itd.AbstractItdTypeDetailsProvidingMetadataItem; import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder; import org.springframework.roo.metadata.MetadataIdentificationUtils; import org.springframework.roo.model.JavaSymbolName; import org.springframework.roo.model.JavaType; import org.springframework.roo.project.LogicalPath; import org.springframework.roo.support.util.CollectionUtils; /** * Metadata for {@link RooEquals}. * * @author Alan Stewart * @since 1.2.0 */ public class EqualsMetadata extends AbstractItdTypeDetailsProvidingMetadataItem { private static final JavaType EQUALS_BUILDER = new JavaType( "org.apache.commons.lang3.builder.EqualsBuilder"); private static final JavaSymbolName EQUALS_METHOD_NAME = new JavaSymbolName("equals"); private static final JavaType HASH_CODE_BUILDER = new JavaType( "org.apache.commons.lang3.builder.HashCodeBuilder"); private static final JavaSymbolName HASH_CODE_METHOD_NAME = new JavaSymbolName("hashCode"); private static final String OBJECT_NAME = "obj"; private static final String PROVIDES_TYPE_STRING = EqualsMetadata.class.getName(); private static final String PROVIDES_TYPE = MetadataIdentificationUtils .create(PROVIDES_TYPE_STRING); public static String createIdentifier(final JavaType javaType, final LogicalPath path) { return PhysicalTypeIdentifierNamingUtils.createIdentifier(PROVIDES_TYPE_STRING, javaType, path); } public static JavaType getJavaType(final String metadataIdentificationString) { return PhysicalTypeIdentifierNamingUtils.getJavaType(PROVIDES_TYPE_STRING, metadataIdentificationString); } /** * Returns the class-level ID of this type of metadata * * @return a valid class-level MID */ public static String getMetadataIdentiferType() { return PROVIDES_TYPE; } public static LogicalPath getPath(final String metadataIdentificationString) { return PhysicalTypeIdentifierNamingUtils.getPath(PROVIDES_TYPE_STRING, metadataIdentificationString); } public static boolean isValid(final String metadataIdentificationString) { return PhysicalTypeIdentifierNamingUtils.isValid(PROVIDES_TYPE_STRING, metadataIdentificationString); } private final EqualsAnnotationValues annotationValues; private final List<FieldMetadata> locatedFields; private final FieldMetadata identifierField; private final boolean isJpaEntity; /** * Constructor * * @param identifier the ID of this piece of metadata (required) * @param aspectName the name of the ITD to generate (required) * @param governorPhysicalTypeMetadata the details of the governor * (required) * @param annotationValues the values of the @RooEquals annotation * (required) * @param equalityFields the fields to be compared by the * `equals` method (can be `null` or empty) * @param identifierField the identifier field, in case the destination * was an entity */ public EqualsMetadata(final String identifier, final JavaType aspectName, final PhysicalTypeMetadata governorPhysicalTypeMetadata, final EqualsAnnotationValues annotationValues, final List<FieldMetadata> equalityFields, final FieldMetadata identifierField) { super(identifier, aspectName, governorPhysicalTypeMetadata); Validate.isTrue(isValid(identifier), "Metadata id '%s' is invalid", identifier); Validate.notNull(annotationValues, "Annotation values required"); this.isJpaEntity = annotationValues.isJpaEntity(); if (this.isJpaEntity) { Validate.notNull(identifierField, "Couldn't find any identifier field for %s", this.destination.getSimpleTypeName()); } this.annotationValues = annotationValues; this.locatedFields = equalityFields; this.identifierField = identifierField; if (!CollectionUtils.isEmpty(equalityFields)) { ensureGovernorHasMethod(new MethodMetadataBuilder(getEqualsMethod())); ensureGovernorHasMethod(new MethodMetadataBuilder(getHashCodeMethod())); } // Create a representation of the desired output ITD itdTypeDetails = builder.build(); } /** * Returns the default `equals` method body. Used for not-entity classes. * * @return {@link InvocableMemberBodyBuilder} */ private InvocableMemberBodyBuilder getDefaultEqualsMethodBody() { String typeName = this.destination.getSimpleTypeName(); final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); bodyBuilder.appendFormalLine("if (!(" + OBJECT_NAME + " instanceof " + typeName + ")) {"); bodyBuilder.indent(); bodyBuilder.appendFormalLine("return false;"); bodyBuilder.indentRemove(); bodyBuilder.appendFormalLine("}"); bodyBuilder.appendFormalLine("if (this == " + OBJECT_NAME + ") {"); bodyBuilder.indent(); bodyBuilder.appendFormalLine("return true;"); bodyBuilder.indentRemove(); bodyBuilder.appendFormalLine("}"); bodyBuilder.appendFormalLine(typeName + " rhs = (" + typeName + ") " + OBJECT_NAME + ";"); final StringBuilder builder = new StringBuilder(String.format("return new %s()", getNameOfJavaType(EQUALS_BUILDER))); if (annotationValues.isAppendSuper()) { builder.append(".appendSuper(super.equals(" + OBJECT_NAME + "))"); } for (final FieldMetadata field : locatedFields) { builder.append(".append(" + field.getFieldName() + ", rhs." + field.getFieldName() + ")"); } builder.append(".isEquals();"); bodyBuilder.appendFormalLine(builder.toString()); return bodyBuilder; } /** * Returns the default `hasCode` method return statement * * @return a {@link StringBuilder} */ private StringBuilder getDefaultHashCodeMethodReturnStatment() { final StringBuilder builder = new StringBuilder(String.format("return new %s()", getNameOfJavaType(HASH_CODE_BUILDER))); if (annotationValues.isAppendSuper()) { builder.append(".appendSuper(super.hashCode())"); } for (final FieldMetadata field : locatedFields) { builder.append(".append(" + field.getFieldName() + ")"); } builder.append(".toHashCode();"); return builder; } /** * Returns the `equals` method to be generated * * @return `null` if no generation is required */ private MethodMetadata getEqualsMethod() { final JavaType parameterType = OBJECT; MethodMetadata method = getGovernorMethod(EQUALS_METHOD_NAME, parameterType); if (method != null) { return method; } final List<JavaSymbolName> parameterNames = Arrays.asList(new JavaSymbolName(OBJECT_NAME)); // Create the method body depending on destination class properties InvocableMemberBodyBuilder bodyBuilder = null; if (this.annotationValues.isJpaEntity()) { bodyBuilder = getJpaEntityEqualsMethodBody(); } else { bodyBuilder = getDefaultEqualsMethodBody(); } MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder(getId(), Modifier.PUBLIC, EQUALS_METHOD_NAME, BOOLEAN_PRIMITIVE, AnnotatedJavaType.convertFromJavaTypes(parameterType), parameterNames, bodyBuilder); if (this.isJpaEntity) { CommentStructure commentStructure = new CommentStructure(); commentStructure .addComment( new JavadocComment( "This `equals` implementation is specific for JPA entities and uses " .concat(IOUtils.LINE_SEPARATOR) .concat("the entity identifier for it, following the article in ") .concat(IOUtils.LINE_SEPARATOR) .concat( "https://vladmihalcea.com/2016/06/06/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/")), CommentLocation.BEGINNING); methodBuilder.setCommentStructure(commentStructure); } return methodBuilder.build(); } /** * Returns the `hashCode` method to be generated * * @return `null` if no generation is required */ private MethodMetadata getHashCodeMethod() { MethodMetadata method = getGovernorMethod(HASH_CODE_METHOD_NAME); if (method != null) { return method; } // Create the method final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); StringBuilder builder = null; if (this.isJpaEntity) { builder = getJpaEntityHashCodeMethodReturnStatment(); } else { builder = getDefaultHashCodeMethodReturnStatment(); } bodyBuilder.appendFormalLine(builder.toString()); MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder(getId(), Modifier.PUBLIC, HASH_CODE_METHOD_NAME, INT_PRIMITIVE, bodyBuilder); if (this.isJpaEntity) { CommentStructure commentStructure = new CommentStructure(); commentStructure .addComment( new JavadocComment( "This `hashCode` implementation is specific for JPA entities and uses a fixed `int` value to be able " .concat(IOUtils.LINE_SEPARATOR) .concat( "to identify the entity in collections after a new id is assigned to the entity, following the article in ") .concat(IOUtils.LINE_SEPARATOR) .concat( "https://vladmihalcea.com/2016/06/06/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/")), CommentLocation.BEGINNING); methodBuilder.setCommentStructure(commentStructure); } return methodBuilder.build(); } /** * Returns the specific `equals` method body defined for JPA entity classes. * * @return {@link InvocableMemberBodyBuilder} */ private InvocableMemberBodyBuilder getJpaEntityEqualsMethodBody() { String typeName = this.destination.getSimpleTypeName(); final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder(); // if (this == obj) { // return true; // } bodyBuilder.appendFormalLine("if (this == " + OBJECT_NAME + ") {"); bodyBuilder.indent(); bodyBuilder.appendFormalLine("return true;"); bodyBuilder.indentRemove(); bodyBuilder.appendFormalLine("}"); // // instanceof is false if the instance is null // if (!(obj instanceof Pet)) { // return false; // } bodyBuilder.appendFormalLine("// instanceof is false if the instance is null"); bodyBuilder.appendFormalLine("if (!(" + OBJECT_NAME + " instanceof " + typeName + ")) {"); bodyBuilder.indent(); bodyBuilder.appendFormalLine("return false;"); bodyBuilder.indentRemove(); bodyBuilder.appendFormalLine("}"); // return getId() != null && Objects.equals(getId(), ((Pet) obj).getId()); bodyBuilder.appendFormalLine( "return %1$s() != null && %2$s.equals(%1$s(), ((%3$s) %4$s).%1$s());", getAccessorMethod(this.identifierField).getMethodName(), getNameOfJavaType(JavaType.OBJECTS), typeName, OBJECT_NAME); return bodyBuilder; } /** * Returns the `hasCode` method return statement for Jpa entities * * @return a {@link StringBuilder} */ private StringBuilder getJpaEntityHashCodeMethodReturnStatment() { final StringBuilder builder = new StringBuilder("return 31;"); return builder; } @Override public String toString() { final ToStringBuilder builder = new ToStringBuilder(this); builder.append("identifier", getId()); builder.append("valid", valid); builder.append("aspectName", aspectName); builder.append("destinationType", destination); builder.append("governor", governorPhysicalTypeMetadata.getId()); builder.append("itdTypeDetails", itdTypeDetails); return builder.toString(); } }