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.List;
import java.util.Locale;
import java.util.function.Function;
import java.util.function.Predicate;

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. */
    private final StringCase stringCase;

    /**
     * A method to join camel string fragments together.
     */
    private static final Function<String[], String> CAMEL_JOINER = a -> {
        StringBuilder sb = new StringBuilder(a[0].toLowerCase(Locale.ROOT));

        for (int i = 1; i < a.length; i++) {
            sb.append(WordUtils.capitalize(a[i].toLowerCase(Locale.ROOT)));
        }
        return sb.toString();
    };

    /**
     * An enumeration of supported string cases.  These cases tag strings as having a specific format.
     */
    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.
         */
        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)),
        /**
         * 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.
         */
        KEBAB(c -> c == '-', false, a -> String.join("-", a)),

        /**
         * 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.
         */
        PHRASE(Character::isWhitespace, false, a -> String.join(" ", a)),

        /**
         * 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.
         */
        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;

        /**
         * 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.
         */
        StringCase(final Predicate<Character> splitter, final boolean preserveSplit, final Function<String[], String> joiner) {
            this.splitter = splitter;
            this.preserveSplit = preserveSplit;
            this.joiner = joiner;
        }

        /**
         * Creates a cased string from a collection of segments.
         * @param segments the segments to create the CasedString from.
         * @return a CasedString
         */
        public String assemble(final String[] segments) {
            return segments.length == 0 ? null : 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.
         */
        public String[] getSegments(final String string) {
            if (string == null) {
                return NULL_SEGMENT;
            }
            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()) {
                        lst.add(sb.toString());
                        sb.setLength(0);
                    }
                    if (preserveSplit) {
                        sb.append(c);
                    }
                } else {
                    sb.append(c);
                }
            }
            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;
        }
        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;
    }
}