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: "­" und "​". 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}