diff --git a/pom.xml b/pom.xml index 8056e07d..4c0618ec 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ 8 - 1.2.1 + 1.3.0 -SNAPSHOT diff --git a/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java b/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java index addd96bf..e6100fda 100644 --- a/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java +++ b/src/main/java/io/jenkins/plugins/checks/api/ChecksOutput.java @@ -1,11 +1,13 @@ package io.jenkins.plugins.checks.api; +import org.apache.commons.lang3.StringUtils; + import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; -import static java.util.Objects.*; +import static java.util.Objects.requireNonNull; /** * An output of a check. The output usually contains the most useful information like summary, description, @@ -13,12 +15,12 @@ */ public class ChecksOutput { private final String title; - private final String summary; - private final String text; + private final TruncatedString summary; + private final TruncatedString text; private final List annotations; private final List images; - private ChecksOutput(final String title, final String summary, final String text, + private ChecksOutput(final String title, final TruncatedString summary, final TruncatedString text, final List annotations, final List images) { this.title = title; this.summary = summary; @@ -34,7 +36,9 @@ private ChecksOutput(final String title, final String summary, final String text * the source to copy from */ public ChecksOutput(final ChecksOutput that) { - this(that.getTitle().orElse(null), that.getSummary().orElse(null), that.getText().orElse(null), + this(that.getTitle().orElse(null), + that.getSummary().map(TruncatedString::fromString).orElse(null), + that.getText().map(TruncatedString::fromString).orElse(null), that.getChecksAnnotations(), that.getChecksImages()); } @@ -43,11 +47,31 @@ public Optional getTitle() { } public Optional getSummary() { - return Optional.ofNullable(summary); + return Optional.ofNullable(summary).map(TruncatedString::build); + } + + /** + * Get the output summary, truncated by {@link TruncatedString} to maxSize. + * + * @param maxSize maximum size to truncate summary to. + * @return Summary, truncated to maxSize with truncation message if appropriate. + */ + public Optional getSummary(final int maxSize) { + return Optional.ofNullable(summary).flatMap(s -> Optional.ofNullable(s.build(maxSize))); } public Optional getText() { - return Optional.ofNullable(text); + return Optional.ofNullable(text).map(TruncatedString::build); + } + + /** + * Get the output text, truncated by {@link TruncatedString} to maxSize. + * + * @param maxSize maximum size to truncate text to. + * @return Text, truncated to maxSize with truncation message if appropriate. + */ + public Optional getText(final int maxSize) { + return Optional.ofNullable(text).flatMap(s -> Optional.ofNullable(s.build(maxSize))); } public List getChecksAnnotations() { @@ -74,8 +98,8 @@ public String toString() { */ public static class ChecksOutputBuilder { private String title; - private String summary; - private String text; + private TruncatedString summary; + private TruncatedString text; private List annotations; private List images; @@ -114,6 +138,22 @@ public ChecksOutputBuilder withTitle(final String title) { */ @SuppressWarnings("HiddenField") // builder pattern public ChecksOutputBuilder withSummary(final String summary) { + return withSummary(new TruncatedString.Builder().addText(summary).build()); + } + + /** + * Sets the summary of the check run, using a {@link TruncatedString}. + * + *

+ * Note that for the GitHub check runs, the {@code summary} supports Markdown. + *

+ * + * @param summary + * the summary of the check run as a {@link TruncatedString} + * @return this builder + */ + @SuppressWarnings("HiddenField") + public ChecksOutputBuilder withSummary(final TruncatedString summary) { this.summary = requireNonNull(summary); return this; } @@ -131,6 +171,22 @@ public ChecksOutputBuilder withSummary(final String summary) { */ @SuppressWarnings("HiddenField") // builder pattern public ChecksOutputBuilder withText(final String text) { + return withText(new TruncatedString.Builder().addText(text).build()); + } + + /** + * Adds the details description for a check run, using a {@link TruncatedString}. This parameter supports Markdown. + * + *

+ * Note that for a GitHub check run, the {@code text} supports Markdown. + *

+ * + * @param text + * the details description in Markdown as a {@link TruncatedString} + * @return this builder + */ + @SuppressWarnings("HiddenField") + public ChecksOutputBuilder withText(final TruncatedString text) { this.text = requireNonNull(text); return this; } diff --git a/src/main/java/io/jenkins/plugins/checks/api/TruncatedString.java b/src/main/java/io/jenkins/plugins/checks/api/TruncatedString.java new file mode 100644 index 00000000..71e6681b --- /dev/null +++ b/src/main/java/io/jenkins/plugins/checks/api/TruncatedString.java @@ -0,0 +1,132 @@ +package io.jenkins.plugins.checks.api; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * 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. + */ +public class TruncatedString { + + @NonNull + private final List chunks; + @NonNull + private final String truncationText; + + /** + * Create a {@link TruncatedString} with the provided chunks and truncation message. + * + * @param truncationText the message to be appended should maxSize be exceeded, e.g. + * "Some output is not shown here, see more on [Jenkins](url)." + * @param chunks a list of {@link CharSequence}s that are to be concatenated. + */ + private TruncatedString(@NonNull final String truncationText, @NonNull final List chunks) { + this.truncationText = Objects.requireNonNull(truncationText); + this.chunks = Objects.requireNonNull(chunks); + } + + /** + * 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 TruncatedString.Builder().addText(string).build(); + } + + @Override + public String toString() { + return String.join("", chunks); + } + + /** + * Builds the string without truncation. + * + * @return A string comprising the joined chunks. + */ + @CheckForNull + public String build() { + return chunks.isEmpty() ? null : String.join("", chunks); + } + + /** + * Builds the string such that it does not exceed maxSize, 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. + */ + @CheckForNull + public String build(final int maxSize) { + if (chunks.isEmpty()) { + return null; + } + String quickJoin = String.join("", chunks); + if (quickJoin.length() <= maxSize) { + return quickJoin; + } + StringBuilder builder = new StringBuilder(); + for (CharSequence chunk: chunks) { + if (builder.length() + chunk.length() + truncationText.length() < maxSize) { + builder.append(chunk); + } + else { + builder.append(truncationText); + break; + } + } + return builder.toString(); + } + + /** + * Builder for {@link TruncatedString}. + */ + public static class Builder { + private String truncationText = "Output truncated."; + private final List chunks = new ArrayList<>(); + + /** + * Builds the {@link TruncatedString}. + * + * @return the build {@link TruncatedString}. + */ + public TruncatedString build() { + return new TruncatedString(truncationText, chunks); + } + + /** + * Sets the truncation text. + * + * @param truncationText the text to append on overflow + * @return this builder + */ + @SuppressWarnings("HiddenField") + public Builder withTruncationText(@NonNull final String truncationText) { + this.truncationText = Objects.requireNonNull(truncationText); + return this; + } + + /** + * Adds a chunk of text to the buidler. + * + * @param chunk the chunk of text to append to this builder + * @return this buidler + */ + public Builder addText(@NonNull final CharSequence chunk) { + this.chunks.add(Objects.requireNonNull(chunk)); + return this; + } + + } + +}