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); + } + } }