001// Generated by delombok at Sat Feb 25 17:58:34 UTC 2023 002package io.cui.tools.string; 003 004import static io.cui.tools.string.MoreStrings.isEmpty; 005import static io.cui.tools.string.MoreStrings.nullToEmpty; 006import static java.lang.Integer.valueOf; 007import java.io.Serializable; 008import java.util.ArrayList; 009import java.util.List; 010import java.util.Map; 011import java.util.Map.Entry; 012import java.util.regex.Pattern; 013import io.cui.tools.collect.MapBuilder; 014 015/** 016 * This class provide functionality to 017 * transform long text to several html useful representation and encapsulate this as an object. 018 * It is implemented as an value-object keeping the calculated text. 019 * 020 * @author Eugen Fischer 021 */ 022public class TextSplitter implements Serializable { 023 /** 024 * serial Version UID 025 */ 026 private static final long serialVersionUID = 6594890288982910944L; 027 /** 028 * Der Browser muss über Sollbruchstellen die Möglichkeit bekommen lange 029 * Wortketten zu trennen/umzubrechen. Dafür gibt es zwei unsichtbare Zeichen, die in den HTML 030 * code eingebaut werden können: "&shy;" und "&#8203;". Der Unterschied zwischen beiden 031 * ist, dass das eine einfach ein Leerzeichen ohne breite ist, welches beim Umbruch keine Spuren 032 * hinterlässt, das andere fügt bei einem Umbruch einen Bindestrich hinzu. Eignet sich also zur 033 * Silbentrennung. 034 */ 035 private static final String ZERO_WIDTH_SPACE = ""; 036 private static final String TRADE_STR = "..."; 037 private static final int DEFAULT_FORCE_LENGTH_BREAK = 15; 038 private static final int DEFAULT_ABRIDGED_LENGTH = 20; 039 private static final Map<Pattern, String> REPLACEMENT_MAP = new MapBuilder<Pattern, String>().put(Pattern.compile("#"), "#" + ZERO_WIDTH_SPACE).put(Pattern.compile("\\+"), "+" + ZERO_WIDTH_SPACE).put(Pattern.compile("-"), "-" + ZERO_WIDTH_SPACE).put(Pattern.compile("_"), "_" + ZERO_WIDTH_SPACE).put(Pattern.compile("\\."), "." + ZERO_WIDTH_SPACE).put(Pattern.compile("\\?"), "?" + ZERO_WIDTH_SPACE).put(Pattern.compile("!"), "!" + ZERO_WIDTH_SPACE).put(Pattern.compile(":"), ":" + ZERO_WIDTH_SPACE).put(Pattern.compile(","), "," + ZERO_WIDTH_SPACE).put(Pattern.compile(";"), ";" + ZERO_WIDTH_SPACE).toImmutableMap(); 040 private final String source; 041 private final java.util.concurrent.atomic.AtomicReference<java.lang.Object> abridgedText = new java.util.concurrent.atomic.AtomicReference<java.lang.Object>(); 042 private boolean abridged = false; 043 private final java.util.concurrent.atomic.AtomicReference<java.lang.Object> textWithEnforcedLineBreaks = new java.util.concurrent.atomic.AtomicReference<java.lang.Object>(); 044 private Integer forceLengthBreak = null; 045 private Integer abridgedLength = null; 046 047 /** 048 * Construct TextSplitter. 049 * 050 * @param longString source text which will be processed 051 */ 052 public TextSplitter(final String longString) { 053 source = nullToEmpty(longString); 054 } 055 056 /** 057 * Alternative Constructor 058 * 059 * @param source target text 060 * @param forceLengthBreakCount count of characters when a text break will forced 061 * @param abridgedLengthCount count of characters 062 */ 063 public TextSplitter(final String source, final int forceLengthBreakCount, final int abridgedLengthCount) { 064 this.source = source; 065 forceLengthBreak = valueOf(forceLengthBreakCount); 066 abridgedLength = valueOf(abridgedLengthCount); 067 } 068 069 private int getForceLengthBreak() { 070 if (null == forceLengthBreak) { 071 return DEFAULT_FORCE_LENGTH_BREAK; 072 } 073 return forceLengthBreak; 074 } 075 076 private int getAbridgedLength() { 077 if (null == abridgedLength) { 078 return DEFAULT_ABRIDGED_LENGTH; 079 } 080 return abridgedLength; 081 } 082 083 private String initAbridged() { 084 var result = ""; 085 if (!isEmpty(source)) { 086 final var sourceSplitted = getSourceSplitted(); 087 if (sourceSplitted.size() == 1) { 088 result = abridgeComputerProducedText(); 089 } else { 090 result = abridgeHumanProducedText(sourceSplitted); 091 } 092 } 093 abridged = endsWith(result, TRADE_STR); 094 return result.trim(); 095 } 096 097 private static boolean endsWith(final String str, final String suffix) { 098 return str.trim().endsWith(suffix); 099 } 100 101 /** 102 * @return abridged text 103 */ 104 private String abridgeComputerProducedText() { 105 final var maxLength = getAbridgedLength() - (TRADE_STR.length() + 1); 106 if (source.length() > maxLength) { 107 return source.substring(0, maxLength) + " ..."; 108 } 109 return source; 110 } 111 112 /** 113 * @param sourceSplitted 114 * @return abridged text 115 */ 116 private String abridgeHumanProducedText(final List<String> sourceSplitted) { 117 final var maxLength = getAbridgedLength() - TRADE_STR.length(); 118 final var builder = new StringBuilder(); 119 var count = 0; 120 for (final String part : sourceSplitted) { 121 count = count + part.length(); 122 if (count >= maxLength) { 123 builder.append(TRADE_STR); 124 break; 125 } 126 builder.append(part).append(" "); 127 count = count + 1; 128 } 129 return builder.toString(); 130 } 131 132 private String initTextWithLineBreaks() { 133 var result = ""; 134 if (!isEmpty(source)) { 135 final var sourceSplitted = getSourceSplitted(); 136 if (sourceSplitted.size() == 1) { 137 result = forceLineBreakForComputerProducedText(source); 138 } else { 139 result = forceLineBreakForHumanProducedText(sourceSplitted); 140 } 141 } 142 return result.trim(); 143 } 144 145 private String forceLineBreakForHumanProducedText(final List<String> sourceSplitted) { 146 final var builder = new StringBuilder(); 147 for (final String text : sourceSplitted) { 148 builder.append(forceLineBreakForComputerProducedText(text)).append(" "); 149 } 150 return builder.toString(); 151 } 152 153 /** 154 * Try to separate text target on native text breaks. If this is not enough use brute-force on 155 * max allowed length. 156 * 157 * @param text target which will be analyzed 158 * @return 159 */ 160 private String forceLineBreakForComputerProducedText(final String text) { 161 // try to separate on native text breaks 162 var clean = text; 163 for (final Entry<Pattern, String> entry : REPLACEMENT_MAP.entrySet()) { 164 final var matcher = entry.getKey().matcher(clean); 165 clean = matcher.replaceAll(entry.getValue()); 166 } 167 final var splittedByZeroWidthSpace = getSplittedByZeroWidthSpace(clean); 168 final List<String> lengthTrimed = new ArrayList<>(); 169 for (final String item : splittedByZeroWidthSpace) { 170 lengthTrimed.add(bruteForceSplit(item)); 171 } 172 return Joiner.on(ZERO_WIDTH_SPACE).join(lengthTrimed); 173 } 174 175 /** 176 * Verify if very long text still exists and execute brute-force dissipation 177 * 178 * @param text target 179 * @return fragmented text if length doesn't fit to force length break 180 */ 181 private String bruteForceSplit(final String text) { 182 final var maxLength = getForceLengthBreak(); 183 if (!isEmpty(text)) { 184 final var builder = new StringBuilder(); 185 var tmp = text; 186 while (tmp.length() > maxLength) { 187 builder.append(tmp, 0, maxLength).append(ZERO_WIDTH_SPACE); 188 tmp = tmp.substring(maxLength); 189 } 190 if (tmp.length() > 0) { 191 builder.append(tmp); 192 } 193 return builder.toString(); 194 } 195 return text; 196 } 197 198 private static List<String> getSplittedByZeroWidthSpace(final String value) { 199 return Splitter.on(ZERO_WIDTH_SPACE).splitToList(value); 200 } 201 202 private List<String> getSourceSplitted() { 203 return Splitter.on(" ").splitToList(source); 204 } 205 206 @java.lang.Override 207 @java.lang.SuppressWarnings("all") 208 @lombok.Generated 209 public boolean equals(final java.lang.Object o) { 210 if (o == this) return true; 211 if (!(o instanceof TextSplitter)) return false; 212 final TextSplitter other = (TextSplitter) o; 213 if (!other.canEqual((java.lang.Object) this)) return false; 214 if (this.getForceLengthBreak() != other.getForceLengthBreak()) return false; 215 if (this.getAbridgedLength() != other.getAbridgedLength()) return false; 216 final java.lang.Object this$source = this.source; 217 final java.lang.Object other$source = other.source; 218 if (this$source == null ? other$source != null : !this$source.equals(other$source)) return false; 219 return true; 220 } 221 222 @java.lang.SuppressWarnings("all") 223 @lombok.Generated 224 protected boolean canEqual(final java.lang.Object other) { 225 return other instanceof TextSplitter; 226 } 227 228 @java.lang.Override 229 @java.lang.SuppressWarnings("all") 230 @lombok.Generated 231 public int hashCode() { 232 final int PRIME = 59; 233 int result = 1; 234 result = result * PRIME + this.getForceLengthBreak(); 235 result = result * PRIME + this.getAbridgedLength(); 236 final java.lang.Object $source = this.source; 237 result = result * PRIME + ($source == null ? 43 : $source.hashCode()); 238 return result; 239 } 240 241 @java.lang.Override 242 @java.lang.SuppressWarnings("all") 243 @lombok.Generated 244 public java.lang.String toString() { 245 return "TextSplitter(source=" + this.source + ", forceLengthBreak=" + this.getForceLengthBreak() + ", abridgedLength=" + this.getAbridgedLength() + ")"; 246 } 247 248 @java.lang.SuppressWarnings({"all", "unchecked"}) 249 @lombok.Generated 250 public String getAbridgedText() { 251 java.lang.Object value = this.abridgedText.get(); 252 if (value == null) { 253 synchronized (this.abridgedText) { 254 value = this.abridgedText.get(); 255 if (value == null) { 256 final String actualValue = initAbridged(); 257 value = actualValue == null ? this.abridgedText : actualValue; 258 this.abridgedText.set(value); 259 } 260 } 261 } 262 return (String) (value == this.abridgedText ? null : value); 263 } 264 265 @java.lang.SuppressWarnings("all") 266 @lombok.Generated 267 public boolean isAbridged() { 268 return this.abridged; 269 } 270 271 @java.lang.SuppressWarnings({"all", "unchecked"}) 272 @lombok.Generated 273 public String getTextWithEnforcedLineBreaks() { 274 java.lang.Object value = this.textWithEnforcedLineBreaks.get(); 275 if (value == null) { 276 synchronized (this.textWithEnforcedLineBreaks) { 277 value = this.textWithEnforcedLineBreaks.get(); 278 if (value == null) { 279 final String actualValue = initTextWithLineBreaks(); 280 value = actualValue == null ? this.textWithEnforcedLineBreaks : actualValue; 281 this.textWithEnforcedLineBreaks.set(value); 282 } 283 } 284 } 285 return (String) (value == this.textWithEnforcedLineBreaks ? null : value); 286 } 287 288 @java.lang.SuppressWarnings("all") 289 @lombok.Generated 290 public void setForceLengthBreak(final Integer forceLengthBreak) { 291 this.forceLengthBreak = forceLengthBreak; 292 } 293 294 @java.lang.SuppressWarnings("all") 295 @lombok.Generated 296 public void setAbridgedLength(final Integer abridgedLength) { 297 this.abridgedLength = abridgedLength; 298 } 299}