Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
295 changes: 181 additions & 114 deletions apache-rat-core/src/main/java/org/apache/rat/utils/CasedString.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,177 +19,244 @@
package org.apache.rat.utils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.WordUtils;

/**
* Handles converting from one string case to another (e.g. camel case to snake case).
* @since 0.17
*/
public class CasedString {
/** the string of the cased format. */
private final String string;
/** the case of the string. */
public final class CasedString {
/** The segments of the cased string */
private final String[] segments;
/** The case of the string as parsed */
private final StringCase stringCase;
/** A joiner used for the pascal and camel cases. */
private static final Function<String[], String> PASCAL_JOINER = strings -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is concurrency an issue here as we are using StringBuilder vs. StringBuffer?
Just wondering while reading through:
https://stackoverflow.com/questions/2971315/string-stringbuffer-and-stringbuilder

If not just ignore my comment ;)

StringBuilder sb = new StringBuilder();
Arrays.stream(strings).map(s -> s == null ? "" : s).forEach(token -> sb.append(WordUtils.capitalize(token.toLowerCase(Locale.ROOT))));
return sb.toString();
};

/**
* A method to join camel string fragments together.
* Creates a cased string by parsing the string argument for the specific case.
* @param stringCase the case of the string being parsed.
* @param string the string to parse.
*/
private static final Function<String[], String> CAMEL_JOINER = a -> {
StringBuilder sb = new StringBuilder(a[0].toLowerCase(Locale.ROOT));
public CasedString(final StringCase stringCase, final String string) {
this.segments = string == null ? CasedString.StringCase.NULL_SEGMENT : stringCase.getSegments(string.trim());
this.stringCase = stringCase;
}

for (int i = 1; i < a.length; i++) {
sb.append(WordUtils.capitalize(a[i].toLowerCase(Locale.ROOT)));
/**
* Creates a cased string of the specified case and segments
* @param stringCase the case of the string.
* @param segments the segments of the string.
*/
public CasedString(final StringCase stringCase, final String[] segments) {
this.segments = segments;
this.stringCase = stringCase;
}

/**
* Converts this cased string into another format.
* @param stringCase the desired format.
* @return the new CasedString.
*/
public CasedString as(final StringCase stringCase) {
return stringCase.name.equals(this.stringCase.name) ? this : new CasedString(stringCase, (String[]) Arrays.copyOf(this.segments, this.segments.length));
}

/**
* Gets the segments of this cased string.
* @return the segments of this cased string.
*/
public String[] getSegments() {
return this.segments;
}

/**
* Generates a string from this cased string but with the desired case.
* @param stringCase the desired case.
* @return this cased string in the desired case.
*/
public String toCase(final StringCase stringCase) {
return this.segments == CasedString.StringCase.NULL_SEGMENT ? null : stringCase.assemble(this.getSegments());
}

@Override
public String toString() {
return this.toCase(this.stringCase);
}

@Override
public boolean equals(final Object o) {
if (o == null || getClass() != o.getClass()) {
return false;
}
return sb.toString();
};
CasedString that = (CasedString) o;
return Objects.deepEquals(getSegments(), that.getSegments()) && Objects.equals(stringCase, that.stringCase);
}

@Override
public int hashCode() {
return Objects.hash(Arrays.hashCode(getSegments()), stringCase);
}

/**
* An enumeration of supported string cases. These cases tag strings as having a specific format.
* The definition of a String case.
*/
public enum StringCase {
/**
* Camel case tags strings like 'CamelCase' or 'camelCase'. This conversion forces the first character to
* lower case. If specific capitalization rules are required use {@link WordUtils#capitalize(String)} to set the first
* character of the string.
public static final class StringCase {
/** The camel case. Example: "HelloWorld"*/
public static final StringCase CAMEL;
/** The pascal case. Example: "helloWorld" */
public static final StringCase PASCAL;
/** The Snake case. Example: "hello_world" */
public static final StringCase SNAKE;
/** The Kebab case. Example: "hello-world" */
public static final StringCase KEBAB;
/** The phrase case. Example: "hello world" */
public static final StringCase PHRASE;
/** The dot case. Example: "hello.world" */
public static final StringCase DOT;
/** The slash case. Example: "hello/world" */
public static final StringCase SLASH;
/** A marker for the parsing of a NULL string. */
private static final String[] NULL_SEGMENT;
/** An empty segment marker. */
private static final String[] EMPTY_SEGMENT;
/** The name of this case */
private final String name;
/** The predicate that determines if a character is a spliter character. A splitter character
* is the character that signals the start of a new segment.
*/
CAMEL(Character::isUpperCase, true, CAMEL_JOINER),
/**
* Snake case tags strings like 'Snake_Case'. This conversion does not change the capitalization of any characters
* in the string. If specific capitalization is required use {@link String#toUpperCase()}, {@link String#toLowerCase()},
* or the commons-text methods {@link WordUtils#capitalize(String)}, or {@link WordUtils#uncapitalize(String)} as required.
*/
SNAKE(c -> c == '_', false, a -> String.join("_", a)),
private final Predicate<Character> splitter;
/**
* Kebab case tags strings like 'kebab-case'. This conversion does not change the capitalization of any characters
* in the string. If specific capitalization is required use {@link String#toUpperCase()}, {@link String#toLowerCase()},
* * or the commons-text methods {@link WordUtils#capitalize(String)}, or {@link WordUtils#uncapitalize(String)} as required.
* If {@code true} the spliter character is preserved as part of the subsequent section otherwise,
* the spliter character is discarded.
*/
KEBAB(c -> c == '-', false, a -> String.join("-", a)),
private final boolean preserveSplit;
/** The function that converts segments into the String representation */
private final Function<String[], String> joiner;
/** A function to provide post-processing on the joined string */
private final UnaryOperator<String> postProcess;

/**
* Phrase case tags phrases of words like 'phrase case'. This conversion does not change the capitalization of any characters
* in the string. If specific capitalization is required use {@link String#toUpperCase()}, {@link String#toLowerCase()},
* * or the commons-text methods {@link WordUtils#capitalize(String)}, or {@link WordUtils#uncapitalize(String)} as required.
* Constructs a StringCase
* @param name the name of the case.
* @param splitter the splitter to determine when to split a string.
* @param preserveSplit the preserveSplit flag.
* @param joiner the joiner to assemble the String from the segments.
*/
PHRASE(Character::isWhitespace, false, a -> String.join(" ", a)),
public StringCase(final String name, final Predicate<Character> splitter, final boolean preserveSplit, final Function<String[], String> joiner) {
this(name, splitter, preserveSplit, joiner, UnaryOperator.identity());
}

/**
* Dot case tags phrases of words like 'phrase.case'. This conversion does not change the capitalization of any characters
* in the string. If specific capitalization is required use {@link String#toUpperCase()}, {@link String#toLowerCase()},
* * or the commons-text methods {@link WordUtils#capitalize(String)}, or {@link WordUtils#uncapitalize(String)} as required.
* Constructs a String case for the common cases where the delimiter is not preserved in the segments.
* @param name the name of the case.
* @param delimiter the delimter between segments.
*/
DOT(c -> c == '.', false, a -> String.join(".", a));

/** The segment value for a null string */
private static final String[] NULL_SEGMENT = new String[0];
/** The segment value for an empty string */
private static final String[] EMPTY_SEGMENT = {""};

/** test for split position character. */
private final Predicate<Character> splitter;
/** if {@code true} split position character will be preserved in following segment. */
private final boolean preserveSplit;
/** a function to joining the segments into this case type. */
private final Function<String[], String> joiner;
public StringCase(final String name, final char delimiter) {
this(name, c -> c == delimiter, false, simpleJoiner(delimiter));
}

/**
* Defines a String Case.
* @param splitter The predicate that determines when a new word in the cased string begins.
* @param preserveSplit if {@code true} the character that the splitter detected is preserved as the first character of the new word.
* @param joiner The function to merge a list of strings into the cased String.
* Constructs a StingCase.
* @param name the name of the string case.
* @param splitter the splitter to detect segments.
* @param preserveSplit the flag to preserve the splitter character.
* @param joiner the joiner to assemble a String from segments.
* @param postProcess the post-process applied to the segments after the splitter has created them.
*/
StringCase(final Predicate<Character> splitter, final boolean preserveSplit, final Function<String[], String> joiner) {
public StringCase(final String name, final Predicate<Character> splitter, final boolean preserveSplit, final Function<String[], String> joiner,
final UnaryOperator<String> postProcess) {
this.name = name;
this.splitter = splitter;
this.preserveSplit = preserveSplit;
this.joiner = joiner;
this.postProcess = postProcess;
}

/**
* Creates a cased string from a collection of segments.
* @param segments the segments to create the CasedString from.
* @return a CasedString
* A simple joiner that assembles a String from a collection of segments.
* Correctly handles the case where there are zero length segments.
* @param delimiter the delimiter to use between the segments.
* @return the assembled string.
*/
public static Function<String[], String> simpleJoiner(final char delimiter) {
return s -> String.join(String.valueOf(delimiter), (CharSequence[]) Arrays.stream(s).filter(Objects::nonNull).toArray(String[]::new));
}

@Override
public String toString() {
return this.name;
}

/**
* Assembles segments into a String.
* @param segments the segments to assemble.
* @return the complete String.
*/
public String assemble(final String[] segments) {
return segments.length == 0 ? null : this.joiner.apply(segments);
return this.joiner.apply(segments);
}

/**
* Returns an array of each of the segments in this CasedString. Segments are defined as the strings between
* the separators in the CasedString. For the CAMEL case the segments are determined by the presence of a capital letter.
* @return the array of Strings that are segments of the cased string.
* Parses a String into segments.
* @param string the string to parse
* @return the segments from the string.
*/
public String[] getSegments(final String string) {
if (string == null) {
return NULL_SEGMENT;
}
if (string.isEmpty()) {
} else if (string.isEmpty()) {
return EMPTY_SEGMENT;
}
List<String> lst = new ArrayList<>();
StringBuilder sb = new StringBuilder();
for (char c : string.toCharArray()) {
if (splitter.test(c)) {
if (!sb.isEmpty()) {
} else {
List<String> lst = new ArrayList<>();
StringBuilder sb = new StringBuilder();

for (char c : string.toCharArray()) {
if (this.splitter.test(c)) {
lst.add(sb.toString());
sb.setLength(0);
}
if (preserveSplit) {
if (this.preserveSplit) {
sb.append(c);
}
} else {
sb.append(c);
}
} else {
sb.append(c);
}

if (!sb.isEmpty()) {
lst.add(sb.toString());
}

return lst.stream().map(this.postProcess).filter(Objects::nonNull).toArray(String[]::new);
}
if (!sb.isEmpty()) {
lst.add(sb.toString());
}
return lst.toArray(new String[0]);
}
}

/**
* A representation of a cased string and the identified case of that string.
* @param stringCase The {@code StringCase} that the {@code string} argument is in.
* @param string The string.
*/
public CasedString(final StringCase stringCase, final String string) {
this.string = string == null ? null : stringCase.assemble(stringCase.getSegments(string.trim()));
this.stringCase = stringCase;
}

/**
* Returns an array of each of the segments in this CasedString. Segments are defined as the strings between
* the separators in the CasedString. For the CAMEL case the segments are determined by the presence of a capital letter.
* @return the array of Strings that are segments of the cased string.
*/
public String[] getSegments() {
return stringCase.getSegments(string);
}

/**
* Converts this cased string into a {@code String} of another format.
* The upper/lower case of the characters within the string are not modified.
* @param stringCase The format to convert to.
* @return the String current string represented in the new format.
*/
public String toCase(final StringCase stringCase) {
if (stringCase == this.stringCase) {
return string;
static {
CAMEL = new StringCase("CAMEL", Character::isUpperCase, true, CasedString.PASCAL_JOINER.andThen(WordUtils::uncapitalize),
x -> (String) StringUtils.defaultIfEmpty(x, (CharSequence) null));
PASCAL = new StringCase("PASCAL", Character::isUpperCase, true, CasedString.PASCAL_JOINER,
x -> (String) StringUtils.defaultIfEmpty(x, (CharSequence) null));
SNAKE = new StringCase("SNAKE", '_');
KEBAB = new StringCase("KEBAB", '-');
PHRASE = new StringCase("PHRASE", Character::isWhitespace, false, simpleJoiner(' '));
DOT = new StringCase("DOT", '.');
SLASH = new StringCase("SLASH", '/');
NULL_SEGMENT = new String[0];
EMPTY_SEGMENT = new String[]{""};
}
return string == null ? null : stringCase.joiner.apply(getSegments());
}

/**
* Returns the string representation provided in the constructor.
* @return the string representation.
*/
@Override
public String toString() {
return string;
}
}
Loading