diff --git a/.github/workflows/maven-build-action.yaml b/.github/workflows/maven-build-action.yaml index 1bdb386f8..30d86d8d7 100644 --- a/.github/workflows/maven-build-action.yaml +++ b/.github/workflows/maven-build-action.yaml @@ -5,6 +5,8 @@ on: branches: [ master, jaxb-tools-4.0.x, jaxb-tools-3.x, jaxb-tools-2.x, 0.15.x ] pull_request: branches: [ master, jaxb-tools-4.0.x, jaxb-tools-3.x, jaxb-tools-2.x, 0.15.x ] + workflow_dispatch: + ## Allow's manual runs jobs: build_and_test: diff --git a/jaxb-plugins-parent/jaxb-plugins/src/main/java/org/jvnet/jaxb/plugin/commons_lang/XjcCommonsLangPlugin.java b/jaxb-plugins-parent/jaxb-plugins/src/main/java/org/jvnet/jaxb/plugin/commons_lang/XjcCommonsLangPlugin.java index 1af1ba23d..02023a3a9 100644 --- a/jaxb-plugins-parent/jaxb-plugins/src/main/java/org/jvnet/jaxb/plugin/commons_lang/XjcCommonsLangPlugin.java +++ b/jaxb-plugins-parent/jaxb-plugins/src/main/java/org/jvnet/jaxb/plugin/commons_lang/XjcCommonsLangPlugin.java @@ -30,6 +30,9 @@ import com.sun.tools.xjc.outline.ClassOutline; import com.sun.tools.xjc.outline.Outline; +import static com.sun.codemodel.JExpr._null; +import static com.sun.codemodel.JExpr.lit; + /** * Automatically generates the toString(), hashCode() and equals() methods * using Jakarta's commons-lang. @@ -62,11 +65,26 @@ * * The default ToStringStyle adopted by this plugin is MULTI_LINE_STYLE. * + * Equals options: + *
+ * -Xcommons-lang:equalsTestTransients=TRUE|FALSE (default: FALSE)
+ * -Xcommons-lang:equalsTestRecursive=TRUE|FALSE (default: FALSE)
+ * 
+ *

NOTE: + * When enabled equals passing when sub objects where last object POJO returns by ref check only but contents is equals + *

+ * Performance Cost: Deep reflection is significantly slower than standard equality checks. + *

+ * * @author Hanson Char */ public class XjcCommonsLangPlugin extends Plugin { private static final String TOSTRING_STYLE_PARAM = "-Xcommons-lang:ToStringStyle="; + private static final String EQUALS_TEST_TRANSIENTS_PARAM = "-Xcommons-lang:equalsTestTransients="; + private static final String EQUALS_TEST_RECURSIVE_PARAM = "-Xcommons-lang:equalsTestRecursive="; + + //Classes private static final String TOSTRINGSTYLE_CLASSNAME = "org.apache.commons.lang3.builder.ToStringStyle"; private static final String EQUALSBUILDER_CLASSNAME = "org.apache.commons.lang3.builder.EqualsBuilder"; private static final String HASHCODEBUILDER_CLASSNAME = "org.apache.commons.lang3.builder.HashCodeBuilder"; @@ -74,6 +92,9 @@ public class XjcCommonsLangPlugin extends Plugin private String toStringStyle = "MULTI_LINE_STYLE"; private Class customToStringStyle; + private boolean equalsTestTransients = false; + private boolean equalsTestRecursive = false; + @Override public String getOptionName() { @@ -90,6 +111,13 @@ public String getUsage() + "| SHORT_PREFIX_STYLE\n\t" + "| SIMPLE_STYLE\n\t" + "| ]\n" + + "\n" + + " Equals options:\n" + + " -Xcommons-lang:equalsTestTransients=TRUE|FALSE (default: FALSE)\n" + + " -Xcommons-lang:equalsTestRecursive=TRUE|FALSE (default: FALSE)\n" + + " NOTE: \n" + + " Performance Cost: When equalsTestRecursive is TRUE, due to Deep reflection is significantly slower than standard equality checks.\n" + + "\n" ; } @@ -141,14 +169,27 @@ private void createEqualsMethod(JDefinedClass implClass) JVar that = toStringMethod.param(Object.class, "that"); // Annotate with @Override toStringMethod.annotate(Override.class); - // Invoke EqualsBuilder.reflectionEquals(Object,Object); - toStringMethod.body()._return( - codeModel.ref(EQUALSBUILDER_CLASSNAME) - .staticInvoke("reflectionEquals") - .arg(JExpr._this()) - .arg(that) - ); - return; + if (equalsTestTransients || equalsTestRecursive) { + //Since: 3.6 + toStringMethod.body()._return( + codeModel.ref(EQUALSBUILDER_CLASSNAME) + .staticInvoke("reflectionEquals") // public static boolean reflectionEquals( + .arg(JExpr._this()) //final Object lhs, + .arg(that) // final Object rhs, + .arg(lit(equalsTestTransients)) // final boolean testTransients, + .arg(_null()) // final Class reflectUpToClass, + .arg(lit(equalsTestRecursive)) // final boolean testRecursive, + .arg(_null()) // final String... excludeFields) { + ); + } else { + // Invoke EqualsBuilder.reflectionEquals(Object,Object); + toStringMethod.body()._return( + codeModel.ref(EQUALSBUILDER_CLASSNAME) + .staticInvoke("reflectionEquals") + .arg(JExpr._this()) + .arg(that) + ); + } } private void createHashCodeMethod(JDefinedClass implClass) @@ -191,6 +232,22 @@ public int parseArgument(Options opt, String[] args, int i) } return 1; } + + // eg. -Xcommons-lang:equalsTestTransients=TRUE + if (arg.startsWith(EQUALS_TEST_TRANSIENTS_PARAM)) + { + String toStringBoolean = arg.substring(EQUALS_TEST_TRANSIENTS_PARAM.length()); + equalsTestTransients = Boolean.parseBoolean(toStringBoolean); + return 1; + } + + // eg. -Xcommons-lang:equalsTestRecursive=TRUE + if (arg.startsWith(EQUALS_TEST_RECURSIVE_PARAM)) + { + String toStringBoolean = arg.substring(EQUALS_TEST_RECURSIVE_PARAM.length()); + equalsTestRecursive = Boolean.parseBoolean(toStringBoolean); + return 1; + } return 0; } } diff --git a/jaxb-plugins-parent/jaxb-plugins/src/test/java/org/jvnet/jaxb/plugin/commons_lang/XjcCommonsLangPluginTest.java b/jaxb-plugins-parent/jaxb-plugins/src/test/java/org/jvnet/jaxb/plugin/commons_lang/XjcCommonsLangPluginTest.java new file mode 100644 index 000000000..4b6fb3a71 --- /dev/null +++ b/jaxb-plugins-parent/jaxb-plugins/src/test/java/org/jvnet/jaxb/plugin/commons_lang/XjcCommonsLangPluginTest.java @@ -0,0 +1,201 @@ +/* + * Copyright 2006 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jvnet.jaxb.plugin.commons_lang; + +import com.sun.codemodel.*; +import com.sun.tools.xjc.BadCommandLineException; +import com.sun.tools.xjc.Options; +import com.sun.tools.xjc.model.CClassInfo; +import com.sun.tools.xjc.outline.ClassOutline; +import com.sun.tools.xjc.outline.Outline; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.xml.sax.ErrorHandler; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +class XjcCommonsLangPluginTest { + + @Mock + private Outline outline; + @Mock + private Options options; + @Mock + private ErrorHandler errorHandler; + @Mock + private CClassInfo cClassInfo; + + private JDefinedClass implClass; + private XjcCommonsLangPlugin plugin; + + @BeforeEach + void setUp() throws Exception { + MockitoAnnotations.openMocks(this); + + JCodeModel codeModel = new JCodeModel(); + implClass = codeModel._class("TestGeneratedClass"); + + ClassOutline classOutline = new TestableClassOutline(cClassInfo, implClass); + when(outline.getClasses()).thenReturn((Collection) Collections.singletonList(classOutline)); + + plugin = new XjcCommonsLangPlugin(); + } + + @Test + void testGetOptionName() { + assertEquals("Xcommons-lang", plugin.getOptionName()); + } + + @Test + void testGetUsage() { + String usage = plugin.getUsage(); + assertNotNull(usage); + assertEquals(" -Xcommons-lang : generate toString(), hashCode() and equals() for generated code using Jakarta's common-lang\n" + + " [-Xcommons-lang:ToStringStyle=MULTI_LINE_STYLE\n" + + "\t| DEFAULT_STYLE\n" + + "\t| NO_FIELD_NAMES_STYLE\n" + + "\t| SHORT_PREFIX_STYLE\n" + + "\t| SIMPLE_STYLE\n" + + "\t| ]\n" + + "\n" + + " Equals options:\n" + + " -Xcommons-lang:equalsTestTransients=TRUE|FALSE (default: FALSE)\n" + + " -Xcommons-lang:equalsTestRecursive=TRUE|FALSE (default: FALSE)\n" + + " NOTE: \n" + + " Performance Cost: When equalsTestRecursive is TRUE, due to Deep reflection is significantly slower than standard equality checks.\n" + + "\n", usage); + } + + @Test + void testRunGeneratesAllMethods() throws Exception { + plugin.run(outline, options, errorHandler); + + assertTrue(hasMethod(implClass, "toString"), "toString() should be generated"); + assertTrue(hasMethod(implClass, "equals"), "equals() should be generated"); + assertTrue(hasMethod(implClass, "hashCode"), "hashCode() should be generated"); + } + + @Test + void testOnlyEqualsGeneratedWithTestTransients() throws Exception { + assertEquals(1, plugin.parseArgument(options, new String[]{"-Xcommons-lang:equalsTestTransients=TRUE"}, 0)); + + plugin.run(outline, options, errorHandler); + + + assertTrue(hasMethod(implClass, "equals"), "equals() should be generated"); + + } + + @Test + void testOnlyEqualsGeneratedWithRecursive() throws Exception { + assertEquals(1, plugin.parseArgument(options, new String[]{"-Xcommons-lang:equalsTestRecursive=TRUE"}, 0)); + + plugin.run(outline, options, errorHandler); + + assertTrue(hasMethod(implClass, "equals"), "equals() should be generated"); + + } + + @Test + void testOnlyEqualsGeneratedWithTransiantAndRecursive() throws Exception { + assertEquals(1, plugin.parseArgument(options, new String[]{"-Xcommons-lang:equalsTestTransients=TRUE"}, 0)); + assertEquals(1, plugin.parseArgument(options, new String[]{"-Xcommons-lang:equalsTestRecursive=TRUE"}, 0)); + + plugin.run(outline, options, errorHandler); + + assertTrue(hasMethod(implClass, "equals"), "equals() should be generated"); + JMethod method = getMethod(implClass, "equals").get(); + assertEquals(1, method.listParams().length); + assertEquals("that", Arrays.stream(method.listParams()).findFirst().get().name()); + assertEquals("java.lang.Object", Arrays.stream(method.listParams()).findFirst().get().type().fullName()); + //Unsure how to verify args inside since post jdk8 its now in a class that does hides its classes externally. + } + + @Test + void testParseArgumentToStringStyleStandard() throws BadCommandLineException { + String arg = "-Xcommons-lang:ToStringStyle=SIMPLE_STYLE"; + assertEquals(1, plugin.parseArgument(options, new String[]{arg}, 0)); + + plugin.run(outline, options, errorHandler); + + assertTrue(hasMethod(implClass, "toString"), "toString() should be generated"); + } + + @Test + void testParseArgumentToStringStyleCustom() throws BadCommandLineException { + String arg = "-Xcommons-lang:ToStringStyle=java.lang.String"; + assertEquals(1, plugin.parseArgument(options, new String[]{arg}, 0)); + + plugin.run(outline, options, errorHandler); + + assertTrue(hasMethod(implClass, "toString"), "toString() should be generated"); + } + + @Test + void testParseArgumentUnknown() throws BadCommandLineException { + assertEquals(0, plugin.parseArgument(options, new String[]{"-Xunknown-param"}, 0)); + + plugin.run(outline, options, errorHandler); + + assertTrue(hasMethod(implClass, "toString"), "toString() should be generated"); + assertTrue(hasMethod(implClass, "equals"), "equals() should be generated"); + assertTrue(hasMethod(implClass, "hashCode"), "hashCode() should be generated"); + } + + @Test + void testCreateToStringMethodWithCustomClassBranch() throws Exception { + String arg = "-Xcommons-lang:ToStringStyle=java.lang.String"; + plugin.parseArgument(options, new String[]{arg}, 0); + + assertTrue(plugin.run(outline, options, errorHandler)); + + assertTrue(hasMethod(implClass, "toString"), "toString() should be generated using custom class branch"); + } + + private boolean hasMethod(JDefinedClass clazz, String methodName) { + for (JMethod m : clazz.methods()) { + if (m.name().equals(methodName)) { + return true; + } + } + return false; + } + + private Optional getMethod(JDefinedClass clazz, String methodName) { + return clazz.methods().stream() + .filter(anno -> anno.name().equals(methodName)) + .findFirst(); + } + + public static class TestableClassOutline extends ClassOutline { + public TestableClassOutline(CClassInfo target, JDefinedClass implClass) { + super(target, null, null, implClass); + } + + @Override + public Outline parent() { + return null; + } + } +} diff --git a/jaxb-plugins-parent/tests/commons_lang_custom_equals/pom.xml b/jaxb-plugins-parent/tests/commons_lang_custom_equals/pom.xml new file mode 100644 index 000000000..9a8be6649 --- /dev/null +++ b/jaxb-plugins-parent/tests/commons_lang_custom_equals/pom.xml @@ -0,0 +1,48 @@ + + 4.0.0 + + org.jvnet.jaxb + jaxb-plugins-tests + 4.1.0-SNAPSHOT + ../pom.xml + + jaxb-plugins-test-commons_lang_custom_equals + jar + JAXB Tools :: JAXB Plugins :: Test [commons_lang_custom_equals] + + + org.jvnet.jaxb + jaxb-maven-plugin-testing + test + + + org.apache.commons + commons-lang3 + + + + test + + + org.jvnet.jaxb + jaxb-maven-plugin + + true + + -Xcommons-lang + -Xcommons-lang:equalsTestTransients=FALSE + -Xcommons-lang:equalsTestRecursive=TRUE + + + + org.jvnet.jaxb + jaxb-plugins + + + + + + + diff --git a/jaxb-plugins-parent/tests/commons_lang_custom_equals/src/main/java/com/example/CustomToStringStyle.java b/jaxb-plugins-parent/tests/commons_lang_custom_equals/src/main/java/com/example/CustomToStringStyle.java new file mode 100644 index 000000000..cd04dd3b7 --- /dev/null +++ b/jaxb-plugins-parent/tests/commons_lang_custom_equals/src/main/java/com/example/CustomToStringStyle.java @@ -0,0 +1,33 @@ +package com.example; + +import org.apache.commons.lang3.builder.ToStringStyle; + +public class CustomToStringStyle extends ToStringStyle { + + public static final ToStringStyle CUSTOM_TO_STRING_STYLE = new CustomToStringStyle(); + + + private static final long serialVersionUID = 1L; + + /** + * Constructs a new instance. + * + *

Use the static constant rather than instantiating.

+ */ + public CustomToStringStyle() { + setUseIdentityHashCode(false); + setContentStart("["); + setFieldSeparator(System.lineSeparator() + " "); + setFieldSeparatorAtStart(true); + setContentEnd(System.lineSeparator() + "]"); + } + + /** + * Ensure Singleton after serialization. + * @return the singleton + */ + private Object readResolve() { + return CUSTOM_TO_STRING_STYLE; + } + +} diff --git a/jaxb-plugins-parent/tests/commons_lang_custom_equals/src/main/resources/Person.xsd b/jaxb-plugins-parent/tests/commons_lang_custom_equals/src/main/resources/Person.xsd new file mode 100644 index 000000000..1ee143788 --- /dev/null +++ b/jaxb-plugins-parent/tests/commons_lang_custom_equals/src/main/resources/Person.xsd @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jaxb-plugins-parent/tests/commons_lang_custom_equals/src/test/java/org/jvnet/jaxb2_commons/tests/commons_lang/AddressTest.java b/jaxb-plugins-parent/tests/commons_lang_custom_equals/src/test/java/org/jvnet/jaxb2_commons/tests/commons_lang/AddressTest.java new file mode 100644 index 000000000..489d4f6f6 --- /dev/null +++ b/jaxb-plugins-parent/tests/commons_lang_custom_equals/src/test/java/org/jvnet/jaxb2_commons/tests/commons_lang/AddressTest.java @@ -0,0 +1,22 @@ +package org.jvnet.jaxb2_commons.tests.commons_lang; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import generated.Address; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class AddressTest { + + @Test + public void testAddress() { + Address a = new Address(); + // No plugin default-value present, checking everything is null or default java value + Assertions.assertEquals(0, a.getNumber()); + Assertions.assertNull(a.getCareOf()); + Assertions.assertNull(a.getStreet()); + + Assertions.assertEquals(a, a); + } +} diff --git a/jaxb-plugins-parent/tests/commons_lang_custom_equals/src/test/java/org/jvnet/jaxb2_commons/tests/commons_lang/PersonTest.java b/jaxb-plugins-parent/tests/commons_lang_custom_equals/src/test/java/org/jvnet/jaxb2_commons/tests/commons_lang/PersonTest.java new file mode 100644 index 000000000..f0a4bf952 --- /dev/null +++ b/jaxb-plugins-parent/tests/commons_lang_custom_equals/src/test/java/org/jvnet/jaxb2_commons/tests/commons_lang/PersonTest.java @@ -0,0 +1,21 @@ +package org.jvnet.jaxb2_commons.tests.commons_lang; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import generated.Person; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class PersonTest { + + @Test + public void testPerson() { + Person p = new Person(); + Person p2 = new Person(); + // No plugin default-value present, checking everything is null or default java value + Assertions.assertEquals(false, p.isMailingAddressIdentical()); + Assertions.assertEquals(p, p2); + + } +} diff --git a/jaxb-plugins-parent/tests/commons_lang_custom_equals/src/test/resources/log4j.properties b/jaxb-plugins-parent/tests/commons_lang_custom_equals/src/test/resources/log4j.properties new file mode 100644 index 000000000..ca4ee5e2c --- /dev/null +++ b/jaxb-plugins-parent/tests/commons_lang_custom_equals/src/test/resources/log4j.properties @@ -0,0 +1,5 @@ +log4j.rootCategory=DEBUG, stdout +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.target=system.out +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n diff --git a/jaxb-plugins-parent/tests/pom.xml b/jaxb-plugins-parent/tests/pom.xml index 41b95f7cf..43af9ff9a 100644 --- a/jaxb-plugins-parent/tests/pom.xml +++ b/jaxb-plugins-parent/tests/pom.xml @@ -19,6 +19,7 @@ camelcase commons_lang commons_lang_custom + commons_lang_custom_equals copyable defaultvalue elementwrapper