diff --git a/README.md b/README.md
index 9b52e1e4..9684aaca 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,8 @@ A site with the links to the [API docs](http://phax.github.io/jcodemodel/) etc.
# News and noteworthy
+* v3.3.0 - work in progress
+ * Added check for package names so that no invalid package names can be created ([issue #70](https://github.com/phax/jcodemodel/issues/70) from @guiguilechat)
* v3.2.4 - 2019-07-15
* Made class `JavaUnicodeEscapeWriter` publicly accessible
* Extended enum constant ref APU ([issue #68](https://github.com/phax/jcodemodel/issues/68) from @guiguilechat)
diff --git a/pom.xml b/pom.xml
index b6a2fb63..b47d3b82 100644
--- a/pom.xml
+++ b/pom.xml
@@ -49,7 +49,7 @@
1.10.9
jcodemodel
- 3.2.5-SNAPSHOT
+ 3.3.0-SNAPSHOT
bundle
jcodemodel
Java code generation library
diff --git a/src/main/java/com/helger/jcodemodel/JJavaName.java b/src/main/java/com/helger/jcodemodel/JJavaName.java
index 7e9797c9..94866d04 100644
--- a/src/main/java/com/helger/jcodemodel/JJavaName.java
+++ b/src/main/java/com/helger/jcodemodel/JJavaName.java
@@ -41,6 +41,7 @@
package com.helger.jcodemodel;
import java.util.HashSet;
+import java.util.Set;
import javax.annotation.Nonnull;
@@ -50,7 +51,7 @@
public final class JJavaName
{
/** All reserved keywords of Java. */
- private static final HashSet RESERVED_KEYWORDS = new HashSet <> ();
+ private static final Set RESERVED_KEYWORDS = new HashSet <> ();
static
{
@@ -123,6 +124,19 @@ public final class JJavaName
private JJavaName ()
{}
+ /**
+ * Check if the passed string is Java keyword or not.
+ *
+ * @param sStr
+ * The string to be checked.
+ * @return true
if the string is a Java keyword,
+ * false
if not.
+ */
+ public static boolean isJavaReservedKeyword (@Nonnull final String sStr)
+ {
+ return sStr.length () > 0 && RESERVED_KEYWORDS.contains (sStr);
+ }
+
/**
* Checks if a given string is usable as a Java identifier.
*
diff --git a/src/main/java/com/helger/jcodemodel/JPackage.java b/src/main/java/com/helger/jcodemodel/JPackage.java
index cbf08c17..0ec6800c 100644
--- a/src/main/java/com/helger/jcodemodel/JPackage.java
+++ b/src/main/java/com/helger/jcodemodel/JPackage.java
@@ -52,11 +52,14 @@
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import com.helger.jcodemodel.fmt.AbstractJResourceFile;
+import com.helger.jcodemodel.util.JCStringHelper;
import com.helger.jcodemodel.util.JCValueEnforcer;
/**
@@ -69,6 +72,70 @@ public class JPackage implements
IJAnnotatable,
IJDocCommentable
{
+ public static final Pattern VALID_PACKAGE_NAME_ANYCASE = Pattern.compile ("[A-Za-z_][A-Za-z0-9_]*");
+ public static final Pattern VALID_PACKAGE_NAME_LOWERCASE = Pattern.compile ("[a-z_][a-z0-9_]*");
+ private static final AtomicBoolean FORCE_PACKAGE_NAME_LOWERCASE = new AtomicBoolean (false);
+
+ /**
+ * @return true
if only lower case package names should be
+ * allowed, false
if also upper case characters are
+ * allowed. For backwards compatibility upper case characters are
+ * allowed so this method returns false
.
+ * @since 3.2.5
+ */
+ public static boolean isForcePackageNameLowercase ()
+ {
+ return FORCE_PACKAGE_NAME_LOWERCASE.get ();
+ }
+
+ /**
+ * Only allow lower case package names
+ *
+ * @param bForcePackageNameLowercase
+ * true
to force lower case package names are recommended
+ * by
+ * https://docs.oracle.com/javase/tutorial/java/package/namingpkgs.html
+ */
+ public static void setForcePackageNameLowercase (final boolean bForcePackageNameLowercase)
+ {
+ FORCE_PACKAGE_NAME_LOWERCASE.set (bForcePackageNameLowercase);
+ }
+
+ /**
+ * Check if the package name part is valid or not.
+ *
+ * @param sName
+ * The name part to check
+ * @return true
if it is invalid, false
if it is
+ * valid
+ */
+ public static boolean isForbiddenPackageNamePart (@Nonnull final String sName)
+ {
+ // Empty is not allowed
+ if (sName == null || sName.length () == 0)
+ return true;
+
+ // Java keywords are now allowed
+ if (JJavaName.isJavaReservedKeyword (sName))
+ return true;
+
+ if (isForcePackageNameLowercase ())
+ {
+ // Lowercase check required?
+ if (!VALID_PACKAGE_NAME_LOWERCASE.matcher (sName).matches ())
+ return true;
+ }
+ else
+ {
+ // Mixed case possible
+ if (!VALID_PACKAGE_NAME_ANYCASE.matcher (sName).matches ())
+ return true;
+ }
+
+ // not forbidden -> allowed
+ return false;
+ }
+
/**
* Name of the package. May be the empty string for the root package.
*/
@@ -116,9 +183,17 @@ public class JPackage implements
protected JPackage (@Nonnull final String sName, @Nonnull final JCodeModel aOwner)
{
JCValueEnforcer.notNull (sName, "Name");
- JCValueEnforcer.isFalse (sName.equals ("."), "Package name . is not allowed");
JCValueEnforcer.notNull (aOwner, "CodeModel");
+ // An empty package name is okay
+ if (sName.length () > 0)
+ {
+ final String [] aParts = JCStringHelper.getExplodedArray ('.', sName);
+ for (final String sPart : aParts)
+ if (isForbiddenPackageNamePart (sPart))
+ throw new IllegalArgumentException ("The package name '" + sName + "' is invalid");
+ }
+
m_aOwner = aOwner;
m_sName = sName;
if (JCodeModel.isFileSystemCaseSensitive ())
diff --git a/src/main/java/com/helger/jcodemodel/util/JCStringHelper.java b/src/main/java/com/helger/jcodemodel/util/JCStringHelper.java
new file mode 100644
index 00000000..949e5c87
--- /dev/null
+++ b/src/main/java/com/helger/jcodemodel/util/JCStringHelper.java
@@ -0,0 +1,122 @@
+package com.helger.jcodemodel.util;
+
+import javax.annotation.CheckForSigned;
+import javax.annotation.Nonnegative;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+public final class JCStringHelper
+{
+ /** Constant empty String array */
+ public static final String [] EMPTY_STRING_ARRAY = new String [0];
+
+ private JCStringHelper ()
+ {}
+
+ /**
+ * Check if the string is null
or empty.
+ *
+ * @param sStr
+ * The string to check. May be null
.
+ * @return true
if the string is null
or empty,
+ * false
otherwise
+ */
+ public static boolean hasNoText (@Nullable final String sStr)
+ {
+ return sStr == null || sStr.isEmpty ();
+ }
+
+ @Nonnegative
+ public static int getCharCount (@Nullable final String s, final char cSearch)
+ {
+ return s == null ? 0 : getCharCount (s.toCharArray (), cSearch);
+ }
+
+ @Nonnegative
+ public static int getCharCount (@Nullable final char [] aChars, final char cSearch)
+ {
+ int ret = 0;
+ if (aChars != null)
+ for (final char c : aChars)
+ if (c == cSearch)
+ ++ret;
+ return ret;
+ }
+
+ /**
+ * Take a concatenated String and return the passed String array of all
+ * elements in the passed string, using specified separator char.
+ *
+ * @param cSep
+ * The separator to use.
+ * @param sElements
+ * The concatenated String to convert. May be null
or
+ * empty.
+ * @return The passed collection and never null
.
+ */
+ @Nonnull
+ public static String [] getExplodedArray (final char cSep, @Nullable final String sElements)
+ {
+ return getExplodedArray (cSep, sElements, -1);
+ }
+
+ /**
+ * Take a concatenated String and return the passed String array of all
+ * elements in the passed string, using specified separator char.
+ *
+ * @param cSep
+ * The separator to use.
+ * @param sElements
+ * The concatenated String to convert. May be null
or
+ * empty.
+ * @param nMaxItems
+ * The maximum number of items to explode. If the passed value is ≤
+ * 0 all items are used. If max items is 1, than the result string is
+ * returned as is. If max items is larger than the number of elements
+ * found, it has no effect.
+ * @return The passed collection and never null
.
+ */
+ @Nonnull
+ public static String [] getExplodedArray (final char cSep,
+ @Nullable final String sElements,
+ @CheckForSigned final int nMaxItems)
+ {
+ if (nMaxItems == 1)
+ return new String [] { sElements };
+ if (hasNoText (sElements))
+ return EMPTY_STRING_ARRAY;
+
+ final int nMaxResultElements = 1 + getCharCount (sElements, cSep);
+ if (nMaxResultElements == 1)
+ {
+ // Separator not found
+ return new String [] { sElements };
+ }
+ final String [] ret = new String [nMaxItems < 1 ? nMaxResultElements : Math.min (nMaxResultElements, nMaxItems)];
+
+ // Do not use RegExCache.stringReplacePattern because of package
+ // dependencies
+ // Do not use String.split because it trims empty tokens from the end
+ int nStartIndex = 0;
+ int nItemsAdded = 0;
+ while (true)
+ {
+ final int nMatchIndex = sElements.indexOf (cSep, nStartIndex);
+ if (nMatchIndex < 0)
+ break;
+
+ ret[nItemsAdded++] = sElements.substring (nStartIndex, nMatchIndex);
+ // 1 == length of separator char
+ nStartIndex = nMatchIndex + 1;
+ if (nMaxItems > 0 && nItemsAdded == nMaxItems - 1)
+ {
+ // We have exactly one item the left: the rest of the string
+ break;
+ }
+ }
+ ret[nItemsAdded++] = sElements.substring (nStartIndex);
+ if (nItemsAdded != ret.length)
+ throw new IllegalStateException ("Added " + nItemsAdded + " but expected " + ret.length);
+ return ret;
+ }
+}
diff --git a/src/test/java/com/helger/jcodemodel/JPackageTest.java b/src/test/java/com/helger/jcodemodel/JPackageTest.java
index c8386fc5..02cd0b6c 100644
--- a/src/test/java/com/helger/jcodemodel/JPackageTest.java
+++ b/src/test/java/com/helger/jcodemodel/JPackageTest.java
@@ -40,8 +40,10 @@
*/
package com.helger.jcodemodel;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
import org.junit.Test;
@@ -54,10 +56,10 @@ public final class JPackageTest
public void testGetParent () throws Exception
{
// Create JCodeModel
- final JCodeModel wModel = new JCodeModel ();
+ final JCodeModel aCM = new JCodeModel ();
// Reflect into class
- final AbstractJClass wClass = wModel.ref (JExpr.class);
+ final AbstractJClass wClass = aCM.ref (JExpr.class);
// Walk up to the root package
JPackage wCurrentPackage = wClass._package ();
@@ -67,4 +69,173 @@ public void testGetParent () throws Exception
assertNotNull (wCurrentPackage);
assertNull (wCurrentPackage.parent ());
}
+
+ @Test
+ public void testInvalidNamesAnyCase ()
+ {
+ final JCodeModel aCM = new JCodeModel ();
+
+ assertFalse (JPackage.isForcePackageNameLowercase ());
+
+ // May not contain empty parts
+ try
+ {
+ aCM._package (".");
+ fail ();
+ }
+ catch (final IllegalArgumentException ex)
+ {}
+
+ // May not contain empty parts
+ try
+ {
+ aCM._package ("abc.");
+ fail ();
+ }
+ catch (final IllegalArgumentException ex)
+ {}
+
+ // May not contain empty parts
+ try
+ {
+ aCM._package ("abc.def.");
+ fail ();
+ }
+ catch (final IllegalArgumentException ex)
+ {}
+
+ // May not contain empty parts
+ try
+ {
+ aCM._package (".abc");
+ fail ();
+ }
+ catch (final IllegalArgumentException ex)
+ {}
+
+ // May not contain empty parts
+ try
+ {
+ aCM._package (".abc.def");
+ fail ();
+ }
+ catch (final IllegalArgumentException ex)
+ {}
+
+ // May not contain empty parts
+ try
+ {
+ aCM._package ("abc..def");
+ fail ();
+ }
+ catch (final IllegalArgumentException ex)
+ {}
+
+ // May not start with a number
+ try
+ {
+ aCM._package ("123");
+ fail ();
+ }
+ catch (final IllegalArgumentException ex)
+ {}
+
+ // May not be a keyword
+ try
+ {
+ aCM._package ("class");
+ fail ();
+ }
+ catch (final IllegalArgumentException ex)
+ {}
+
+ // May not be a keyword
+ try
+ {
+ aCM._package ("org.example.var");
+ fail ();
+ }
+ catch (final IllegalArgumentException ex)
+ {}
+
+ // May not be a keyword
+ try
+ {
+ aCM._package ("org.class.simple");
+ fail ();
+ }
+ catch (final IllegalArgumentException ex)
+ {}
+
+ // May not contain special chars
+ try
+ {
+ aCM._package ("org.pub$.anything");
+ fail ();
+ }
+ catch (final IllegalArgumentException ex)
+ {}
+
+ // May not contain special chars
+ try
+ {
+ aCM._package ("org.pub+.anything");
+ fail ();
+ }
+ catch (final IllegalArgumentException ex)
+ {}
+
+ // May not contain special chars
+ try
+ {
+ aCM._package ("org.pub.any/thing");
+ fail ();
+ }
+ catch (final IllegalArgumentException ex)
+ {}
+ }
+
+ @Test
+ public void testInvalidNamesLowerCase ()
+ {
+ final JCodeModel aCM = new JCodeModel ();
+
+ assertFalse (JPackage.isForcePackageNameLowercase ());
+ try
+ {
+ // Enforce lowercase
+ JPackage.setForcePackageNameLowercase (true);
+
+ // May not contain an upper case char
+ try
+ {
+ aCM._package ("Abc");
+ fail ();
+ }
+ catch (final IllegalArgumentException ex)
+ {}
+
+ // May not contain an upper case char
+ try
+ {
+ aCM._package ("org.EXAMPLE.simple");
+ fail ();
+ }
+ catch (final IllegalArgumentException ex)
+ {}
+
+ // May not contain an upper case char
+ try
+ {
+ aCM._package ("org.exmaple.UpperCase");
+ fail ();
+ }
+ catch (final IllegalArgumentException ex)
+ {}
+ }
+ finally
+ {
+ JPackage.setForcePackageNameLowercase (false);
+ }
+ }
}