001// --------------------------------------------------------------------------------
002// Copyright 2002-2026 Echo Three, LLC
003//
004// Licensed under the Apache License, Version 2.0 (the "License");
005// you may not use this file except in compliance with the License.
006// You may obtain a copy of the License at
007//
008//     http://www.apache.org/licenses/LICENSE-2.0
009//
010// Unless required by applicable law or agreed to in writing, software
011// distributed under the License is distributed on an "AS IS" BASIS,
012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013// See the License for the specific language governing permissions and
014// limitations under the License.
015// --------------------------------------------------------------------------------
016
017package com.echothree.util.common.string;
018
019import com.echothree.model.control.core.common.MimeTypes;
020import com.google.common.html.HtmlEscapers;
021import com.ibm.icu.text.Transliterator;
022import java.util.Arrays;
023import java.util.Iterator;
024import java.util.Locale;
025
026public class StringUtils {
027    
028    private StringUtils() {
029        super();
030    }
031    
032    private static class StringUtilsHolder {
033        static StringUtils instance = new StringUtils();
034    }
035    
036    public static StringUtils getInstance() {
037        return StringUtilsHolder.instance;
038    }
039
040    private final Transliterator transliterator = Transliterator.getInstance("Any-Latin; Latin-ASCII; NFKD; [:Nonspacing Mark:] Remove; NFKC", Transliterator.FORWARD);
041
042    public String toAscii7(String text) {
043        return transliterator.transliterate(text);
044    }
045
046    public String getIndent(int indentAmount, int indentCount) {
047        return String.format("%" + indentAmount * indentCount + "s", "");
048    }
049
050    /** Take a string and trim all leading and trailing spaces from it.
051     * If the resulting length is zero, return null.
052     */
053    public String trimToNull(String string) {
054        String result = null;
055
056        if(string != null) {
057            result = string.trim();
058
059            if(result.isEmpty()) {
060                result = null;
061            }
062        }
063
064        return result;
065    }
066
067    /** 
068     * null-safe String comparison.
069     */
070    public int nullSafeCompareTo(final String s1, final String s2) {
071        if (s1 == null ^ s2 == null) {
072            return (s1 == null) ? -1 : 1;
073        }
074
075        if (s1 == null) {
076            return 0;
077        }
078
079        return s1.compareTo(s2);
080    }
081
082    public String left(String str, int len) {
083        return str == null ? null : str.length() <= len ? str : str.substring(0, len);
084    }
085
086    public String right(String str, int len) {
087        return str == null ? null : str.length() <= len ? str : str.substring(str.length() - len);
088    }
089
090    public String padLeftZeros(final String input, final int length) {
091        return pad(input, length, '0', true);
092    }
093
094    public String padLeftSpaces(final String input, final int length) {
095        return pad(input, length, ' ', true);
096    }
097
098    public String padRightSpaces(final String input, final int length) {
099        return pad(input, length, ' ', false);
100    }
101
102    private String pad(final String input, final int length, final char padChar, final boolean padLeft) {
103        var value = input == null ? "" : input;
104        var paddingLength = length - value.length();
105
106        if(paddingLength < 0) {
107            throw new IllegalArgumentException("input length may not be greater than length");
108        }
109
110        var padding = repeatingStringFromChar(padChar, paddingLength);
111        
112        return padLeft ? padding + value : value + padding;
113    }
114
115    public String cleanStringToName(final String s) {
116        var a = s.toCharArray();
117        var l = a.length;
118        var r = new StringBuilder(l);
119
120        for(char c : a) {
121            var t = c;
122
123            if(!Character.isLetterOrDigit(t) && t != '-' && t != '_') {
124                t = '_';
125            }
126
127            r.append(t);
128        }
129        
130        return r.toString();
131    }
132
133    public int countSpaces(final String str) {
134        var count = 0;
135
136        for(int codePoint : codePoints(str)) {
137            if(Character.isSpaceChar(codePoint)) {
138                count++;
139            }
140        }
141
142        return count;
143    }
144    
145    // Look for the first Space character in a String, and construct a new String that contains it.
146    public String getSpace(final String str) {
147        String result = null;
148        
149        for(int codePoint : codePoints(str)) {
150            if(Character.isSpaceChar(codePoint)) {
151                result = new String(new int[] { codePoint }, 0, 1);
152                break;
153            }
154        }
155        
156        return result;
157    }
158
159    public String cleanString(final String str, final boolean includeDigits, final boolean includeLetters, final boolean includeSpaces) {
160        var sb = new StringBuilder();
161
162        for(int codePoint : codePoints(str)) {
163            var appended = false;
164
165            if(includeDigits && includeLetters) {
166                if(Character.isLetterOrDigit(codePoint)) {
167                    sb.appendCodePoint(codePoint);
168                    appended = true;
169                }
170            } else {
171                if(includeDigits && Character.isDigit(codePoint)) {
172                    sb.appendCodePoint(codePoint);
173                    appended = true;
174                }
175
176                if(!appended && includeLetters && Character.isLetter(codePoint)) {
177                    sb.appendCodePoint(codePoint);
178                    appended = true;
179                }
180            }
181
182            if(!appended && includeSpaces && Character.isSpaceChar(codePoint)) {
183                sb.appendCodePoint(codePoint);
184            }
185        }
186
187        return sb.toString();
188    }
189
190    public String cleanStringToLettersOrDigits(final String str) {
191        return cleanString(str, true, true, false);
192    }
193    
194    public String upperCaseFirstCharacter(final String str) {
195        return str == null ? null : str.isEmpty() ? str : str.substring(0, 1).toUpperCase(Locale.getDefault()) + str.substring(1);
196    }
197
198    public String lowerCaseFirstCharacter(final String str) {
199        return str == null ? null : str.isEmpty() ? str : str.substring(0, 1).toLowerCase(Locale.getDefault()) + str.substring(1);
200    }
201
202    public String normalizeCase(final String str) {
203        return str == null ? null : str.isEmpty() ? str : str.substring(0, 1).toUpperCase(Locale.getDefault()) + str.substring(1).toLowerCase(Locale.getDefault());
204    }
205
206    // Similar to http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=5003547
207    public Iterable<Integer> codePoints(final String str) {
208        return () -> new Iterator<>() {
209            int nextIndex = 0;
210            final int len = str.length();
211
212            @Override
213            public boolean hasNext() {
214                return nextIndex < len;
215            }
216
217            @Override
218            public Integer next() {
219                var result = str.codePointAt(nextIndex);
220                nextIndex += Character.charCount(result);
221                return result;
222            }
223
224            @Override
225            public void remove() {
226                throw new UnsupportedOperationException();
227            }
228        };
229    }
230    
231    public String convertToHtml(final String value, final String mimeTypeName) {
232        if(mimeTypeName.equals(MimeTypes.TEXT_HTML.mimeTypeName())) {
233            return value;
234        } else if(mimeTypeName.equals(MimeTypes.TEXT_PLAIN.mimeTypeName())) {
235            return "<pre>" + HtmlEscapers.htmlEscaper().escape(value) + "</pre>";
236        } else if(mimeTypeName.equals(MimeTypes.TEXT_X_MARKUP.mimeTypeName())) {
237            return MarkupUtils.getInstance().filter(value);
238        } else if(mimeTypeName.equals(MimeTypes.TEXT_X_TEXTILE.mimeTypeName())) {
239            return TextileUtils.getInstance().filter(value);
240        } else {
241            return "Unsupported mimeTypeName.";
242        }
243    }
244
245    public String mask(final String value, final char mask, final int unmasked) {
246        var len = value.length();
247        String result;
248
249        // Make sure there's enough String to mask anything at all
250        if(len - unmasked > 0) {
251            char[] maskChars = new char[len - unmasked];
252
253            Arrays.fill(maskChars, mask);
254
255            result = String.valueOf(maskChars) + value.substring(len - unmasked, len);
256        } else {
257            result = value;
258        }
259
260        return result;
261    }
262
263    public String mask(final String value, final char mask) {
264        char[] maskChars = new char[value.length()];
265
266        Arrays.fill(maskChars, mask);
267
268        return new String(maskChars);
269    }
270
271    public String repeatingStringFromChar(final char val, final int length) {
272        var data = new char[length];
273
274        Arrays.fill(data, val);
275
276        return String.valueOf(data);
277    }
278
279    public boolean isAllUpperCase(String str) {
280        return str != null && str.equals(str.toUpperCase(Locale.getDefault()));
281    }
282
283    public boolean isAllLowerCase(String str) {
284        return str != null && str.equals(str.toLowerCase(Locale.getDefault()));
285    }
286
287    public boolean isAllSameCase(String str) {
288        return str != null && (isAllUpperCase(str) || isAllLowerCase(str));
289    }
290
291}