StringHolderSequence.java
/*
* stringhold
*
* Copyright 2022-2024 Christian Kohlschütter
*
* Licensed 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 com.kohlschutter.stringhold;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
/**
* An {@link Appendable} sequence of strings or {@link StringHolder}s.
*
* @author Christian Kohlschütter
*/
@SuppressWarnings("PMD.CyclomaticComplexity")
public class StringHolderSequence extends AbstractStringHolder implements Appendable {
List<CharSequence> sequence;
private boolean immutable = false;
private boolean cannotUseCache = false;
private Integer cachedHashCode = null;
private Integer cachedLength = null;
/**
* Constructs a new, empty {@link StringHolderSequence}.
*/
StringHolderSequence() {
this(10);
}
/**
* Constructs a new, empty {@link StringHolderSequence}.
*
* @param estimatedNumberOfAppends Estimated number of calls to {@link #append(Object)}, etc.
*/
StringHolderSequence(int estimatedNumberOfAppends) {
super(0);
sequence = new ArrayList<>(estimatedNumberOfAppends);
}
/**
* Checks if the to-be-appended {@link StringHolder} should be added as a {@link String} (with
* conversion via {@link StringHolder#toString()}) instead of adding it directly.
*
* This is false by default for all objects. Subclasses may override this selectively. If all
* objects should be converted to strings, use {@link StringOnlySequence}.
*
* @param sh The {@link StringHolder}.
* @return {@code true} if it should be appended as a string.
* @see StringOnlySequence
*/
protected boolean needsStringConversion(StringHolder sh) {
return false;
}
private void checkMutable() {
if (immutable) {
throw new IllegalStateException(
"Cannot append -- instance is marked as effectively-immutable");
}
}
@Override
public StringHolderSequence append(CharSequence csq, int start, int end) {
if (end == start) {
return this;
}
if (csq instanceof String) {
append(((String) csq).substring(start, end));
} else if (csq instanceof StringBuilder) {
append(((StringBuilder) csq).substring(start, end));
} else if (csq instanceof StringBuffer) {
append(((StringBuffer) csq).substring(start, end));
} else {
append(csq.subSequence(start, end));
}
return this;
}
@Override
public StringHolderSequence append(char c) {
addSequence(String.valueOf(c));
return this;
}
/**
* Appends the given {@link String}, unless it is empty.
*
* @param s The string.
* @return This instance.
*/
public StringHolderSequence append(String s) {
if (!s.isEmpty()) {
addSequence(CommonStrings.lookupIfPossible(s));
}
return this;
}
private void addSequence(String s) {
int len = s.length();
if (len == 0) {
return;
}
checkMutable();
uncache();
sequence.add(s);
resizeBy(len, len);
}
/**
* Appends the given {@link StringHolder}, unless it is known to be empty.
*
* As a side-effect, the scope of the given {@link StringHolder} is updated with the scope of this
* instance.
*
* @param s The string.
* @return This instance.
*/
public StringHolderSequence append(StringHolder s) {
if (s.isKnownEmpty()) {
return this;
} else if (s.isString() || needsStringConversion(s)) {
checkMutable();
if (s.getScope() == getScope()) { // NOPMD
// don't double count
s.updateScope(StringHolderScope.NONE);
}
addSequence(s.toString());
return this;
}
if (!s.isEffectivelyImmutable()) {
cannotUseCache = true;
}
resizeBy(s.getMinimumLength(), s.getExpectedLength());
uncache();
sequence.add(s);
return this;
}
/**
* Appends the given {@link CharSequence}, unless it is known to be empty.
*
* @param s The string.
*/
@Override
public StringHolderSequence append(CharSequence s) {
if (s instanceof StringHolder) {
return append((StringHolder) s);
}
if (!CharSequenceReleaseShim.isEmpty(s)) {
addSequence(CommonStrings.lookupIfPossible(String.valueOf(s)));
}
return this;
}
/**
* Appends all given objects.
*
* @param objects The objects to append.
* @return This instance.
*/
public StringHolderSequence appendAll(Object... objects) {
Objects.requireNonNull(objects);
for (Object obj : objects) {
append(obj);
}
return this;
}
/**
* Appends all given objects.
*
* @param objects The objects to append.
* @return This instance.
*/
public StringHolderSequence appendAll(Iterable<Object> objects) {
Objects.requireNonNull(objects);
for (Object obj : objects) {
append(obj);
}
return this;
}
/**
* Appends the given object, unless it is known to be empty.
*
* The object
*
* @param s The string.
* @return This instance.
*/
public StringHolderSequence append(Object s) {
if (s instanceof StringHolder) {
return append((StringHolder) s);
} else if (s instanceof String) {
return append((String) s);
} else if (s instanceof CharSequence) {
return append((CharSequence) s);
} else {
return append(String.valueOf(s));
}
}
@Override
protected int appendToAndReturnLengthDefaultImpl(Appendable out) throws IOException {
int len = 0;
for (Object obj : sequence) {
if (obj instanceof StringHolder) {
StringHolder holder = (StringHolder) obj;
if (holder.isKnownEmpty()) {
continue;
}
len += holder.appendToAndReturnLength(out);
} else {
String s = (String) obj;
len += s.length();
out.append(s);
}
}
return len;
}
@Override
protected int appendToAndReturnLengthImpl(StringBuilder out) {
out.ensureCapacity(out.length() + getMinimumLength());
int len = 0;
for (Object obj : sequence) {
if (obj instanceof StringHolder) {
StringHolder holder = (StringHolder) obj;
if (holder.isKnownEmpty()) {
continue;
}
len += holder.appendToAndReturnLength(out);
} else {
String s = (String) obj;
len += s.length();
out.append(s);
}
}
return len;
}
@Override
protected int appendToAndReturnLengthImpl(StringBuffer out) {
out.ensureCapacity(out.length() + getMinimumLength());
int len = 0;
for (Object obj : sequence) {
if (obj instanceof StringHolder) {
StringHolder holder = (StringHolder) obj;
if (holder.isKnownEmpty()) {
continue;
}
len += holder.appendToAndReturnLength(out);
} else {
String s = (String) obj;
len += s.length();
out.append(s);
}
}
return len;
}
/**
* Appends to a list that only holds Strings or StringSuppliers, excluding
* {@link StringHolderSequence}s, whose contents are flattened to the list.
*
* @param flatList The list to append to.
* @return The minimum for the estimated length.
*/
final int appendToFlatList(List<Object> flatList) {
int len = 0;
for (Object obj : sequence) {
if (obj instanceof StringHolder) {
StringHolder holder = (StringHolder) obj;
if (holder.isKnownEmpty()) {
continue;
}
if (obj instanceof StringHolderSequence) {
len += ((StringHolderSequence) obj).appendToFlatList(flatList);
} else {
len += holder.getMinimumLength();
flatList.add(obj);
}
} else {
String s = (String) obj;
len += s.length();
flatList.add(s);
}
}
return len;
}
@Override
protected String getString() {
StringBuilder sb = new StringBuilder(Math.max(16, getExpectedLength()));
int len = appendToAndReturnLength(sb);
uncache();
final String s;
sequence.clear();
if (len == 0) {
s = "";
} else {
s = sb.toString();
sequence.add(s);
}
return s;
}
@Override
protected Reader newReader() {
if (sequence.isEmpty()) {
return new StringReader("");
}
final List<Object> flatList = new ArrayList<>(sequence.size());
int len = appendToFlatList(flatList);
if (len == 0) {
return new StringReader("");
}
return new StringSequenceReader(flatList.iterator());
}
private static final class StringSequenceReader extends Reader {
private boolean closed = false;
private String currentString = null;
private int currentPos;
private final Iterator<Object> flatObjectIterator;
private StringSequenceReader(Iterator<Object> flatObjectIterator) {
super();
this.flatObjectIterator = flatObjectIterator;
}
private void ensureOpen() throws IOException {
if (closed) {
throw new IOException("Stream closed");
}
}
@Override
public boolean ready() throws IOException {
ensureOpen();
return true;
}
@Override
public void close() throws IOException {
closed = true;
}
int ensureObject() {
if (currentString != null) {
int len = currentString.length();
if (currentPos >= len) {
currentString = null;
return ensureObject();
} else {
return len;
}
} else {
while (flatObjectIterator.hasNext()) {
currentString = flatObjectIterator.next().toString();
currentPos = 0;
int len = currentString.length();
if (len > 0) {
return len;
}
}
currentString = null;
return 0;
}
}
@Override
public int read(char[] cbuf, int off, int len) throws IOException {
int currentLen = ensureObject();
if (currentLen == 0) {
return -1;
}
len = Math.min(currentLen - currentPos, Math.min(cbuf.length - off, len));
currentString.getChars(currentPos, currentPos + len, cbuf, off);
currentPos += len;
return len;
}
@Override
public int read() throws IOException {
int currentLen = ensureObject();
if (currentLen == 0) {
return -1;
}
return currentString.charAt(currentPos++);
}
}
/**
* Returns the number of appends (calls to {@link #append(Object)}, etc.) made to this instance so
* far, minus the number of calls that were decided avoidable (e.g., zero-length appends).
*
* @return The number of appends.
*/
public int numberOfAppends() {
return sequence.size();
}
/**
* Returns a simplified version of the contents of this sequence, if possible.
*
* <ol>
* <li>If the content is a string already, the string is returned.</li>
* <li>If there's no element stored, an empty string is returned.</li>
* <li>If there is only a single element stored, its content is returned as if
* {@link #asContent()} was called on that element.</li>
* </ol>
*/
@Override
public Object asContent() {
if (isString()) {
return toString();
}
if (sequence.isEmpty()) {
return "";
} else if (sequence.size() == 1) {
Object obj = sequence.get(0);
if (obj instanceof String) {
return obj;
}
StringHolder sc = (StringHolder) obj;
return sc.asContent();
} else {
return this;
}
}
@Override
@SuppressWarnings("PMD.CognitiveComplexity")
public char charAt(int index) {
int offset = 0;
if (index < 0) {
throw new IndexOutOfBoundsException();
}
for (Object obj : sequence) {
final int len;
if (obj instanceof StringHolder) {
StringHolder sh = (StringHolder) obj;
if (sh.isEmpty()) {
len = 0;
} else {
if (index == offset) {
return sh.charAt(index - offset);
}
len = sh.length();
if (index < offset + len) {
return sh.charAt(index - offset);
}
}
} else {
String s = (String) obj;
len = s.length();
if (index < offset + len) {
return s.charAt(index - offset);
}
}
offset += len;
}
throw new IndexOutOfBoundsException();
}
@Override
public int hashCode() { // NOPMD.OverrideBothEqualsAndHashcode
if (cachedHashCode == null || cannotUseCache) {
cachedHashCode = updateHashCode(0);
}
return cachedHashCode;
}
@Override
protected int updateHashCode(int h) {
if (isString()) {
return super.updateHashCode(h);
}
for (CharSequence obj : sequence) {
h = updateHashCode(obj, h);
}
return h;
}
private static int updateHashCode(Object obj, int h) {
if (obj instanceof AbstractStringHolder) {
AbstractStringHolder sh = (AbstractStringHolder) obj;
if (!sh.isKnownEmpty()) {
h = sh.updateHashCode(h);
}
} else {
CharSequence s = (CharSequence) obj;
if (h == 0) {
return s.hashCode();
}
int len = s.length();
for (int i = 0; i < len; i++) {
h = 31 * h + s.charAt(i);
}
}
return h;
}
@Override
protected boolean checkEquals(StringHolder sh) {
if (sh instanceof StringHolderSequence) {
StringHolderSequence shs = (StringHolderSequence) sh;
if (sequence.equals(shs.sequence)) {
return true;
}
}
return super.checkEquals(sh);
}
@Override
protected void uncache() {
super.uncache();
cachedHashCode = null;
cachedLength = null;
}
@Override
protected int computeLength() {
Integer length = cachedLength;
if (length == null || cannotUseCache) {
int len = 0;
for (CharSequence obj : sequence) {
len += obj.length();
}
cachedLength = length = len;
}
return length;
}
@Override
public boolean isEffectivelyImmutable() {
return immutable;
}
@Override
public void markEffectivelyImmutable() {
if (immutable) {
return;
}
for (Object seq : sequence) {
if (!(seq instanceof StringHolder)) {
continue;
}
StringHolder sh = (StringHolder) seq;
sh.markEffectivelyImmutable();
}
immutable = true;
}
@Override
public boolean isCacheable() {
for (Object seq : sequence) {
if (!(seq instanceof StringHolder)) {
continue;
}
StringHolder sh = (StringHolder) seq;
if (!sh.isCacheable()) {
return false;
}
}
return true;
}
@Override
public StringHolderSequence clone() {
StringHolderSequence clone = (StringHolderSequence) super.clone();
List<CharSequence> seq = new ArrayList<>();
for (int i = 0, n = sequence.size(); i < n; i++) {
CharSequence cs = sequence.get(i);
if (cs instanceof StringHolder) {
cs = ((StringHolder) cs).clone();
}
seq.add(cs);
}
clone.sequence = seq;
return clone;
}
}