diff options
author | Cow <cow@volloeko.de> | 2020-06-22 03:47:04 +0200 |
---|---|---|
committer | Cow <cow@volloeko.de> | 2020-06-22 03:47:04 +0200 |
commit | 3d3f861866c2196bdb83bdfbd73e58aa56be2966 (patch) | |
tree | 9247ac8a5aef35cdbcaf74016611fc1167dc7a77 /src | |
parent | 830e815c429905b29a49ecc1a2bf9e9abbca148b (diff) | |
download | Cowlection-3d3f861866c2196bdb83bdfbd73e58aa56be2966.tar.gz Cowlection-3d3f861866c2196bdb83bdfbd73e58aa56be2966.tar.bz2 Cowlection-3d3f861866c2196bdb83bdfbd73e58aa56be2966.zip |
Improved mc log file search
- added help button
- added search option: chatbox only
- enhanced log entry details (including time and log file)
- double click to open corresponding log file
- fix: search results are now properly sorted by date
Diffstat (limited to 'src')
8 files changed, 496 insertions, 90 deletions
diff --git a/src/main/java/eu/olli/cowmoonication/Cowmoonication.java b/src/main/java/eu/olli/cowmoonication/Cowmoonication.java index 45fb791..ebadcde 100644 --- a/src/main/java/eu/olli/cowmoonication/Cowmoonication.java +++ b/src/main/java/eu/olli/cowmoonication/Cowmoonication.java @@ -32,6 +32,7 @@ public class Cowmoonication { public static final String MODNAME = "@MODNAME@"; public static final String GITURL = "@GITURL@"; private static Cowmoonication instance; + private File configDir; private File modsDir; private MooConfig config; private FriendsHandler friendsHandler; @@ -46,13 +47,13 @@ public class Cowmoonication { logger = e.getModLog(); modsDir = e.getSourceFile().getParentFile(); - File modDir = new File(e.getModConfigurationDirectory(), MODID + File.separatorChar); - if (!modDir.exists()) { - modDir.mkdirs(); + this.configDir = new File(e.getModConfigurationDirectory(), MODID + File.separatorChar); + if (!configDir.exists()) { + configDir.mkdirs(); } - friendsHandler = new FriendsHandler(this, new File(modDir, "friends.json")); - config = new MooConfig(this, new Configuration(new File(modDir, MODID + ".cfg"))); + friendsHandler = new FriendsHandler(this, new File(configDir, "friends.json")); + config = new MooConfig(this, new Configuration(new File(configDir, MODID + ".cfg"))); chatHelper = new ChatHelper(); } @@ -94,6 +95,10 @@ public class Cowmoonication { return playerCache; } + public File getConfigDirectory() { + return configDir; + } + public File getModsDirectory() { return modsDir; } diff --git a/src/main/java/eu/olli/cowmoonication/command/MooCommand.java b/src/main/java/eu/olli/cowmoonication/command/MooCommand.java index 7a468de..c926939 100644 --- a/src/main/java/eu/olli/cowmoonication/command/MooCommand.java +++ b/src/main/java/eu/olli/cowmoonication/command/MooCommand.java @@ -114,10 +114,11 @@ public class MooCommand extends CommandBase { } } } - main.getChatHelper().sendMessage(EnumChatFormatting.YELLOW, - "Found " + EnumChatFormatting.GOLD + detectedMinionCount + EnumChatFormatting.YELLOW + " minions" + - (minionsWithSkinCount > 0 ? (" + " + EnumChatFormatting.GOLD + minionsWithSkinCount + EnumChatFormatting.YELLOW + " unknown minions with skins") : "") + - " on this island"); + StringBuilder analysisResults = new StringBuilder("Found ").append(EnumChatFormatting.GOLD).append(detectedMinionCount).append(EnumChatFormatting.YELLOW).append(" minions"); + if (minionsWithSkinCount > 0) { + analysisResults.append(" + ").append(EnumChatFormatting.GOLD).append(minionsWithSkinCount).append(EnumChatFormatting.YELLOW).append(" unknown minions with skins"); + } + analysisResults.append(" on this island"); detectedMinions.entrySet().stream() .sorted(Map.Entry.comparingByKey()) // sort alphabetically by minion name and tier .forEach(minion -> { @@ -130,15 +131,18 @@ public class MooCommand extends CommandBase { EnumChatFormatting tierColor = Utils.getMinionTierColor(tierArabic); minionWithTier = minionWithTier.substring(0, lastSpace) + " " + tierColor + (MooConfig.useRomanNumerals() ? tierRoman : tierArabic); - main.getChatHelper().sendMessage(EnumChatFormatting.GOLD, " " + minion.getValue() + (minion.getValue() > 1 ? "✕ " : "⨉ ") + EnumChatFormatting.YELLOW + minionWithTier); + analysisResults.append("\n ").append(EnumChatFormatting.GOLD).append(minion.getValue()).append(minion.getValue() > 1 ? "✕ " : "⨉ ") + .append(EnumChatFormatting.YELLOW).append(minionWithTier); }); detectedMinionsWithSkin.entrySet().stream() .sorted(Map.Entry.comparingByKey()) // sort by tier .forEach(minionWithSkin -> { EnumChatFormatting tierColor = Utils.getMinionTierColor(minionWithSkin.getKey()); String minionTier = MooConfig.useRomanNumerals() ? Utils.convertArabicToRoman(minionWithSkin.getKey()) : String.valueOf(minionWithSkin.getKey()); - main.getChatHelper().sendMessage(EnumChatFormatting.GOLD, " " + minionWithSkin.getValue() + (minionWithSkin.getValue() > 1 ? "✕ " : "⨉ ") + EnumChatFormatting.RED + "Unknown minion " + EnumChatFormatting.YELLOW + "(new or with minion skin) " + tierColor + minionTier); + analysisResults.append("\n ").append(EnumChatFormatting.GOLD).append(minionWithSkin.getValue()).append(minionWithSkin.getValue() > 1 ? "✕ " : "⨉ ") + .append(EnumChatFormatting.RED).append("Unknown minion ").append(EnumChatFormatting.YELLOW).append("(new or with minion skin) ").append(tierColor).append(minionTier); }); + main.getChatHelper().sendMessage(EnumChatFormatting.YELLOW, analysisResults.toString()); } else if (args.length == 2 && args[0].equalsIgnoreCase("add")) { handleBestFriendAdd(args[1]); } else if (args.length == 2 && args[0].equalsIgnoreCase("remove")) { @@ -153,7 +157,7 @@ public class MooCommand extends CommandBase { else if (args[0].equalsIgnoreCase("config") || args[0].equalsIgnoreCase("toggle")) { new TickDelay(() -> Minecraft.getMinecraft().displayGuiScreen(new MooGuiConfig(null)), 1); // delay by 1 tick, because the chat closing would close the new gui instantly as well. } else if (args[0].equalsIgnoreCase("search")) { - new TickDelay(() -> Minecraft.getMinecraft().displayGuiScreen(new GuiSearch()), 1); // delay by 1 tick, because the chat closing would close the new gui instantly as well. + new TickDelay(() -> Minecraft.getMinecraft().displayGuiScreen(new GuiSearch(main.getConfigDirectory())), 1); // delay by 1 tick, because the chat closing would close the new gui instantly as well. } else if (args[0].equalsIgnoreCase("guiscale")) { int currentGuiScale = (Minecraft.getMinecraft()).gameSettings.guiScale; if (args.length == 1) { @@ -205,8 +209,10 @@ public class MooCommand extends CommandBase { } } // "catch-all" remaining sub-commands - else { + else if (args[0].equalsIgnoreCase("help")) { sendCommandUsage(sender); + } else { + main.getChatHelper().sendMessage(EnumChatFormatting.RED, "Command " + EnumChatFormatting.DARK_RED + "/" + getCommandName() + " " + args[0] + EnumChatFormatting.RED + " doesn't exist. Use " + EnumChatFormatting.DARK_RED + "/" + getCommandName() + " help " + EnumChatFormatting.RED + "to show command usage."); } } diff --git a/src/main/java/eu/olli/cowmoonication/config/MooConfig.java b/src/main/java/eu/olli/cowmoonication/config/MooConfig.java index 235fd14..dd6b462 100644 --- a/src/main/java/eu/olli/cowmoonication/config/MooConfig.java +++ b/src/main/java/eu/olli/cowmoonication/config/MooConfig.java @@ -221,7 +221,7 @@ public class MooConfig { List<String> logsDirs = new ArrayList<>(); File currentMcLogsDirFile = new File(Minecraft.getMinecraft().mcDataDir, "logs"); if (currentMcLogsDirFile.exists() && currentMcLogsDirFile.isDirectory()) { - String currentMcLogsDir = currentMcLogsDirFile.getAbsolutePath(); + String currentMcLogsDir = Utils.toRealPath(currentMcLogsDirFile); logsDirs.add(currentMcLogsDir); } @@ -240,7 +240,7 @@ public class MooConfig { } File defaultMcLogsDirFile = new File(defaultMcLogsDir); if (defaultMcLogsDirFile.exists() && defaultMcLogsDirFile.isDirectory() && !currentMcLogsDirFile.equals(defaultMcLogsDirFile)) { - logsDirs.add(defaultMcLogsDirFile.getAbsolutePath()); + logsDirs.add(Utils.toRealPath(defaultMcLogsDirFile)); } return logsDirs.toArray(new String[]{}); } diff --git a/src/main/java/eu/olli/cowmoonication/config/MooGuiConfig.java b/src/main/java/eu/olli/cowmoonication/config/MooGuiConfig.java index 7807e03..a89556d 100644 --- a/src/main/java/eu/olli/cowmoonication/config/MooGuiConfig.java +++ b/src/main/java/eu/olli/cowmoonication/config/MooGuiConfig.java @@ -2,6 +2,7 @@ package eu.olli.cowmoonication.config; import eu.olli.cowmoonication.Cowmoonication; import eu.olli.cowmoonication.search.GuiTooltip; +import eu.olli.cowmoonication.util.Utils; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.GuiTextField; @@ -30,7 +31,7 @@ public class MooGuiConfig extends GuiConfig { false, false, EnumChatFormatting.BOLD + "Configuration for " + Cowmoonication.MODNAME); - titleLine2 = EnumChatFormatting.GRAY + MooConfig.getConfig().getConfigFile().getAbsolutePath(); + titleLine2 = EnumChatFormatting.GRAY + Utils.toRealPath(MooConfig.getConfig().getConfigFile()); } private static List<IConfigElement> getConfigElements() { diff --git a/src/main/java/eu/olli/cowmoonication/data/LogEntry.java b/src/main/java/eu/olli/cowmoonication/data/LogEntry.java new file mode 100644 index 0000000..e258105 --- /dev/null +++ b/src/main/java/eu/olli/cowmoonication/data/LogEntry.java @@ -0,0 +1,82 @@ +package eu.olli.cowmoonication.data; + +import net.minecraft.util.EnumChatFormatting; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.util.regex.Pattern; + +public class LogEntry { + private static final Pattern UTF_PARAGRAPH_SYMBOL = Pattern.compile("§"); + private LocalDateTime time; + private Path filePath; + private String message; + + public LogEntry(LocalDateTime time, Path filePath, String logEntry) { + this.time = time; + this.filePath = filePath; + this.message = logEntry; + } + + public LogEntry(String message) { + this.message = message; + } + + public LocalDateTime getTime() { + return time; + } + + public Path getFilePath() { + return filePath; + } + + public String getMessage() { + return message; + } + + public void addLogLine(String logLine) { + message += "\n" + logLine; + } + + public void removeFormatting() { + this.message = EnumChatFormatting.getTextWithoutFormattingCodes(message); + } + + public void fixWeirdCharacters() { + if (message.contains("§")) { + message = UTF_PARAGRAPH_SYMBOL.matcher(message).replaceAll("§"); + } + } + + /** + * Is this log entry a 'real' log entry or just an error message from the search process? + * + * @return true if error message, otherwise false + */ + public boolean isError() { + return time == null && filePath == null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + LogEntry logEntry = (LogEntry) o; + return new EqualsBuilder() + .append(time, logEntry.time) + .append(filePath, logEntry.filePath) + .append(message, logEntry.message) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(time) + .append(filePath) + .append(message) + .toHashCode(); + } +} diff --git a/src/main/java/eu/olli/cowmoonication/search/GuiSearch.java b/src/main/java/eu/olli/cowmoonication/search/GuiSearch.java index b53aa8c..7932b29 100644 --- a/src/main/java/eu/olli/cowmoonication/search/GuiSearch.java +++ b/src/main/java/eu/olli/cowmoonication/search/GuiSearch.java @@ -1,42 +1,70 @@ package eu.olli.cowmoonication.search; -import com.google.common.base.Joiner; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.mojang.realmsclient.util.Pair; +import eu.olli.cowmoonication.Cowmoonication; import eu.olli.cowmoonication.config.MooConfig; +import eu.olli.cowmoonication.data.LogEntry; +import eu.olli.cowmoonication.util.Utils; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.*; import net.minecraft.client.renderer.GlStateManager; import net.minecraft.client.renderer.Tessellator; +import net.minecraft.util.ChatComponentText; import net.minecraft.util.EnumChatFormatting; import net.minecraft.util.IChatComponent; -import net.minecraftforge.common.ForgeHooks; import net.minecraftforge.common.ForgeVersion; import net.minecraftforge.fml.client.GuiScrollingList; import net.minecraftforge.fml.client.config.GuiButtonExt; import net.minecraftforge.fml.client.config.GuiCheckBox; import net.minecraftforge.fml.client.config.GuiUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.commons.lang3.tuple.ImmutableTriple; import org.lwjgl.input.Keyboard; +import org.lwjgl.opengl.GL11; +import java.awt.*; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; +import java.lang.reflect.Field; import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; import java.util.*; -import java.util.concurrent.Executors; +import java.util.concurrent.*; +import java.util.stream.Collectors; +import java.util.zip.GZIPInputStream; public class GuiSearch extends GuiScreen { private static final String SEARCH_QUERY_PLACE_HOLDER = "Search for..."; + private final File mcLogOutputFile; + /** + * @see Executors#newCachedThreadPool() + */ + private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, + 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue<>(), new ThreadFactoryBuilder().setNameFormat(Cowmoonication.MODID + "-logfilesearcher-%d").build()); // data private String searchQuery; + private boolean chatOnly; private boolean matchCase; private boolean removeFormatting; /** * Cached results are required after resizing the client */ - private List<String> searchResults; + private List<LogEntry> searchResults; private LocalDate dateStart; private LocalDate dateEnd; // gui elements private GuiButton buttonSearch; private GuiButton buttonClose; + private GuiButton buttonHelp; + private GuiCheckBox checkboxChatOnly; private GuiCheckBox checkboxMatchCase; private GuiCheckBox checkboxRemoveFormatting; private GuiTextField fieldSearchQuery; @@ -45,13 +73,22 @@ public class GuiSearch extends GuiScreen { private SearchResults guiSearchResults; private List<GuiTooltip> guiTooltips; private boolean isSearchInProgress; - - public GuiSearch() { + private String analyzedFiles; + private String analyzedFilesWithHits; + private boolean areEntriesSearchResults; + + public GuiSearch(File configDirectory) { + this.mcLogOutputFile = new File(configDirectory, "mc-log.txt"); + try { + mcLogOutputFile.createNewFile(); + } catch (IOException e) { + e.printStackTrace(); + } this.searchQuery = SEARCH_QUERY_PLACE_HOLDER; - this.matchCase = false; this.searchResults = new ArrayList<>(); this.dateStart = MooConfig.calculateStartDate(); this.dateEnd = LocalDate.now(); + this.chatOnly = true; } /** @@ -62,7 +99,7 @@ public class GuiSearch extends GuiScreen { public void initGui() { this.guiTooltips = new ArrayList<>(); - this.fieldSearchQuery = new GuiTextField(42, this.fontRendererObj, this.width / 2 - 100, 15, 200, 20); + this.fieldSearchQuery = new GuiTextField(42, this.fontRendererObj, this.width / 2 - 100, 13, 200, 20); this.fieldSearchQuery.setMaxStringLength(255); this.fieldSearchQuery.setText(searchQuery); if (SEARCH_QUERY_PLACE_HOLDER.equals(searchQuery)) { @@ -70,23 +107,33 @@ public class GuiSearch extends GuiScreen { this.fieldSearchQuery.setSelectionPos(0); } - // date fields + // date field: start this.fieldDateStart = new GuiDateField(50, this.fontRendererObj, this.width / 2 + 110, 15, 70, 15); this.fieldDateStart.setText(dateStart.toString()); addTooltip(fieldDateStart, Arrays.asList(EnumChatFormatting.YELLOW + "Start date", "" + EnumChatFormatting.GRAY + EnumChatFormatting.ITALIC + "Format: " + EnumChatFormatting.RESET + "year-month-day")); - + // date field: end this.fieldDateEnd = new GuiDateField(51, this.fontRendererObj, this.width / 2 + 110, 35, 70, 15); this.fieldDateEnd.setText(dateEnd.toString()); addTooltip(fieldDateEnd, Arrays.asList(EnumChatFormatting.YELLOW + "End date", "" + EnumChatFormatting.GRAY + EnumChatFormatting.ITALIC + "Format: " + EnumChatFormatting.RESET + "year-month-day")); - // buttons + // close this.buttonList.add(this.buttonClose = new GuiButtonExt(0, this.width - 25, 3, 22, 20, EnumChatFormatting.RED + "X")); addTooltip(buttonClose, Arrays.asList(EnumChatFormatting.RED + "Close search interface", "" + EnumChatFormatting.GRAY + EnumChatFormatting.ITALIC + "Hint:" + EnumChatFormatting.RESET + " alternatively press ESC")); - - this.buttonList.add(this.checkboxMatchCase = new GuiCheckBox(1, this.width / 2 - 100, 40, " Match case", matchCase)); + // help + this.buttonList.add(this.buttonHelp = new GuiButtonExt(1, this.width - 25 - 25, 3, 22, 20, "?")); + addTooltip(buttonHelp, Collections.singletonList(EnumChatFormatting.YELLOW + "Show help")); + + // chatOnly + this.buttonList.add(this.checkboxChatOnly = new GuiCheckBox(21, this.width / 2 - 100, 35, " Chatbox only", chatOnly)); + addTooltip(checkboxChatOnly, Collections.singletonList(EnumChatFormatting.YELLOW + "Should " + EnumChatFormatting.GOLD + "only " + EnumChatFormatting.YELLOW + "results that have " + EnumChatFormatting.GOLD + "appeared in the chat box " + EnumChatFormatting.YELLOW + "be displayed?\n" + + EnumChatFormatting.GRAY + "For example, this " + EnumChatFormatting.WHITE + "excludes error messages" + EnumChatFormatting.GRAY + " but still " + EnumChatFormatting.WHITE + "includes messages sent by a server" + EnumChatFormatting.GRAY + ".")); + // matchCase + this.buttonList.add(this.checkboxMatchCase = new GuiCheckBox(20, this.width / 2 - 100, 45, " Match case", matchCase)); addTooltip(checkboxMatchCase, Collections.singletonList(EnumChatFormatting.YELLOW + "Should the search be " + EnumChatFormatting.GOLD + "case-sensitive" + EnumChatFormatting.YELLOW + "?")); - this.buttonList.add(this.checkboxRemoveFormatting = new GuiCheckBox(1, this.width / 2 - 100, 50, " Remove formatting", removeFormatting)); + // removeFormatting + this.buttonList.add(this.checkboxRemoveFormatting = new GuiCheckBox(22, this.width / 2 - 100, 55, " Remove formatting", removeFormatting)); addTooltip(checkboxRemoveFormatting, Collections.singletonList(EnumChatFormatting.YELLOW + "Should " + EnumChatFormatting.GOLD + "formatting " + EnumChatFormatting.YELLOW + "and " + EnumChatFormatting.GOLD + "color codes " + EnumChatFormatting.YELLOW + "be " + EnumChatFormatting.GOLD + "removed " + EnumChatFormatting.YELLOW + "from the search results?")); + // search this.buttonList.add(this.buttonSearch = new GuiButtonExt(100, this.width / 2 + 40, 40, 60, 20, "Search")); this.guiSearchResults = new SearchResults(70); @@ -156,15 +203,24 @@ public class GuiSearch extends GuiScreen { // copy all search results String searchResults = guiSearchResults.getAllSearchResults(); if (!searchResults.isEmpty()) { - GuiScreen.setClipboardString(EnumChatFormatting.getTextWithoutFormattingCodes(searchResults)); + GuiScreen.setClipboardString(searchResults); } } else if (GuiScreen.isKeyComboCtrlC(keyCode)) { // copy current selected entry - String selectedSearchResult = guiSearchResults.getSelectedSearchResult(); + LogEntry selectedSearchResult = guiSearchResults.getSelectedSearchResult(); + if (selectedSearchResult != null) { + GuiScreen.setClipboardString(EnumChatFormatting.getTextWithoutFormattingCodes(selectedSearchResult.getMessage())); + } + } else if (keyCode == Keyboard.KEY_C && isCtrlKeyDown() && isShiftKeyDown() && !isAltKeyDown()) { + // copy current selected entry with formatting codes + LogEntry selectedSearchResult = guiSearchResults.getSelectedSearchResult(); if (selectedSearchResult != null) { - GuiScreen.setClipboardString(EnumChatFormatting.getTextWithoutFormattingCodes(selectedSearchResult)); + GuiScreen.setClipboardString(selectedSearchResult.getMessage()); } } else { + if (keyCode == Keyboard.KEY_ESCAPE) { + guiSearchResults = null; + } super.keyTyped(typedChar, keyCode); } @@ -181,7 +237,7 @@ public class GuiSearch extends GuiScreen { @Override public void drawScreen(int mouseX, int mouseY, float partialTicks) { this.drawDefaultBackground(); - this.drawCenteredString(this.fontRendererObj, EnumChatFormatting.BOLD + "Minecraft Log Search", this.width / 2, 3, 0xFFFFFF); + this.drawCenteredString(this.fontRendererObj, EnumChatFormatting.BOLD + "Minecraft Log Search", this.width / 2, 2, 0xFFFFFF); this.fieldSearchQuery.drawTextBox(); this.fieldDateStart.drawTextBox(); this.fieldDateEnd.drawTextBox(); @@ -201,6 +257,7 @@ public class GuiSearch extends GuiScreen { @Override protected void actionPerformed(GuiButton button) throws IOException { if (button == this.buttonClose && button.enabled) { + guiSearchResults = null; this.mc.setIngameFocus(); } if (isSearchInProgress || !button.enabled) { @@ -209,20 +266,53 @@ public class GuiSearch extends GuiScreen { if (button == this.buttonSearch) { setIsSearchInProgress(true); - Executors.newSingleThreadExecutor().execute(() -> { - searchResults = new LogFilesSearcher().searchFor(this.fieldSearchQuery.getText(), checkboxMatchCase.isChecked(), checkboxRemoveFormatting.isChecked(), dateStart, dateEnd); - if (searchResults.isEmpty()) { - searchResults.add(EnumChatFormatting.ITALIC + "No results"); + executorService.execute(() -> { + try { + ImmutableTriple<Integer, Integer, List<LogEntry>> searchResultsData = new LogFilesSearcher().searchFor(this.fieldSearchQuery.getText(), checkboxChatOnly.isChecked(), checkboxMatchCase.isChecked(), checkboxRemoveFormatting.isChecked(), dateStart, dateEnd); + this.searchResults = searchResultsData.right; + this.analyzedFiles = "Analyzed files: " + EnumChatFormatting.WHITE + searchResultsData.left; + this.analyzedFilesWithHits = "Files with hits: " + EnumChatFormatting.WHITE + searchResultsData.middle; + if (this.searchResults.isEmpty()) { + this.searchResults.add(new LogEntry(EnumChatFormatting.ITALIC + "No results")); + areEntriesSearchResults = false; + } else { + areEntriesSearchResults = true; + } + } catch (IOException e) { + System.err.println("Error reading/parsing file log files:"); + e.printStackTrace(); + if (e.getStackTrace().length > 0) { + searchResults.add(new LogEntry(StringUtils.replaceEach(ExceptionUtils.getStackTrace(e), new String[]{"\t", "\r\n"}, new String[]{" ", "\n"}))); + } } Minecraft.getMinecraft().addScheduledTask(() -> { - this.guiSearchResults.setResults(searchResults); + this.guiSearchResults.setResults(this.searchResults); setIsSearchInProgress(false); }); }); + } else if (button == checkboxChatOnly) { + chatOnly = checkboxChatOnly.isChecked(); } else if (button == checkboxMatchCase) { matchCase = checkboxMatchCase.isChecked(); } else if (button == checkboxRemoveFormatting) { removeFormatting = checkboxRemoveFormatting.isChecked(); + } else if (button == buttonHelp) { + this.areEntriesSearchResults = false; + this.searchResults.clear(); + this.searchResults.add(new LogEntry("" + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + "Initial setup/Configuration " + EnumChatFormatting.GRAY + EnumChatFormatting.ITALIC + "/moo config")); + this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " 1) " + EnumChatFormatting.RESET + "Configure directories that should be scanned for log files (\"Directories with Minecraft log files\")")); + this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " 2) " + EnumChatFormatting.RESET + "Set default starting date (\"Start date for log file search\")")); + this.searchResults.add(new LogEntry("" + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + "Performing a search " + EnumChatFormatting.GRAY + EnumChatFormatting.ITALIC + "/moo search")); + this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " 1) " + EnumChatFormatting.RESET + "Enter search term")); + this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " 2) " + EnumChatFormatting.RESET + "Adjust start and end date")); + this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " 3) " + EnumChatFormatting.RESET + "Select desired options (match case, ...)")); + this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " 4) " + EnumChatFormatting.RESET + "Click 'Search'")); + this.searchResults.add(new LogEntry("" + EnumChatFormatting.GOLD + EnumChatFormatting.BOLD + "Search results")); + this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " - " + EnumChatFormatting.YELLOW + "CTRL + C " + EnumChatFormatting.RESET + "to copy selected search result")); + this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " - " + EnumChatFormatting.YELLOW + "CTRL + Shift + C " + EnumChatFormatting.RESET + "to copy selected search result " + EnumChatFormatting.ITALIC + "with" + EnumChatFormatting.RESET + " formatting codes")); + this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " - " + EnumChatFormatting.YELLOW + "CTRL + A " + EnumChatFormatting.RESET + "to copy all search results")); + this.searchResults.add(new LogEntry(EnumChatFormatting.GOLD + " - " + EnumChatFormatting.YELLOW + "Double click search result " + EnumChatFormatting.RESET + "to open corresponding log file in default text editor")); + this.guiSearchResults.setResults(searchResults); } } @@ -232,15 +322,18 @@ public class GuiSearch extends GuiScreen { fieldSearchQuery.setEnabled(!isSearchInProgress); fieldDateStart.setEnabled(!isSearchInProgress); fieldDateEnd.setEnabled(!isSearchInProgress); - checkboxRemoveFormatting.enabled = !isSearchInProgress; + checkboxChatOnly.enabled = !isSearchInProgress; checkboxMatchCase.enabled = !isSearchInProgress; + checkboxRemoveFormatting.enabled = !isSearchInProgress; if (isSearchInProgress) { fieldSearchQuery.setFocused(false); fieldDateStart.setFocused(false); fieldDateEnd.setFocused(false); buttonSearch.displayString = EnumChatFormatting.ITALIC + "Searching"; searchResults.clear(); - this.guiSearchResults.clearResults(); + guiSearchResults.clearResults(); + analyzedFiles = null; + analyzedFilesWithHits = null; } else { buttonSearch.displayString = "Search"; } @@ -262,9 +355,15 @@ public class GuiSearch extends GuiScreen { */ class SearchResults extends GuiScrollingList { private final String[] spinner = new String[]{"oooooo", "Oooooo", "oOoooo", "ooOooo", "oooOoo", "ooooOo", "oooooO"}; - private List<String> rawResults; + private final DateTimeFormatter coloredDateFormatter = DateTimeFormatter.ofPattern(EnumChatFormatting.GRAY + "HH" + EnumChatFormatting.DARK_GRAY + ":" + EnumChatFormatting.GRAY + "mm" + EnumChatFormatting.DARK_GRAY + ":" + EnumChatFormatting.GRAY + "ss"); + private List<LogEntry> rawResults; private List<IChatComponent> slotsData; + /** + * key: slot id of 1st line of a search result (if multi-line-result), value: search result id + */ private NavigableMap<Integer, Integer> searchResultEntries; + private Pair<Long, String> errorMessage; + private String resultsCount; SearchResults(int marginTop) { super(GuiSearch.this.mc, @@ -287,6 +386,82 @@ public class GuiSearch extends GuiScreen { GuiSearch.this.drawCenteredString(GuiSearch.this.fontRendererObj, "Searching for '" + GuiSearch.this.searchQuery + "'", GuiSearch.this.width / 2, GuiSearch.this.height / 2, 16777215); GuiSearch.this.drawCenteredString(GuiSearch.this.fontRendererObj, spinner[(int) (Minecraft.getSystemTime() / 150L % (long) spinner.length)], GuiSearch.this.width / 2, GuiSearch.this.height / 2 + GuiSearch.this.fontRendererObj.FONT_HEIGHT * 2, 16777215); } + int hoveredSlotId = this.func_27256_c(mouseX, mouseY); + if (hoveredSlotId >= 0 && mouseY > top && mouseY < bottom) { + float scrollDistance = getScrollDistance(); + if (scrollDistance != Float.MIN_VALUE) { + // draw hovered entry details + + int hoveredSearchResultId = getSearchResultIdBySlotId(hoveredSlotId); + LogEntry hoveredEntry = getSearchResultByResultId(hoveredSearchResultId); + if (hoveredEntry != null && !hoveredEntry.isError()) { + // draw 'tooltips' in the top left corner + drawString(fontRendererObj, "Log file: ", 2, 2, 0xff888888); + GlStateManager.pushMatrix(); + float scaleFactor = 0.75f; + GL11.glScalef(scaleFactor, scaleFactor, scaleFactor); + fontRendererObj.drawSplitString(EnumChatFormatting.GRAY + Utils.toRealPath(hoveredEntry.getFilePath()), 5, (int) ((4 + fontRendererObj.FONT_HEIGHT) * (1 / scaleFactor)), (int) ((GuiSearch.this.fieldSearchQuery.xPosition - 8) * (1 / scaleFactor)), 0xff888888); + GlStateManager.popMatrix(); + drawString(fontRendererObj, "Result: " + EnumChatFormatting.WHITE + (hoveredSearchResultId + 1) + EnumChatFormatting.RESET + "/" + EnumChatFormatting.WHITE + this.rawResults.size(), 8, 48, 0xff888888); + drawString(fontRendererObj, "Time: " + hoveredEntry.getTime().format(coloredDateFormatter), 8, 58, 0xff888888); + } + + // formula from GuiScrollingList#drawScreen slotTop + int baseY = this.top + /* border: */4 - (int) scrollDistance; + + // highlight multiline search results + Integer resultIndexStart = searchResultEntries.floorKey(hoveredSlotId); + Integer resultIndexEnd = searchResultEntries.higherKey(hoveredSlotId); + + if (resultIndexStart == null) { + return; + } else if (resultIndexEnd == null) { + // last result entry + resultIndexEnd = getSize(); + } + + int slotTop = baseY + resultIndexStart * this.slotHeight - 2; + int slotBottom = baseY + resultIndexEnd * this.slotHeight - 2; + drawRect(this.left, Math.max(slotTop, top), right - /* scrollBar: */7, Math.min(slotBottom, bottom), 0x22ffffff); + } + } else if (areEntriesSearchResults) { + if (analyzedFiles != null) { + drawString(fontRendererObj, analyzedFiles, 8, 22, 0xff888888); + } + if (analyzedFilesWithHits != null) { + drawString(fontRendererObj, analyzedFilesWithHits, 8, 32, 0xff888888); + } + if (resultsCount != null) { + drawString(fontRendererObj, resultsCount, 8, 48, 0xff888888); + } + } + if (errorMessage != null) { + if (errorMessage.first().compareTo(System.currentTimeMillis()) > 0) { + String errorText = "Error: " + EnumChatFormatting.RED + errorMessage.second(); + int stringWidth = fontRendererObj.getStringWidth(errorText); + int margin = 5; + int left = width / 2 - stringWidth / 2 - margin; + int top = height / 2 - margin; + drawRect(left, top, left + stringWidth + 2 * margin, top + fontRendererObj.FONT_HEIGHT + 2 * margin, 0xff000000); + drawCenteredString(fontRendererObj, errorText,/* 2, 30*/width / 2, height / 2, 0xffDD1111); + } else { + errorMessage = null; + } + } + } + + private float getScrollDistance() { + Field scrollDistanceField = FieldUtils.getField(GuiScrollingList.class, "scrollDistance", true); + if (scrollDistanceField == null) { + // scrollDistance field not found in class GuiScrollingList + return Float.MIN_VALUE; + } + try { + return (float) scrollDistanceField.get(this); + } catch (IllegalAccessException e) { + e.printStackTrace(); + return Float.MIN_VALUE; + } } @Override @@ -296,6 +471,46 @@ public class GuiSearch extends GuiScreen { @Override protected void elementClicked(int index, boolean doubleClick) { + if (doubleClick) { + int searchResultIdBySlotId = getSearchResultIdBySlotId(index); + LogEntry searchResult = rawResults.get(searchResultIdBySlotId); + if (searchResult.getFilePath() == null) { + setErrorMessage("This log entry is not from a file"); + return; + } + byte[] buffer = new byte[1024]; + String logFileName = Utils.toRealPath(searchResult.getFilePath()); + try (GZIPInputStream logFileGzipped = new GZIPInputStream(new FileInputStream(logFileName)); + FileOutputStream logFileUnGzipped = new FileOutputStream(mcLogOutputFile)) { + String newLine = System.getProperty("line.separator"); + logFileUnGzipped.write(("# Original filename: " + logFileName + newLine + "# Use CTRL + F to search for specific words" + newLine + newLine).getBytes()); + int len; + while ((len = logFileGzipped.read(buffer)) > 0) { + logFileUnGzipped.write(buffer, 0, len); + } + } catch (IOException e) { + e.printStackTrace(); + } + + try { + Desktop.getDesktop().open(mcLogOutputFile); + System.out.println("Opened " + mcLogOutputFile); + } catch (IOException e) { + setErrorMessage("File extension .txt has no associated default editor"); + e.printStackTrace(); + } catch (IllegalArgumentException e) { + setErrorMessage(e.getMessage()); // The file: <path> doesn't exist. + e.printStackTrace(); + } catch (UnsupportedOperationException e) { + setErrorMessage("Can't open files on this OS"); + e.printStackTrace(); + } + } + } + + private void setErrorMessage(String errorMessage) { + int showDuration = 10000; // ms + this.errorMessage = Pair.of(System.currentTimeMillis() + showDuration, errorMessage); } @Override @@ -305,12 +520,12 @@ public class GuiSearch extends GuiScreen { @Override protected void drawBackground() { - } @Override protected void drawSlot(int slotIdx, int entryRight, int slotTop, int slotBuffer, Tessellator tess) { - if (Objects.equals(searchResultEntries.floorKey(selectedIndex), searchResultEntries.floorKey(slotIdx))) { + int drawnResultIndex = searchResultEntries.floorKey(slotIdx); + if (Objects.equals(searchResultEntries.floorKey(selectedIndex), drawnResultIndex)) { // highlight all lines of selected entry drawRect(this.left, slotTop - 2, entryRight, slotTop + slotHeight - 2, 0x99000000); } @@ -323,37 +538,56 @@ public class GuiSearch extends GuiScreen { } } - private void setResults(List<String> searchResult) { + private void setResults(List<LogEntry> searchResult) { this.rawResults = searchResult; this.slotsData = resizeContent(searchResult); + if (GuiSearch.this.areEntriesSearchResults) { + this.resultsCount = "Results: " + EnumChatFormatting.WHITE + this.rawResults.size(); + } } private void clearResults() { this.rawResults = Collections.emptyList(); + this.resultsCount = null; this.slotsData = resizeContent(Collections.emptyList()); } - private List<IChatComponent> resizeContent(List<String> searchResults) { + private List<IChatComponent> resizeContent(List<LogEntry> searchResults) { this.searchResultEntries = new TreeMap<>(); List<IChatComponent> slotsData = new ArrayList<>(); for (int searchResultIndex = 0; searchResultIndex < searchResults.size(); searchResultIndex++) { - String searchResult = searchResults.get(searchResultIndex); + LogEntry searchResult = searchResults.get(searchResultIndex); + String searchResultEntry; + if (searchResult.isError()) { + searchResultEntry = searchResult.getMessage(); + } else { + searchResultEntry = EnumChatFormatting.DARK_GRAY + searchResult.getTime().format(DateTimeFormatter.ISO_LOCAL_DATE) + " " + EnumChatFormatting.RESET + searchResult.getMessage(); + } searchResultEntries.put(slotsData.size(), searchResultIndex); - IChatComponent chat = ForgeHooks.newChatWithLinks(searchResult, false); - List<IChatComponent> multilineResult = GuiUtilRenderComponents.splitText(chat, this.listWidth - 8, GuiSearch.this.fontRendererObj, false, true); + List<IChatComponent> multilineResult = GuiUtilRenderComponents.splitText(new ChatComponentText(searchResultEntry), this.listWidth - 8, GuiSearch.this.fontRendererObj, false, true); slotsData.addAll(multilineResult); } return slotsData; } - String getSelectedSearchResult() { - Map.Entry<Integer, Integer> selectedResultIndex = searchResultEntries.floorEntry(selectedIndex); - return (selectedResultIndex != null && selectedResultIndex.getValue() < rawResults.size()) ? rawResults.get(selectedResultIndex.getValue()) : null; + LogEntry getSelectedSearchResult() { + int searchResultId = getSearchResultIdBySlotId(selectedIndex); + return getSearchResultByResultId(searchResultId); + } + + private LogEntry getSearchResultByResultId(int searchResultId) { + return (searchResultId >= 0 && searchResultId < rawResults.size()) ? rawResults.get(searchResultId) : null; + } + + private int getSearchResultIdBySlotId(int slotId) { + Map.Entry<Integer, Integer> searchResultIds = searchResultEntries.floorEntry(slotId); + return searchResultIds != null ? searchResultIds.getValue() : -1; } String getAllSearchResults() { - return Joiner.on('\n').join(rawResults); + return rawResults.stream().map(logEntry -> EnumChatFormatting.getTextWithoutFormattingCodes(logEntry.getMessage())) + .collect(Collectors.joining("\n")); } } } diff --git a/src/main/java/eu/olli/cowmoonication/search/LogFilesSearcher.java b/src/main/java/eu/olli/cowmoonication/search/LogFilesSearcher.java index 50d7c85..a8ebef8 100644 --- a/src/main/java/eu/olli/cowmoonication/search/LogFilesSearcher.java +++ b/src/main/java/eu/olli/cowmoonication/search/LogFilesSearcher.java @@ -1,81 +1,146 @@ package eu.olli.cowmoonication.search; import eu.olli.cowmoonication.config.MooConfig; +import eu.olli.cowmoonication.data.LogEntry; import net.minecraft.util.EnumChatFormatting; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.commons.lang3.tuple.ImmutableTriple; import java.io.*; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Comparator; import java.util.List; +import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import java.util.zip.GZIPInputStream; class LogFilesSearcher { - private static final Pattern UTF_PARAGRAPH_SYMBOL = Pattern.compile("§"); + /** + * Log4j.xml PatternLayout: [%d{HH:mm:ss}] [%t/%level]: %msg%n + * Log line: [TIME] [THREAD/LEVEL]: [CHAT] msg + * examples: + * - [13:33:37] [Client thread/INFO]: [CHAT] Hello World + * - [08:15:42] [Client thread/ERROR]: Item entity 9001 has no item?! + */ + private static final Pattern LOG4J_PATTERN = Pattern.compile("^\\[(?<timeHours>[\\d]{2}):(?<timeMinutes>[\\d]{2}):(?<timeSeconds>[\\d]{2})] \\[(?<thread>[^/]+)/(?<logLevel>[A-Z]+)]:(?<isChat> \\[CHAT])? (?<message>.*)$"); + private int analyzedFilesWithHits = 0; - List<String> searchFor(String searchQuery, boolean matchCase, boolean removeFormatting, LocalDate dateStart, LocalDate dateEnd) { - List<String> files = new ArrayList<>(); + ImmutableTriple<Integer, Integer, List<LogEntry>> searchFor(String searchQuery, boolean chatOnly, boolean matchCase, boolean removeFormatting, LocalDate dateStart, LocalDate dateEnd) throws IOException { + List<Path> files = new ArrayList<>(); for (String logsDirPath : MooConfig.logsDirs) { File logsDir = new File(logsDirPath); if (logsDir.exists() && logsDir.isDirectory()) { try { files.addAll(fileList(logsDir, dateStart, dateEnd)); } catch (IOException e) { - return printErrors(logsDirPath, e); + throw throwIoException(logsDirPath, e); } } } if (files.isEmpty()) { - List<String> errors = new ArrayList<>(); - errors.add(EnumChatFormatting.DARK_RED + "ERROR: Couldn't find any Minecraft log files. Please check if the log file directories are set correctly (/moo config)."); - return errors; + throw new FileNotFoundException(EnumChatFormatting.DARK_RED + "ERROR: Couldn't find any Minecraft log files. Please check if the log file directories are set correctly (/moo config)."); } else { - return analyseFiles(files, searchQuery, matchCase, removeFormatting); + List<LogEntry> searchResults = analyzeFiles(files, searchQuery, chatOnly, matchCase, removeFormatting) + .stream().sorted(Comparator.comparing(LogEntry::getTime)).collect(Collectors.toList()); + return new ImmutableTriple<>(files.size(), analyzedFilesWithHits, searchResults); } } - private List<String> analyseFiles(List<String> files, String searchTerm, boolean matchCase, boolean removeFormatting) { - List<String> searchResults = new ArrayList<>(); - for (String file : files) { - try (BufferedReader in = new BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream(file))))) { + private List<LogEntry> analyzeFiles(List<Path> paths, String searchTerm, boolean chatOnly, boolean matchCase, boolean removeFormatting) throws IOException { + List<LogEntry> searchResults = new ArrayList<>(); + for (Path path : paths) { + boolean foundSearchTermInFile = false; + try (BufferedReader in = new BufferedReader(new InputStreamReader(new GZIPInputStream(new FileInputStream(path.toFile()))))) { + String fileName = path.getFileName().toString(); // 2020-04-20-3.log.gz + String date = fileName.substring(0, fileName.lastIndexOf('-')); String content; + LogEntry logEntry = null; while ((content = in.readLine()) != null) { - String result = analyseLine(content, searchTerm, matchCase); - if (result != null) { - if (result.contains("§")) { - result = UTF_PARAGRAPH_SYMBOL.matcher(result).replaceAll("§"); + Matcher logLineMatcher = LOG4J_PATTERN.matcher(content); + if (logLineMatcher.matches()) { // current line is a new log entry + if (logEntry != null) { + // we had a previous log entry; analyze it! + LogEntry result = analyzeLogEntry(logEntry, searchTerm, matchCase, removeFormatting); + if (result != null) { + searchResults.add(result); + foundSearchTermInFile = true; + } + logEntry = null; } - if (removeFormatting) { - result = EnumChatFormatting.getTextWithoutFormattingCodes(result); + // handle first line of new log entry + if (chatOnly && logLineMatcher.group("isChat") == null) { + // not a chat log entry, although we're only searching for chat messages, abort! + continue; } - String date = file.substring(file.lastIndexOf('\\') + 1, file.lastIndexOf('-')); - searchResults.add(EnumChatFormatting.DARK_GRAY + date + " " + EnumChatFormatting.RESET + result); + LocalDateTime dateTime = getDate(date, logLineMatcher); + logEntry = new LogEntry(dateTime, path, logLineMatcher.group("message")); + } else if (logEntry != null) { + // multiline log entry + logEntry.addLogLine(content); + } + } + if (logEntry != null) { + // end of file! analyze last log entry in file + LogEntry result = analyzeLogEntry(logEntry, searchTerm, matchCase, removeFormatting); + if (result != null) { + searchResults.add(result); + foundSearchTermInFile = true; } } + if (foundSearchTermInFile) { + analyzedFilesWithHits++; + } } catch (IOException e) { - return printErrors(file, e); + throw throwIoException(path.toString(), e); } } return searchResults; } - private String analyseLine(String logLine, String searchTerms, boolean matchCase) { - String result = logLine; + private LocalDateTime getDate(String date, Matcher logLineMatcher) { + int year = Integer.parseInt(date.substring(0, 4)); + int month = Integer.parseInt(date.substring(5, 7)); + int day = Integer.parseInt(date.substring(8, 10)); + int hour = Integer.parseInt(logLineMatcher.group(1)); + int minute = Integer.parseInt(logLineMatcher.group(2)); + int sec = Integer.parseInt(logLineMatcher.group(3)); + + return LocalDateTime.of(year, month, day, hour, minute, sec); + } + + private LogEntry analyzeLogEntry(LogEntry logEntry, String searchTerms, boolean matchCase, boolean removeFormatting) { + if (logEntry.getMessage().length() > 5000) { + // avoid ultra long log entries + return null; + } + logEntry.fixWeirdCharacters(); + + if (removeFormatting) { + logEntry.removeFormatting(); + } + String logMessage = logEntry.getMessage(); if (!matchCase) { - logLine = logLine.toLowerCase(); - searchTerms = searchTerms.toLowerCase(); + if (!StringUtils.containsIgnoreCase(logMessage, searchTerms)) { + // no result, abort + return null; + } + } else if (!logMessage.contains(searchTerms)) { + // no result, abort + return null; } - return logLine.contains(searchTerms) ? (result.contains("[Client thread/INFO]: [CHAT]") ? result.substring(40) : result) : null; + + return logEntry; } - private List<String> fileList(File directory, LocalDate startDate, LocalDate endDate) throws IOException { - List<String> fileNames = new ArrayList<>(); + private List<Path> fileList(File directory, LocalDate startDate, LocalDate endDate) throws IOException { + List<Path> fileNames = new ArrayList<>(); try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(directory.toPath())) { for (Path path : directoryStream) { if (path.toString().endsWith(".log.gz")) { @@ -85,7 +150,7 @@ class LogFilesSearcher { Integer.parseInt(fileDate[1]), Integer.parseInt(fileDate[2])); if (fileLocalDate.compareTo(startDate) >= 0 && fileLocalDate.compareTo(endDate) <= 0) { - fileNames.add(path.toString()); + fileNames.add(path); } } else { System.err.println("Error with " + path.toString()); @@ -96,12 +161,9 @@ class LogFilesSearcher { return fileNames; } - private List<String> printErrors(String file, IOException e) { - System.err.println("Error reading/parsing file: " + file); - e.printStackTrace(); - List<String> errorMessage = new ArrayList<>(); - errorMessage.add(EnumChatFormatting.DARK_RED + "ERROR: An error occurred trying to read/parse '" + EnumChatFormatting.RED + file + EnumChatFormatting.DARK_RED + "'"); - errorMessage.add(StringUtils.replaceEach(ExceptionUtils.getStackTrace(e), new String[]{"\t", "\r\n"}, new String[]{" ", "\n"})); - return errorMessage; + private IOException throwIoException(String file, IOException e) throws IOException { + IOException ioException = new IOException(EnumChatFormatting.DARK_RED + "ERROR: An error occurred trying to read/parse '" + EnumChatFormatting.RED + file + EnumChatFormatting.DARK_RED + "'"); + ioException.setStackTrace(e.getStackTrace()); + throw ioException; } } diff --git a/src/main/java/eu/olli/cowmoonication/util/Utils.java b/src/main/java/eu/olli/cowmoonication/util/Utils.java index 8eb08a2..22f32d3 100644 --- a/src/main/java/eu/olli/cowmoonication/util/Utils.java +++ b/src/main/java/eu/olli/cowmoonication/util/Utils.java @@ -6,6 +6,9 @@ import org.apache.commons.lang3.text.WordUtils; import org.apache.commons.lang3.time.DateFormatUtils; import org.apache.commons.lang3.time.DurationFormatUtils; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; @@ -55,6 +58,19 @@ public final class Utils { } } + public static String toRealPath(Path path) { + try { + return path.toRealPath().toString(); + } catch (IOException e) { + e.printStackTrace(); + return "file not found"; + } + } + + public static String toRealPath(File path) { + return toRealPath(path.toPath()); + } + /** * Formats a large number with abbreviations for each factor of a thousand (k, m, ...) * |