diff --git a/modello-core/pom.xml b/modello-core/pom.xml index b4d99bf64..0ae29f85f 100644 --- a/modello-core/pom.xml +++ b/modello-core/pom.xml @@ -48,6 +48,11 @@ junit-jupiter-api test + + org.junit.jupiter + junit-jupiter-params + test + org.codehaus.plexus plexus-testing diff --git a/modello-core/src/main/java/org/codehaus/modello/ModelloParameterConstants.java b/modello-core/src/main/java/org/codehaus/modello/ModelloParameterConstants.java index 820dbe545..572144dc2 100644 --- a/modello-core/src/main/java/org/codehaus/modello/ModelloParameterConstants.java +++ b/modello-core/src/main/java/org/codehaus/modello/ModelloParameterConstants.java @@ -87,5 +87,11 @@ public class ModelloParameterConstants { */ public static final String LICENSE_TEXT = "modello.license.text"; + /** + * Additional plural to singular exceptions + * @since 2.5.0 + */ + public static final String PLURAL_EXCEPTIONS = "modello.plural.exceptions"; + private ModelloParameterConstants() {} } diff --git a/modello-core/src/main/java/org/codehaus/modello/plugin/AbstractModelloGenerator.java b/modello-core/src/main/java/org/codehaus/modello/plugin/AbstractModelloGenerator.java index f58a8c28c..e4fb59eec 100644 --- a/modello-core/src/main/java/org/codehaus/modello/plugin/AbstractModelloGenerator.java +++ b/modello-core/src/main/java/org/codehaus/modello/plugin/AbstractModelloGenerator.java @@ -32,8 +32,10 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import org.codehaus.modello.ModelloException; import org.codehaus.modello.ModelloParameterConstants; @@ -57,6 +59,62 @@ public abstract class AbstractModelloGenerator implements ModelloGenerator { private final Logger logger = LoggerFactory.getLogger(getClass()); + private static final Map PLURAL_EXCEPTIONS = new HashMap<>(); + + static { + // Irregular names + PLURAL_EXCEPTIONS.put("children", "child"); + PLURAL_EXCEPTIONS.put("feet", "foot"); + PLURAL_EXCEPTIONS.put("geese", "goose"); + PLURAL_EXCEPTIONS.put("indices", "index"); + PLURAL_EXCEPTIONS.put("men", "man"); + PLURAL_EXCEPTIONS.put("mice", "mouse"); + PLURAL_EXCEPTIONS.put("people", "person"); + PLURAL_EXCEPTIONS.put("teeth", "tooth"); + PLURAL_EXCEPTIONS.put("women", "woman"); + + // Invariant names + PLURAL_EXCEPTIONS.put("aircraft", "aircraft"); + PLURAL_EXCEPTIONS.put("bison", "bison"); + PLURAL_EXCEPTIONS.put("deer", "deer"); + PLURAL_EXCEPTIONS.put("elk", "elk"); + PLURAL_EXCEPTIONS.put("fish", "fish"); + PLURAL_EXCEPTIONS.put("series", "series"); + PLURAL_EXCEPTIONS.put("sheep", "sheep"); + PLURAL_EXCEPTIONS.put("species", "species"); + + // Special "oes" exceptions + PLURAL_EXCEPTIONS.put("buffaloes", "buffalo"); + PLURAL_EXCEPTIONS.put("cargoes", "cargo"); + PLURAL_EXCEPTIONS.put("echoes", "echo"); + PLURAL_EXCEPTIONS.put("goes", "go"); + PLURAL_EXCEPTIONS.put("haloes", "halo"); + PLURAL_EXCEPTIONS.put("heroes", "hero"); + PLURAL_EXCEPTIONS.put("mosquitoes", "mosquito"); + PLURAL_EXCEPTIONS.put("noes", "no"); + PLURAL_EXCEPTIONS.put("potatoes", "potato"); + PLURAL_EXCEPTIONS.put("tomatoes", "tomato"); + PLURAL_EXCEPTIONS.put("torpedoes", "torpedo"); + PLURAL_EXCEPTIONS.put("vetoes", "veto"); + PLURAL_EXCEPTIONS.put("volcanoes", "volcano"); + + // Special "ses" exceptions + PLURAL_EXCEPTIONS.put("horses", "horse"); + PLURAL_EXCEPTIONS.put("licenses", "license"); + PLURAL_EXCEPTIONS.put("phases", "phase"); + + // Special "zzes" exceptions + PLURAL_EXCEPTIONS.put("fezzes", "fez"); + PLURAL_EXCEPTIONS.put("whizzes", "whiz"); + + // Special "ies" exceptions + PLURAL_EXCEPTIONS.put("movies", "movie"); + + // Special "ves" exceptions + PLURAL_EXCEPTIONS.put("archives", "archive"); + PLURAL_EXCEPTIONS.put("relatives", "relative"); + } + private Model model; private File outputDirectory; @@ -76,6 +134,7 @@ protected Logger getLogger() { return logger; } + @SuppressWarnings("uncheked") protected void initialize(Model model, Map parameters) throws ModelloException { this.model = model; @@ -91,6 +150,9 @@ protected void initialize(Model model, Map parameters) throws Mo encoding = (String) parameters.get(ModelloParameterConstants.ENCODING); licenseText = (List) parameters.get(ModelloParameterConstants.LICENSE_TEXT); + + Optional.ofNullable(parameters.get(ModelloParameterConstants.PLURAL_EXCEPTIONS)) + .ifPresent(o -> PLURAL_EXCEPTIONS.putAll((Map) o)); } protected Model getModel() { @@ -150,6 +212,7 @@ protected boolean isClassInModel(String fieldType, Model model) { /** * Return the child fields of this class. + * * @param modelClass current class * @return the list of fields of this class */ @@ -194,23 +257,67 @@ protected String capitalise(String str) { } public static String singular(String name) { - if (StringUtils.isEmpty(name)) { - return name; + if (name == null || name.isEmpty()) return name; + + String lower = name.toLowerCase(); + + if (!lower.equals(name)) { + // we can have a case like otherArchives + String[] split = splitByUpperCase(name); + if (split != null && PLURAL_EXCEPTIONS.containsKey(split[1])) { + String plural = PLURAL_EXCEPTIONS.get(split[1]); + return split[0] + Character.toUpperCase(plural.charAt(0)) + plural.substring(1); + } } - if (name.endsWith("ies")) { + if (PLURAL_EXCEPTIONS.containsKey(lower)) { + return PLURAL_EXCEPTIONS.get(lower); + } + + // Suffix-based rules + if (lower.endsWith("ies") && name.length() > 3) { return name.substring(0, name.length() - 3) + "y"; - } else if (name.endsWith("es") && name.endsWith("ches")) { + } + if (lower.endsWith("aves") || lower.endsWith("lves") || lower.endsWith("rves")) { + return name.substring(0, name.length() - 3) + "f"; + } + if (lower.endsWith("ves") && !lower.endsWith("fves")) { + return name.substring(0, name.length() - 3) + "fe"; + } + if (lower.endsWith("zzes")) { return name.substring(0, name.length() - 2); - } else if (name.endsWith("xes")) { + } + if (lower.endsWith("sses")) { return name.substring(0, name.length() - 2); - } else if (name.endsWith("s") && (name.length() != 1)) { + } + if (lower.endsWith("ses")) { + return name.substring(0, name.length() - 2); + } + if (lower.endsWith("ches") || lower.endsWith("shes")) { + return name.substring(0, name.length() - 2); + } + if (lower.endsWith("xes")) { + return name.substring(0, name.length() - 2); + } + if (lower.endsWith("oes")) { + return name.substring(0, name.length() - 1); + } + if (lower.endsWith("s") && name.length() > 1) { return name.substring(0, name.length() - 1); } return name; } + private static String[] splitByUpperCase(String name) { + for (int i = name.length() - 1; i >= 0; i--) { + if (Character.isUpperCase(name.charAt(i))) { + return new String[] {name.substring(0, i), name.substring(i).toLowerCase()}; + } + } + return null; + } + public static String uncapitalise(String str) { if (StringUtils.isEmpty(str)) { return str; diff --git a/modello-core/src/test/java/org/codehaus/modello/plugin/AbstractModelloGeneratorTest.java b/modello-core/src/test/java/org/codehaus/modello/plugin/AbstractModelloGeneratorTest.java new file mode 100644 index 000000000..30ddb729d --- /dev/null +++ b/modello-core/src/test/java/org/codehaus/modello/plugin/AbstractModelloGeneratorTest.java @@ -0,0 +1,119 @@ +package org.codehaus.modello.plugin; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class AbstractModelloGeneratorTest { + + @CsvSource({ + ",", + "'',''", + "s,s", + + // Known exceptions + "men, man", + "women, woman", + "children,child", + "mice, mouse", + "people, person", + "teeth, tooth", + "feet, foot", + "geese, goose", + "series, series", + "species, species", + "sheep, sheep", + "fish, fish", + "deer, deer", + "aircraft, aircraft", + "heroes, hero", + "potatoes, potato", + "tomatoes, tomato", + "echoes, echo", + "vetoes, veto", + "torpedoes, torpedo", + "cargoes, cargo", + "haloes, halo", + "mosquitoes, mosquito", + "buffaloes, buffalo", + "bison, bison", + "elk, elk", + + // Regular plural forms with suffixes + "voes, voe", + "hoes, hoe", + "canoes, canoe", + "toes, toe", + "foes, foe", + "oboes, oboe", + "noes, no", + "boxes, box", + "wishes, wish", + "dishes, dish", + "brushes, brush", + "classes, class", + "buzzes, buzz", + "cars, car", + "dogs, dog", + "cats, cat", + "horses, horse", + "fezzes, fez", + "whizzes, whiz", + "foxes, fox", + + // Some test cases with different rules + "archives, archive", + "otherArchives, otherArchive", + "Archives, Archive", + "wolves, wolf", + "knives, knife", + "leaves, leaf", + "wives, wife", + "lives, life", + "babies, baby", + "parties, party", + "cities, city", + "buses, bus", + "boxes, box", + "churches, church", + "matches, match", + "watches, watch", + "riches, rich", + "dresses, dress", + "crosses, cross", + "lunches, lunch", + "relatives, relative", + + // More edge cases + "heroes, hero", + "vetoes, veto", + "torpedoes, torpedo", + "tomatoes, tomato", + "potatoes, potato", + "echoes, echo", + "mosquitoes, mosquito", + "buffaloes, buffalo", + "volcanoes, volcano", + "goes, go", + "indices, index", + "phases, phase", + "kisses, kiss", + "movies, movie", + "shoes, shoe", + + // other examples + "aliases, alias", + "ids, id", + "licenses, license", + "repositories, repository", + "roles, role", + }) + @ParameterizedTest + public void testSingular(String plural, String singular) { + assertEquals( + singular, + AbstractModelloGenerator.singular(plural), + "singular of: " + plural + " should be: " + singular); + } +} diff --git a/modello-maven-plugin/src/it/clone/pom.xml b/modello-maven-plugin/src/it/clone/pom.xml index a3600e97c..063ae02ca 100644 --- a/modello-maven-plugin/src/it/clone/pom.xml +++ b/modello-maven-plugin/src/it/clone/pom.xml @@ -56,6 +56,9 @@ src/main/mdo/thing.mdo + + ownSingularStringSet + diff --git a/modello-maven-plugin/src/it/clone/src/main/mdo/thing.mdo b/modello-maven-plugin/src/it/clone/src/main/mdo/thing.mdo index aa8a806d2..9a5de71ca 100644 --- a/modello-maven-plugin/src/it/clone/src/main/mdo/thing.mdo +++ b/modello-maven-plugin/src/it/clone/src/main/mdo/thing.mdo @@ -104,7 +104,7 @@ under the License. - someStringList + someStringLists 1.0.0 List @@ -113,7 +113,7 @@ under the License. - someStringSet + someStringSets 1.0.0 Set diff --git a/modello-maven-plugin/src/it/clone/src/test/java/test/CloneTest.java b/modello-maven-plugin/src/it/clone/src/test/java/test/CloneTest.java index 41f8e167c..e824dfcec 100644 --- a/modello-maven-plugin/src/it/clone/src/test/java/test/CloneTest.java +++ b/modello-maven-plugin/src/it/clone/src/test/java/test/CloneTest.java @@ -37,7 +37,7 @@ public void testClone() orig.setSomeDate( new Date() ); orig.setSomeDom( new Xpp3Dom( "test" ) ); orig.addSomeStringList( "string" ); - orig.addSomeStringSet( "string" ); + orig.addOwnSingularStringSet( "string" ); orig.setDeepThingy( new Thingy() ); orig.addDeepThingyList( new Thingy() ); orig.addDeepThingySet( new Thingy() ); @@ -67,10 +67,10 @@ public void testClone() assertEquals( orig.getSomeDom(), copy.getSomeDom() ); assertNotSame( orig.getSomeDom(), copy.getSomeDom() ); - assertEquals( orig.getSomeStringList(), copy.getSomeStringList() ); - assertNotSame( orig.getSomeStringList(), copy.getSomeStringList() ); - assertEquals( orig.getSomeStringSet(), copy.getSomeStringSet() ); - assertNotSame( orig.getSomeStringSet(), copy.getSomeStringSet() ); + assertEquals( orig.getSomeStringLists(), copy.getSomeStringLists() ); + assertNotSame( orig.getSomeStringLists(), copy.getSomeStringLists() ); + assertEquals( orig.getSomeStringSets(), copy.getSomeStringSets() ); + assertNotSame( orig.getSomeStringSets(), copy.getSomeStringSets() ); assertNotSame( orig.getDeepThingy(), copy.getDeepThingy() ); assertNotSame( orig.getDeepThingyList(), copy.getDeepThingyList() ); diff --git a/modello-maven-plugin/src/main/java/org/codehaus/modello/maven/AbstractModelloGeneratorMojo.java b/modello-maven-plugin/src/main/java/org/codehaus/modello/maven/AbstractModelloGeneratorMojo.java index 7dc03da8f..936df9dcb 100644 --- a/modello-maven-plugin/src/main/java/org/codehaus/modello/maven/AbstractModelloGeneratorMojo.java +++ b/modello-maven-plugin/src/main/java/org/codehaus/modello/maven/AbstractModelloGeneratorMojo.java @@ -115,6 +115,19 @@ public abstract class AbstractModelloGeneratorMojo extends AbstractMojo { @Parameter private File licenseFile; + /** + * Additional exceptions to the singularization rules, changing plural noun to singular. + *

+ * As a kay we provide plural noun and as value we provide singular noun, eg: + *

+     *     kiss
+     * 
+ * + * @since 2.5.0 + */ + @Parameter + private Map pluralExceptions; + /** * @since 1.0.1 */ @@ -180,6 +193,8 @@ public void execute() throws MojoExecutionException { parameters.put(ModelloParameterConstants.PACKAGE_WITH_VERSION, Boolean.toString(packageWithVersion)); + parameters.put(ModelloParameterConstants.PLURAL_EXCEPTIONS, keysToLower(pluralExceptions)); + if (!packagedVersions.isEmpty()) { parameters.put(ModelloParameterConstants.ALL_VERSIONS, StringUtils.join(packagedVersions.iterator(), ",")); } @@ -224,6 +239,13 @@ public void execute() throws MojoExecutionException { } } + private Object keysToLower(Map maps) { + if (maps == null) { + return null; + } + return maps.entrySet().stream().collect(Collectors.toMap(e -> e.getKey().toLowerCase(), Map.Entry::getValue)); + } + /** * Performs execute on a single specified model. */ diff --git a/modello-plugins/modello-plugin-java/src/main/java/org/codehaus/modello/plugin/java/JavaModelloGenerator.java b/modello-plugins/modello-plugin-java/src/main/java/org/codehaus/modello/plugin/java/JavaModelloGenerator.java index 890c26c2a..19cdeb5f0 100644 --- a/modello-plugins/modello-plugin-java/src/main/java/org/codehaus/modello/plugin/java/JavaModelloGenerator.java +++ b/modello-plugins/modello-plugin-java/src/main/java/org/codehaus/modello/plugin/java/JavaModelloGenerator.java @@ -1665,7 +1665,7 @@ private void createAdder(ModelAssociation modelAssociation, JClass jClass, boole jClass.addMethod(adder); } else { - String adderName = "add" + singular(capitalise(singular(fieldName))); + String adderName = "add" + capitalise(singular(fieldName)); JMethod adder; if (isBuilderMethod) { @@ -1711,7 +1711,7 @@ private void createAdder(ModelAssociation modelAssociation, JClass jClass, boole return; } - JMethod remover = new JMethod("remove" + singular(capitalise(fieldName))); + JMethod remover = new JMethod("remove" + capitalise(singular(fieldName))); remover.addParameter(new JParameter(addType, parameterName));