Repository: pmd/pmd (Java)

Issue:
[core] Allow trailing commas in multivalued properties
**Is your feature request related to a problem? Please describe.**
```console
Exception applying rule LooseCoupling on file /Users/ben/projects/caffeine/caffeine/src/jmh/java/com/github/benmanes/caffeine/cache/impl/CoherenceCache.java, continuing with next rule
org.apache.commons.lang3.exception.ContextedRuntimeException: java.lang.IllegalArgumentException: Not a Java binary name ''
Exception Context:
        [1:Rule applied on node=!debug only! [ClassOrInterfaceType:27:17]LocalCache]
---------------------------------
        at net.sourceforge.pmd.util.AssertionUtil.contexted(AssertionUtil.java:232)
        at net.sourceforge.pmd.lang.rule.internal.RuleApplicator.applyOnIndex(RuleApplicator.java:77)
        at net.sourceforge.pmd.lang.rule.internal.RuleApplicator.apply(RuleApplicator.java:55)
        at net.sourceforge.pmd.RuleSets.apply(RuleSets.java:158)
        at net.sourceforge.pmd.lang.impl.PmdRunnable.processSource(PmdRunnable.java:140)
        at net.sourceforge.pmd.lang.impl.PmdRunnable.run(PmdRunnable.java:80)
        at java.base@11.0.15/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
        at java.base@11.0.15/java.util.concurrent.FutureTask.run(FutureTask.java:264)
        at java.base@11.0.15/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
        at java.base@11.0.15/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
        at java.base@11.0.15/java.lang.Thread.run(Thread.java:829)
Caused by: java.lang.IllegalArgumentException: Not a Java binary name ''
        at net.sourceforge.pmd.util.AssertionUtil.assertValidJavaBinaryName(AssertionUtil.java:55)
        at net.sourceforge.pmd.lang.java.types.TypeTestUtil.isA(TypeTestUtil.java:116)
        at net.sourceforge.pmd.lang.java.rule.bestpractices.LooseCouplingRule.isAllowedType(LooseCouplingRule.java:72)
        at net.sourceforge.pmd.lang.java.rule.bestpractices.LooseCouplingRule.visit(LooseCouplingRule.java:49)
        at net.sourceforge.pmd.lang.java.ast.ASTClassOrInterfaceType.acceptVisitor(ASTClassOrInterfaceType.java:159)
        at net.sourceforge.pmd.lang.java.ast.AbstractJavaNode.acceptVisitor(AbstractJavaNode.java:38)
        at net.sourceforge.pmd.lang.java.rule.AbstractJavaRule.apply(AbstractJavaRule.java:24)
        at net.sourceforge.pmd.lang.rule.AbstractDelegateRule.apply(AbstractDelegateRule.java:232)
        at net.sourceforge.pmd.lang.rule.internal.RuleApplicator.applyOnIndex(RuleApplicator.java:75)
        ... 9 more
```

**Describe the solution you'd like**
When splitting the property string by the separator and trimming, an empty string should be ignored. This avoids having to use a prefix or remembering to keep the trailing comma off the last item.

**Additional context**
```xml
  <rule ref="category/java/bestpractices.xml/LooseCoupling">
    <properties>
      <property name="allowedTypes" value="
        java.util.Properties,
        java.util.concurrent.ConcurrentHashMap,
        com.google.common.collect.ImmutableMap,
        com.google.common.collect.ImmutableSet,
        com.google.common.collect.ImmutableList,
        com.github.benmanes.caffeine.cache.BoundedLocalCache,
        com.github.benmanes.caffeine.cache.UnboundedLocalCache
      "/>
    </properties>
  </rule>
```



Fix (patch):
diff --git a/docs/pages/release_notes.md b/docs/pages/release_notes.md
index 5ee534fe541..29f830ac87f 100644
--- a/docs/pages/release_notes.md
+++ b/docs/pages/release_notes.md
@@ -25,6 +25,8 @@ This is a {{ site.pmd.release_type }} release.
 ### 🚀 New and noteworthy
 
 ### 🐛 Fixed Issues
+* core
+  * [#4714](https://github.com/pmd/pmd/issues/4714): \[core] Allow trailing commas in multivalued properties
 * apex-design
   * [#6022](https://github.com/pmd/pmd/issues/6022): \[apex] ExcessiveClassLength/ExcessiveParameterList include the metric in the message
 * java-codestyle
@@ -56,6 +58,7 @@ This is a {{ site.pmd.release_type }} release.
 * [#6024](https://github.com/pmd/pmd/pull/6024): \[java] Fix #5878: DontUseFloatTypeForLoopIndices now checks the UpdateStatement as well - [UncleOwen](https://github.com/UncleOwen) (@UncleOwen)
 * [#6029](https://github.com/pmd/pmd/pull/6029): \[java] Fix UnnecessaryCast false-negative in method calls - [Zbynek Konecny](https://github.com/zbynek) (@zbynek)
 * [#6031](https://github.com/pmd/pmd/pull/6031): \[java] Fix #5880: False Negatives in DoubleCheckedLocking - [Lukas Gräf](https://github.com/lukasgraef) (@lukasgraef)
+* [#6039](https://github.com/pmd/pmd/pull/6039): \[core] Fix #4714: trim token before feeding it to the extractor - [UncleOwen](https://github.com/UncleOwen) (@UncleOwen)
 * [#6040](https://github.com/pmd/pmd/pull/6040): \[java,apex,plsql,velocity] Change description of "minimum" parameter - [UncleOwen](https://github.com/UncleOwen) (@UncleOwen)
 
 ### 📦 Dependency updates
diff --git a/pmd-core/src/main/java/net/sourceforge/pmd/properties/internal/PropertyParsingUtil.java b/pmd-core/src/main/java/net/sourceforge/pmd/properties/internal/PropertyParsingUtil.java
index 7966886f59a..69f56aaa3e5 100644
--- a/pmd-core/src/main/java/net/sourceforge/pmd/properties/internal/PropertyParsingUtil.java
+++ b/pmd-core/src/main/java/net/sourceforge/pmd/properties/internal/PropertyParsingUtil.java
@@ -148,6 +148,15 @@ public static <T, C extends Iterable<T>> PropertySerializer<C> delimitedString(P
      * "a\"   -> [ "a\"  ]   (a backslash at the end of the string is just a backslash)
      *
      * }</pre>
+     *
+     * The list may end with a separator. Any whitespace around tokens is ignored:
+     *
+     * <pre>{@code
+     * " a , c " -> [ "a", "c" ]
+     * " a , c , " -> [ "a", "c" ]
+     * }</pre>
+     *
+     *
      */
     public static <U> List<U> parseListWithEscapes(String str, char delimiter, Function<? super String, ? extends U> extractor) {
         if (str.isEmpty()) {
@@ -165,7 +174,7 @@ public static <U> List<U> parseListWithEscapes(String str, char delimiter, Funct
                 inEscapeMode = false;
                 currentToken.append(c);
             } else if (c == delimiter) {
-                result.add(extractor.apply(currentToken.toString()));
+                result.add(extractor.apply(currentToken.toString().trim()));
                 currentToken = new StringBuilder();
             } else if (c == ESCAPE_CHAR && i < str.length() - 1) {
                 // this is ordered this way so that if the delimiter is
@@ -176,8 +185,9 @@ public static <U> List<U> parseListWithEscapes(String str, char delimiter, Funct
             }
         }
 
-        if (currentToken.length() > 0) {
-            result.add(extractor.apply(currentToken.toString()));
+        String currentTokenString = currentToken.toString().trim();
+        if (!currentTokenString.isEmpty()) {
+            result.add(extractor.apply(currentTokenString));
         }
         return result;
     }