cdc.issues.StructuredDescription Maven / Gradle / Ivy
package cdc.issues;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import cdc.util.strings.StringUtils;
/**
* Class used to build simple structured descriptions.
*
* It can have an optional header, followed by optional sections.
* Each section has a title and a content.
* A section is marked as {@code "# name"} where name is the section name (title).
* Header and sections contents can be structured as lists of ordered or unordered items.
* Unordered list items are generated as {@code "- item"}.
* Ordered list items are generated as {@code "N) item"}, where {@code N} is a number.
* List items can be indented.
*
* @author Damien Carbonne
*/
public class StructuredDescription {
private static final Pattern SECTION_PATTERN = Pattern.compile("(\\R|^)# (.+)(\\R|$)");
private static final Pattern TAIL_NL_PATTERN = Pattern.compile("\\R$");
private static final Pattern ITEM_START_PATTERN = Pattern.compile("(\\R|^) *(- |\\d+\\) )");
private static Pattern sectionPattern(String section) {
return Pattern.compile("(\\R|^)# " + section + "(\\R|$)");
}
private static String removeTailNL(String s) {
return TAIL_NL_PATTERN.matcher(s).replaceFirst("");
}
private final String text;
protected StructuredDescription(Builder> builder) {
this.text = builder.text.toString();
}
/**
* @return The text of the description.
*/
public String getText() {
return text;
}
/**
* @return The description header (text before first section).
*/
public String getHeader() {
final Matcher first = SECTION_PATTERN.matcher(text);
if (first.find()) {
return removeTailNL(text.substring(0, first.start()));
} else {
return text;
}
}
/**
* @return A list of declared sections.
*/
public List getSections() {
return SECTION_PATTERN.matcher(text)
.results()
.map(mr -> mr.group(2))
.toList();
}
/**
* @param section The section name.
* @return {@code true} if a section named {@code section} exists in this description.
*/
public boolean hasSection(String section) {
final Pattern pattern = sectionPattern(section);
return pattern.matcher(text).find();
}
/**
* @param section The section name.
* @return The content of the section named {@code section},
* or an empty string if no such section exists.
*
*/
public String getSectionContent(String section) {
final Matcher start = sectionPattern(section).matcher(text);
if (start.find()) {
final String tail = text.substring(start.end());
final Matcher end = SECTION_PATTERN.matcher(tail);
final String content;
if (end.find()) {
// There is an additional NL in some cases
content = removeTailNL(tail.substring(0, end.start()));
} else {
content = tail;
}
return content;
} else {
return "";
}
}
/**
* @param section The section name.
* @return A list of items declared in section named {@code section}.
* The returned list is empty if no such section exists.
*/
public List getSectionItems(String section) {
final String content = getSectionContent(section);
if (content != null) {
final List list = new ArrayList<>();
final String[] items = ITEM_START_PATTERN.split(content);
for (final String item : items) {
if (!StringUtils.isNullOrEmpty(item)) {
list.add(item);
}
}
return list;
} else {
return Collections.emptyList();
}
}
@Override
public String toString() {
return getText();
}
public static Builder> builder() {
return new Builder<>();
}
/**
* {@link StructuredDescription} builder.
*
* @author Damien Carbonne
* @param The Builder type.
*/
public static class Builder> {
private final StringBuilder text = new StringBuilder();
/** The indentation String. */
public static final String INDENT = " ";
protected Builder() {
}
protected static String indent(int level) {
final StringBuilder builder = new StringBuilder();
for (int i = 0; i < level; i++) {
builder.append(INDENT);
}
return builder.toString();
}
protected B self() {
@SuppressWarnings("unchecked")
final B x = (B) this;
return x;
}
/**
* Adds text to description.
*
* @param text The text to add.
* @return This builder.
*/
public B text(String text) {
this.text.append(text);
return self();
}
/**
* Appends: {@code text}.
*
* @param text The header text.
* @return This Builder.
*/
public B header(String text) {
return text(text);
}
/**
* Starts a section.
*
* @param section The section title.
* @return This builder.
*/
public B section(String section) {
if (this.text.length() > 0) {
this.text.append("\n\n# ")
.append(section);
} else {
this.text.append("# ")
.append(section);
}
return self();
}
/**
* Adds 1 unordered list {@code item} at indentation {@code level}.
*
* @param level The indentation level.
* @param item The list item.
* @return This builder.
*/
public B uItem(int level,
String item) {
this.text.append("\n");
if (level > 0) {
this.text.append(indent(level));
}
this.text.append("- ")
.append(item);
return self();
}
/**
* Adds 1 unordered list {@code item} at indentation {@code level 0}.
*
* @param item The list item.
* @return This builder.
*/
public B uItem(String item) {
return uItem(0, item);
}
/**
* Adds unordered list {@code items} at indentation {@code level}.
*
* @param level The indentation level.
* @param items The list items.
* @return This builder.
*/
public B uItems(int level,
String... items) {
for (final String item : items) {
uItem(level, item);
}
return self();
}
/**
* Adds unordered list {@code items} at indentation {@code level 0}.
*
* @param items The list items.
* @return This builder.
*/
public B uItems(String... items) {
return uItems(0, items);
}
/**
* Adds unordered list {@code items} at indentation {@code level}.
*
* @param level The indentation level.
* @param items The list items.
* @return This builder.
*/
public B uItems(int level,
Collection items) {
for (final String item : items) {
uItem(level, item);
}
return self();
}
/**
* Adds unordered list {@code items} at indentation {@code level 0}.
*
* @param items The list items.
* @return This builder.
*/
public B uItems(Collection items) {
return uItems(0, items);
}
/**
* Adds 1 ordered list {@code item} at indentation {@code level} and with {@code number}.
*
* @param level The indentation level.
* @param number The number.
* @param item The list item.
* @return This builder.
*/
public B oItem(int level,
int number,
String item) {
this.text.append("\n");
if (level > 0) {
this.text.append(indent(level));
}
this.text.append(Integer.toString(number))
.append(") ")
.append(item);
return self();
}
/**
* Adds 1 ordered list {@code item} at indentation {@code level 0} and with {@code number}.
*
* @param number The number.
* @param item The list item.
* @return This builder.
*/
public B oItem(int number,
String item) {
return oItem(0, number, item);
}
/**
* Adds unordered list {@code items} at indentation {@code level}, starting at {@code number}.
*
* @param level The indentation level.
* @param number The first number.
* @param items The list items.
* @return This builder.
*/
public B oItems(int level,
int number,
String... items) {
for (final String item : items) {
oItem(level, number, item);
number++;
}
return self();
}
/**
* Adds unordered list {@code items} at indentation {@code level 0}, starting at {@code number}.
*
* @param number The first number.
* @param items The list items.
* @return This builder.
*/
public B oItems(int number,
String... items) {
return oItems(0, number, items);
}
/**
* Adds unordered list {@code items} at indentation {@code level}, starting at {@code number}.
*
* @param level The indentation level.
* @param number The first number.
* @param items The list items.
* @return This builder.
*/
public B oItems(int level,
int number,
Collection items) {
for (final String item : items) {
oItem(level, item);
}
return self();
}
/**
* Adds unordered list {@code items} at indentation {@code level °}, starting at {@code number}.
*
* @param number The first number.
* @param items The list items.
* @return This builder.
*/
public B oItems(int number,
Collection items) {
return oItems(0, number, items);
}
/**
* Appends a violation explanation.
*
* @param explanation The violation explanation.
* @return This Builder.
*/
public B violation(String explanation) {
return text("\n").text(explanation);
}
public B value(String value) {
return text("\n'").text(value)
.text("'");
}
public B justification(int number,
String justification) {
return oItem(number, justification);
}
public B justifications(String... justifications) {
return oItems(1, justifications);
}
public StructuredDescription build() {
return new StructuredDescription(this);
}
}
}