CasedString.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
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 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> CAMEL_JOINER = strings -> {
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();
};
/**
* 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.
*/
public CasedString(final StringCase stringCase, final String string) {
this.segments = string == null ? CasedString.StringCase.NULL_SEGMENT : stringCase.getSegments(string.trim());
this.stringCase = stringCase;
}
/**
* 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, 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;
}
CasedString that = (CasedString) o;
return Objects.deepEquals(getSegments(), that.getSegments()) && Objects.equals(stringCase, that.stringCase);
}
@Override
public int hashCode() {
return Arrays.hashCode(getSegments());
}
/**
* The definition of a String case.
*/
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. */
static final String[] NULL_SEGMENT;
/** An empty segment marker. */
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.
*/
private final Predicate<Character> splitter;
/**
* If {@code true} the spliter character is preserved as part of the subsequent section otherwise,
* the spliter character is discarded.
*/
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;
/**
* 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.
*/
public StringCase(final String name, final Predicate<Character> splitter, final boolean preserveSplit, final Function<String[], String> joiner) {
this(name, splitter, preserveSplit, joiner, UnaryOperator.identity());
}
/**
* 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 delimiter between segments.
*/
public StringCase(final String name, final char delimiter) {
this(name, c -> c == delimiter, false, simpleJoiner(delimiter));
}
/**
* 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.
*/
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;
}
/**
* 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), 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 this.joiner.apply(segments);
}
/**
* 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;
} else if (string.isEmpty()) {
return EMPTY_SEGMENT;
} 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 (this.preserveSplit) {
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);
}
}
static {
CAMEL = new StringCase("CAMEL", Character::isUpperCase, true, CasedString.CAMEL_JOINER,
x -> (String) StringUtils.defaultIfEmpty(x, (CharSequence) null));
PASCAL = new StringCase("PASCAL", Character::isUpperCase, true, CasedString.CAMEL_JOINER.andThen(WordUtils::uncapitalize),
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[]{""};
}
}
}