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}