From 43e24c3eb42085f99191905e4691691a50e65ac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20R=C3=B6sch?= Date: Sat, 19 Aug 2017 00:28:58 +0200 Subject: [PATCH] Tabulate crash report mod list and add signature information (#4251) --- .../common/ForgeModContainer.java | 5 + .../minecraftforge/common/util/TextTable.java | 220 ++++++++++++++++++ .../fml/common/LoadController.java | 25 +- .../minecraftforge/test/TextTableTest.java | 119 ++++++++++ 4 files changed, 364 insertions(+), 5 deletions(-) create mode 100644 src/main/java/net/minecraftforge/common/util/TextTable.java create mode 100644 src/test/java/net/minecraftforge/test/TextTableTest.java diff --git a/src/main/java/net/minecraftforge/common/ForgeModContainer.java b/src/main/java/net/minecraftforge/common/ForgeModContainer.java index de8b492ef..edf614d5b 100644 --- a/src/main/java/net/minecraftforge/common/ForgeModContainer.java +++ b/src/main/java/net/minecraftforge/common/ForgeModContainer.java @@ -414,6 +414,11 @@ public class ForgeModContainer extends DummyModContainer implements WorldAccessC all.add(asm.getClassName()); for (ASMData asm : evt.getASMHarvestedData().getAll(ICrashCallable.class.getName().replace('.', '/'))) all.add(asm.getClassName()); + // Add table classes for mod list tabulation + all.add("net/minecraftforge/common/util/TextTable"); + all.add("net/minecraftforge/common/util/TextTable$Column"); + all.add("net/minecraftforge/common/util/TextTable$Row"); + all.add("net/minecraftforge/common/util/TextTable$Alignment"); all.removeIf(cls -> !cls.startsWith("net/minecraft/") && !cls.startsWith("net/minecraftforge/")); diff --git a/src/main/java/net/minecraftforge/common/util/TextTable.java b/src/main/java/net/minecraftforge/common/util/TextTable.java new file mode 100644 index 000000000..b14c97315 --- /dev/null +++ b/src/main/java/net/minecraftforge/common/util/TextTable.java @@ -0,0 +1,220 @@ +/* + * Minecraft Forge + * Copyright (c) 2016. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package net.minecraftforge.common.util; + +import com.google.common.collect.Streams; +import org.apache.commons.lang3.StringUtils; + +import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Utility to format data into a textual (markdown-compliant) table. + */ +public class TextTable +{ + public static Column column(String header) + { + return new Column(header); + } + + public static Column column(String header, Alignment alignment) + { + return new Column(header, alignment); + } + + private final List columns; + private final List rows = new ArrayList<>(); + + public TextTable(List columns) + { + this.columns = columns; + } + + public String build(String lineEnding) + { + StringBuilder destination = new StringBuilder(); + append(destination, lineEnding); + return destination.toString(); + } + + /** + * Appends the data formatted as a table to the given string builder. + * The padding character used for the column alignments is a single space (' '), + * the separate between column headers and values is a dash ('-'). + * Note that you *have* to specify a line ending, '\n' isn't used by default. + *

+ * The generated table is compliant with the markdown file format. + * + * @param destination a string builder to append the table to + * @param lineEnding the line ending to use for each row of the table + */ + public void append(StringBuilder destination, String lineEnding) + { + List headers = columns.stream().map(c -> c.formatHeader(" ")).collect(Collectors.toList()); + printRow(destination, headers); + destination.append(lineEnding); + printSeparators(destination); + for (Row row : rows) + { + destination.append(lineEnding); + printRow(destination, row.format(columns, " ")); + } + } + + private void printSeparators(StringBuilder destination) + { + destination.append('|'); + for (Column column : columns) + { + destination.append(column.alignment != Alignment.RIGHT ? ':' : ' '); + destination.append(column.getSeparator('-')); + destination.append(column.alignment != Alignment.LEFT ? ':' : ' '); + destination.append('|'); + } + } + + private void printRow(StringBuilder destination, List values) + { + destination.append('|'); + for (String value : values) + { + destination.append(' '); + destination.append(value); + destination.append(' '); + destination.append('|'); + } + } + + public void add(@Nonnull Object... values) + { + if (values.length != columns.size()) + { + throw new IllegalArgumentException("Received wrong amount of values for table row, expected " + columns.size() + ", received " + columns.size() + "."); + } + Row row = new Row(); + for (int i = 0; i < values.length; i++) + { + String value = Objects.toString(values[i]); + row.values.add(value); + columns.get(i).fit(value); + } + rows.add(row); + } + + public void clear() + { + for (Column column : columns) + { + column.resetWidth(); + } + rows.clear(); + } + + public List getColumns() + { + return Collections.unmodifiableList(columns); + } + + public static class Column + { + private String header; + private int width; + private Alignment alignment; + + public Column(String header) + { + this(header, Alignment.LEFT); + } + + public Column(String header, Alignment alignment) + { + this.header = header; + this.width = header.length(); + this.alignment = alignment; + } + + public String formatHeader(String padding) + { + return format(header, padding); + } + + public String format(String value, String padding) + { + switch (alignment) + { + case LEFT: + return StringUtils.rightPad(value, width, padding); + case RIGHT: + return StringUtils.leftPad(value, width, padding); + default: + int length = value.length(); + int left = (width - length) / 2; + int leftWidth = left + length; + return StringUtils.rightPad(StringUtils.leftPad(value, leftWidth, padding), width, padding); + } + } + + public String getSeparator(char character) + { + return StringUtils.leftPad("", width, character); + } + + public void fit(String value) + { + if (value.length() > width) + { + width = value.length(); + } + } + + public void resetWidth() + { + this.width = header.length(); + } + + public int getWidth() + { + return width; + } + } + + public static class Row + { + private final ArrayList values = new ArrayList<>(); + + public List format(List columns, String padding) + { + if (columns.size() != values.size()) + { + throw new IllegalArgumentException("Received wrong amount of columns for table row, expected " + columns.size() + ", received " + columns.size() + "."); + } + return Streams.zip(values.stream(), columns.stream(), (v, c) -> c.format(v, padding)).collect(Collectors.toList()); + } + } + + public enum Alignment + { + LEFT, CENTER, RIGHT + } +} diff --git a/src/main/java/net/minecraftforge/fml/common/LoadController.java b/src/main/java/net/minecraftforge/fml/common/LoadController.java index e612ceb24..087673272 100644 --- a/src/main/java/net/minecraftforge/fml/common/LoadController.java +++ b/src/main/java/net/minecraftforge/fml/common/LoadController.java @@ -28,6 +28,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.stream.Collectors; +import net.minecraftforge.common.util.TextTable; import net.minecraftforge.fml.common.LoaderState.ModState; import net.minecraftforge.fml.common.ProgressManager.ProgressBar; import net.minecraftforge.fml.common.event.FMLEvent; @@ -310,14 +311,28 @@ public class LoadController for (ModState state : ModState.values()) ret.append(" '").append(state.getMarker()).append("' = ").append(state.toString()); + TextTable table = new TextTable(Lists.newArrayList( + TextTable.column("State"), + TextTable.column("ID"), + TextTable.column("Version"), + TextTable.column("Source"), + TextTable.column("Signature")) + ); for (ModContainer mc : loader.getModList()) { - ret.append("\n\t"); - for (ModState state : modStates.get(mc.getModId())) - ret.append(state.getMarker()); - - ret.append("\t").append(mc.getModId()).append("{").append(mc.getVersion()).append("} [").append(mc.getName()).append("] (").append(mc.getSource().getName()).append(") "); + table.add( + modStates.get(mc.getModId()).stream().map(ModState::getMarker).reduce("", (a, b) -> a + b), + mc.getModId(), + mc.getVersion(), + mc.getSource().getName(), + mc.getSigningCertificate() != null ? CertificateHelper.getFingerprint(mc.getSigningCertificate()) : "None" + ); } + + ret.append("\n"); + ret.append("\n\t"); + table.append(ret, "\n\t"); + ret.append("\n"); } public List getActiveModList() diff --git a/src/test/java/net/minecraftforge/test/TextTableTest.java b/src/test/java/net/minecraftforge/test/TextTableTest.java new file mode 100644 index 000000000..0203eaa73 --- /dev/null +++ b/src/test/java/net/minecraftforge/test/TextTableTest.java @@ -0,0 +1,119 @@ +/* + * Minecraft Forge + * Copyright (c) 2016. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package net.minecraftforge.test; + +import com.google.common.collect.Lists; +import net.minecraftforge.common.util.TextTable; +import net.minecraftforge.common.util.TextTable.Column; +import org.junit.Assert; +import org.junit.Test; + +import static net.minecraftforge.common.util.TextTable.column; + +public class TextTableTest +{ + private static final String WIDTH_REFERENCE = "StringOfWidth15"; + private static final int WIDTH_REFERENCE_LENGTH = WIDTH_REFERENCE.length(); + + @Test + public void testColumnWidthAdjustment() + { + Column column = column("Column", TextTable.Alignment.LEFT); + column.fit(WIDTH_REFERENCE); + String paddedHeader = column.formatHeader("-"); + Assert.assertEquals("Formatted column header didn't have correct length", WIDTH_REFERENCE_LENGTH, paddedHeader.length()); + Assert.assertEquals("Formatted column header wasn't padded properly", "Column---------", paddedHeader); + + String paddedReference = column.format(WIDTH_REFERENCE, "-"); + Assert.assertEquals("Formatted width reference didn't have correct length", WIDTH_REFERENCE_LENGTH, paddedReference.length()); + Assert.assertEquals("Formatted width reference was changed despite defining width", WIDTH_REFERENCE, paddedReference); + } + + @Test + public void testLeftAlignment() + { + Column column = column("Left", TextTable.Alignment.LEFT); + column.fit(WIDTH_REFERENCE); + + String paddedHeader = column.formatHeader("-"); + Assert.assertEquals("Left-aligned header should be padded on the right", "Left-----------", paddedHeader); + String paddedReference = column.format(WIDTH_REFERENCE, "-"); + Assert.assertEquals("Left-aligned reference should'nt be padded", WIDTH_REFERENCE, paddedReference); + String paddedValue = column.format("Value", "-"); + Assert.assertEquals("Left-aligned value should be padded on the right", "Value----------", paddedValue); + } + + @Test + public void testCenterAlignment() + { + Column column = column("Centered", TextTable.Alignment.CENTER); + column.fit(WIDTH_REFERENCE); + + String paddedHeader = column.formatHeader("-"); + Assert.assertEquals("Centered header should be padded equally on both sides", "---Centered----", paddedHeader); + String paddedReference = column.format(WIDTH_REFERENCE, "-"); + Assert.assertEquals("Centered reference should'nt be padded", WIDTH_REFERENCE, paddedReference); + String paddedValue = column.format("Value", "-"); + Assert.assertEquals("Centered value should be padded equally on both sides", "-----Value-----", paddedValue); + String paddedOffCenter = column.format("Value1", "-"); + Assert.assertNotEquals("Center padding should be left-biased", "-----Value1----", paddedOffCenter); + } + + @Test + public void testRightAlignment() + { + Column column = column("Right", TextTable.Alignment.RIGHT); + column.fit(WIDTH_REFERENCE); + + String paddedHeader = column.formatHeader("-"); + Assert.assertEquals("Right-aligned header should be padded on the left", "----------Right", paddedHeader); + String paddedReference = column.format(WIDTH_REFERENCE, "-"); + Assert.assertEquals("Right-aligned reference should'nt be padded", WIDTH_REFERENCE, paddedReference); + String paddedValue = column.format("Value", "-"); + Assert.assertEquals("Right-aligned value should be padded on the left", "----------Value", paddedValue); + } + + @Test + public void testMarkdownCompliance() + { + TextTable table = new TextTable(Lists.newArrayList( + column("Left", TextTable.Alignment.LEFT), + column("Center", TextTable.Alignment.CENTER), + column("Right", TextTable.Alignment.RIGHT) + )); + table.add("Long Value 1", "Value 2", "Value 3"); + table.add("Value 1", "Long Value 2", "Value 3"); + table.add("Value 1", "Value 2", "Long Value 3"); + int[] columnWidths = table.getColumns().stream().mapToInt(Column::getWidth).toArray(); + Assert.assertArrayEquals("Column widths should adjust for long values", new int[]{12, 12, 12}, columnWidths); + + String[] result = table.build("\n").split("\n"); + Assert.assertEquals("Header row + separator row + value rows should result in 5 lines", 5, result.length); + Assert.assertEquals( + "Column headers should be properly formatted", + "| Left | Center | Right |", + result[0]); + Assert.assertEquals( + "Header-body separators should contain markdown alignment information", + "|:------------ |:------------:| ------------:|", + result[1] + ); + } +}