Skip to content

Commit

Permalink
Added package name checks; #70
Browse files Browse the repository at this point in the history
  • Loading branch information
phax committed Sep 30, 2019
1 parent 1170363 commit 8f06931
Show file tree
Hide file tree
Showing 6 changed files with 389 additions and 5 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
<version>1.10.9</version>
</parent>
<artifactId>jcodemodel</artifactId>
<version>3.2.5-SNAPSHOT</version>
<version>3.3.0-SNAPSHOT</version>
<packaging>bundle</packaging>
<name>jcodemodel</name>
<description>Java code generation library</description>
Expand Down
16 changes: 15 additions & 1 deletion src/main/java/com/helger/jcodemodel/JJavaName.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
package com.helger.jcodemodel;

import java.util.HashSet;
import java.util.Set;

import javax.annotation.Nonnull;

Expand All @@ -50,7 +51,7 @@
public final class JJavaName
{
/** All reserved keywords of Java. */
private static final HashSet <String> RESERVED_KEYWORDS = new HashSet <> ();
private static final Set <String> RESERVED_KEYWORDS = new HashSet <> ();

static
{
Expand Down Expand Up @@ -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 <code>true</code> if the string is a Java keyword,
* <code>false</code> 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.
*
Expand Down
77 changes: 76 additions & 1 deletion src/main/java/com/helger/jcodemodel/JPackage.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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 <code>true</code> if only lower case package names should be
* allowed, <code>false</code> if also upper case characters are
* allowed. For backwards compatibility upper case characters are
* allowed so this method returns <code>false</code>.
* @since 3.2.5
*/
public static boolean isForcePackageNameLowercase ()
{
return FORCE_PACKAGE_NAME_LOWERCASE.get ();
}

/**
* Only allow lower case package names
*
* @param bForcePackageNameLowercase
* <code>true</code> 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 <code>true</code> if it is invalid, <code>false</code> 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.
*/
Expand Down Expand Up @@ -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 ())
Expand Down
122 changes: 122 additions & 0 deletions src/main/java/com/helger/jcodemodel/util/JCStringHelper.java
Original file line number Diff line number Diff line change
@@ -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 <code>null</code> or empty.
*
* @param sStr
* The string to check. May be <code>null</code>.
* @return <code>true</code> if the string is <code>null</code> or empty,
* <code>false</code> 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 <code>null</code> or
* empty.
* @return The passed collection and never <code>null</code>.
*/
@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 <code>null</code> or
* empty.
* @param nMaxItems
* The maximum number of items to explode. If the passed value is &le;
* 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 <code>null</code>.
*/
@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;
}
}
Loading

0 comments on commit 8f06931

Please sign in to comment.