edu.hm.hafner.grading.TruncatedString Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of autograding-model Show documentation
Show all versions of autograding-model Show documentation
This module autogrades Java projects based on a configurable set of metrics.
The newest version!
package edu.hm.hafner.grading;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
/**
* Utility wrapper that silently truncates output with a message at a certain size.
*
* The GitHub Checks API has a size limit on text fields. Because it also accepts markdown, it is not trivial to
* truncate to the required length as this could lead to unterminated syntax. The use of this class allows for adding
* chunks of complete markdown until an overflow is detected, at which point a message will be added and all future
* additions will be silently discarded.
*
*
* @author Bill Collins
*/
public final class TruncatedString {
private final List chunks;
private final String truncationText;
private final boolean truncateStart;
private final boolean chunkOnNewlines;
private TruncatedString(final List chunks, final String truncationText,
final boolean truncateStart, final boolean chunkOnNewlines) {
this.chunks = Collections.unmodifiableList(Objects.requireNonNull(chunks));
this.truncationText = Objects.requireNonNull(truncationText);
this.truncateStart = truncateStart;
this.chunkOnNewlines = chunkOnNewlines;
}
/**
* Wrap the provided string as a {@link TruncatedString}.
*
* @param string
* String to wrap as a {@link TruncatedString}
*
* @return a {@link TruncatedString} wrapping the provided input
*/
static TruncatedString fromString(final String string) {
return new TruncatedStringBuilder().setChunkOnNewlines().addText(string).build();
}
/**
* Builds the string without truncation.
*
* @return A string comprising the joined chunks.
*/
@Override
public String toString() {
return String.join("", chunks);
}
private List getChunks() {
if (chunkOnNewlines) {
return Arrays.asList(String.join("", chunks).split("(?<=\r?\n)"));
}
return new ArrayList<>(chunks);
}
/**
* Builds the string such that it does not exceed maxSize in bytes, including the truncation string.
*
* @param maxSize
* the maximum size of the resultant string.
*
* @return A string comprising as many of the joined chunks that will fit in the given size, plus the truncation
* string if truncation was necessary.
*/
public String buildByBytes(final int maxSize) {
return build(maxSize, false);
}
/**
* Builds the string such that it does not exceed maxSize in chars, including the truncation string.
*
* @param maxSize
* the maximum size of the resultant string.
*
* @return A string comprising as many of the joined chunks that will fit in the given size, plus the truncation
* string if truncation was necessary.
*/
public String buildByChars(final int maxSize) {
return build(maxSize, true);
}
private String build(final int maxSize, final boolean chunkOnChars) {
List parts = getChunks();
if (truncateStart) {
Collections.reverse(parts);
}
List truncatedParts = parts.stream().collect(new Joiner(truncationText, maxSize, chunkOnChars));
if (truncateStart) {
Collections.reverse(truncatedParts);
}
return String.join("", truncatedParts);
}
/**
* TruncatedStringBuilder for {@link TruncatedString}.
*/
public static class TruncatedStringBuilder {
private String truncationText = "Output truncated.";
private boolean truncateStart;
private boolean chunkOnNewlines;
private final List chunks = new ArrayList<>();
/**
* Builds the {@link TruncatedString}.
*
* @return the build {@link TruncatedString}.
*/
public TruncatedString build() {
return new TruncatedString(chunks, truncationText, truncateStart, chunkOnNewlines);
}
/**
* Adds a chunk of text to the builder.
*
* @param text
* the chunk of text to append to this builder
*
* @return this builder
*/
@CanIgnoreReturnValue
public TruncatedStringBuilder addText(final String text) {
this.chunks.add(text);
return this;
}
/**
* Adds a chunk of text to the builder, if the specified guard is {@code true}.
*
* @param text
* the chunk of text to append to this builder
* @param guard
* determines if the text should be added
*
* @return this builder
*/
@CanIgnoreReturnValue
public TruncatedStringBuilder addTextIf(final String text, final boolean guard) {
if (guard) {
this.chunks.add(text);
}
return this;
}
/**
* Adds a newline to the builder.
*
* @return this builder
*/
@CanIgnoreReturnValue
public TruncatedStringBuilder addNewline() {
this.chunks.add("\n");
return this;
}
/**
* Sets the truncation text.
*
* @param truncationText
* the text to append on overflow
*
* @return this builder
*/
@CanIgnoreReturnValue
@SuppressWarnings({"HiddenField", "ParameterHidesMemberVariable"})
public TruncatedStringBuilder withTruncationText(final String truncationText) {
this.truncationText = truncationText;
return this;
}
/**
* Sets truncator to remove excess text from the start, rather than the end.
*
* @return this builder
*/
@CanIgnoreReturnValue
public TruncatedStringBuilder setTruncateStart() {
this.truncateStart = true;
return this;
}
/**
* Sets truncator to chunk on newlines rather than the chunks.
*
* @return this builder
*/
@CanIgnoreReturnValue
public TruncatedStringBuilder setChunkOnNewlines() {
this.chunkOnNewlines = true;
return this;
}
}
private static class Joiner implements Collector> {
private final int maxLength;
private final String truncationText;
private final boolean chunkOnChars;
Joiner(final String truncationText, final int maxLength, final boolean chunkOnChars) {
this.truncationText = truncationText;
this.maxLength = maxLength;
this.chunkOnChars = chunkOnChars;
if (maxLength < getLength(truncationText)) {
throw new IllegalArgumentException("Maximum length is less than truncation text.");
}
}
private int getLength(final String text) {
return chunkOnChars ? text.length() : text.getBytes(StandardCharsets.UTF_8).length;
}
@Override
public Supplier supplier() {
return Accumulator::new;
}
@Override
public BiConsumer accumulator() {
return Accumulator::add;
}
@Override
public BinaryOperator combiner() {
return Accumulator::combine;
}
@Override
public Function> finisher() {
return Accumulator::truncate;
}
@Override
public Set characteristics() {
return Collections.emptySet();
}
private class Accumulator {
private final List chunks = new ArrayList<>();
private int length;
private boolean truncated;
@CanIgnoreReturnValue
Accumulator combine(final Accumulator other) {
other.chunks.forEach(this::add);
return this;
}
void add(final String chunk) {
if (truncated) {
return;
}
if (length + getLength(chunk) > maxLength) {
truncated = true;
return;
}
chunks.add(chunk);
length += getLength(chunk);
}
List truncate() {
if (truncated) {
if (length + getLength(truncationText) > maxLength) {
chunks.remove(chunks.size() - 1);
}
chunks.add(truncationText);
}
return chunks;
}
}
}
}