#1467 Fix messages verification tool task + remove empty messages in YML files

This commit is contained in:
ljacqu
2018-02-01 20:35:30 +01:00
parent f67ddb0c77
commit f44353ed4c
39 changed files with 726 additions and 858 deletions
@@ -1,13 +0,0 @@
package tools.messages;
import java.util.List;
/**
* Represents a section of one or more consecutive comment lines in a file.
*/
public class MessageFileComments extends MessageFileElement {
public MessageFileComments(List<String> lines) {
super(lines);
}
}
@@ -1,20 +0,0 @@
package tools.messages;
import java.util.Collections;
import java.util.List;
/**
* An element (a logical unit) in a messages file.
*/
public abstract class MessageFileElement {
private final List<String> lines;
protected MessageFileElement(List<String> lines) {
this.lines = Collections.unmodifiableList(lines);
}
public List<String> getLines() {
return lines;
}
}
@@ -1,132 +0,0 @@
package tools.messages;
import fr.xephi.authme.message.MessageKey;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Creates the same order of message file elements as the given default message elements,
* using the local file's own elements as much as possible and filling in elements from the
* default messages file where necessary.
* <p>
* The current implementation (of this merger and of the {@link MessageFileElementReader reader})
* has the following limitations:
* <ul>
* <li>It assumes that new comments are only ever added to the bottom of the default file.</li>
* <li>If a file only has a partial number of the comments present in the default messages file,
* the file's comments will be moved to the top. This most likely adds the comment above the
* wrong group of messages.</li>
* <li>Assumes that the text for a message only takes one line.</li>
* <li>Ignores the last comment section of a file if it is not followed by any message entry.</li>
* </ul>
*/
public class MessageFileElementMerger {
/** Ordered list of comments in the messages file. */
private final List<MessageFileComments> comments;
/** List of message entries by corresponding MessageKey. */
private final Map<MessageKey, MessageFileEntry> entries;
/**
* Ordered list of file elements of the default file. The entries of the (non-default) messages
* file are based on this.
*/
private final List<MessageFileElement> defaultFileElements;
/** Missing tags in message entries. */
private final Map<MessageKey, Collection<String>> missingTags;
/** Counter for encountered comment elements. */
private int commentsCounter = 0;
private MessageFileElementMerger(List<MessageFileElement> defaultFileElements,
List<MessageFileComments> comments,
Map<MessageKey, MessageFileEntry> entries,
Map<MessageKey, Collection<String>> missingTags) {
this.defaultFileElements = defaultFileElements;
this.comments = comments;
this.entries = entries;
this.missingTags = missingTags;
}
/**
* Returns a list of file elements that follow the order and type of the provided default file elements.
* In other words, using the list of default file elements as template and fallback, it returns the provided
* file elements in the same order and fills in default file elements if an equivalent in {@code fileElements}
* is not present.
*
* @param fileElements file elements to sort and merge
* @param defaultFileElements file elements of the default file to base the operation on
* @param missingTags list of missing tags per message key
* @return ordered and complete list of file elements
*/
public static List<MessageFileElement> mergeElements(List<MessageFileElement> fileElements,
List<MessageFileElement> defaultFileElements,
Map<MessageKey, Collection<String>> missingTags) {
List<MessageFileComments> comments = filteredStream(fileElements, MessageFileComments.class)
.collect(Collectors.toList());
Map<MessageKey, MessageFileEntry> entries = filteredStream(fileElements, MessageFileEntry.class)
.collect(Collectors.toMap(MessageFileEntry::getMessageKey, Function.identity(), (e1, e2) -> e1));
MessageFileElementMerger merger = new MessageFileElementMerger(
defaultFileElements, comments, entries, missingTags);
return merger.mergeElements();
}
private List<MessageFileElement> mergeElements() {
List<MessageFileElement> mergedElements = new ArrayList<>(defaultFileElements.size());
for (MessageFileElement element : defaultFileElements) {
if (element instanceof MessageFileComments) {
mergedElements.add(getCommentsEntry((MessageFileComments) element));
} else if (element instanceof MessageFileEntry) {
mergedElements.add(getEntryForDefaultMessageEntry((MessageFileEntry) element));
} else {
throw new IllegalStateException("Found element of unknown subtype '" + element.getClass() + "'");
}
}
return mergedElements;
}
private MessageFileComments getCommentsEntry(MessageFileComments defaultComments) {
if (comments.size() > commentsCounter) {
MessageFileComments localComments = comments.get(commentsCounter);
++commentsCounter;
return localComments;
}
return defaultComments;
}
private MessageFileElement getEntryForDefaultMessageEntry(MessageFileEntry entry) {
MessageKey messageKey = entry.getMessageKey();
if (messageKey == null) {
throw new IllegalStateException("Default message file should not have unknown entries, but "
+ " entry with lines '" + entry.getLines() + "' has message key = null");
}
MessageFileEntry localEntry = entries.get(messageKey);
if (localEntry == null) {
return entry.convertToMissingEntryComment();
}
Collection<String> absentTags = missingTags.get(messageKey);
return absentTags == null
? localEntry
: localEntry.convertToEntryWithMissingTagsComment(absentTags);
}
/**
* Creates a stream of the entries in {@code collection} with only the elements which are of type {@code clazz}.
*
* @param collection the collection to stream over
* @param clazz the class to restrict the elements to
* @param <P> the collection type (parent)
* @param <C> the type to restrict to (child)
* @return stream over all elements of the given type
*/
private static <P, C extends P> Stream<C> filteredStream(Collection<P> collection, Class<C> clazz) {
return collection.stream().filter(clazz::isInstance).map(clazz::cast);
}
}
@@ -1,77 +0,0 @@
package tools.messages;
import tools.utils.FileIoUtils;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static com.google.common.base.Preconditions.checkArgument;
/**
* Reads a messages file and returns the lines as corresponding {@link MessageFileElement} objects.
*
* @see MessageFileElementMerger
*/
public class MessageFileElementReader {
private final List<MessageFileElement> elements = new ArrayList<>();
private MessageFileElementReader() {
}
/**
* Returns the message files as separate {@link MessageFileElement elements}.
*
* @param file the file to read
* @return the file's elements
*/
public static List<MessageFileElement> readFileIntoElements(File file) {
checkArgument(file.exists(), "Template file '" + file + "' must exist");
MessageFileElementReader reader = new MessageFileElementReader();
reader.loadElements(file.toPath());
return reader.elements;
}
private void loadElements(Path path) {
List<String> currentCommentSection = new ArrayList<>(10);
for (String line : FileIoUtils.readLinesFromFile(path)) {
if (isCommentLine(line)) {
currentCommentSection.add(line);
} else {
if (!currentCommentSection.isEmpty()) {
processTempCommentsList(currentCommentSection);
}
if (MessageFileEntry.isMessageEntry(line)) {
elements.add(new MessageFileEntry(line));
} else if (!isTodoComment(line)) {
throw new IllegalStateException("Could not match line '" + line + "' to any type");
}
}
}
}
/**
* Creates a message file comments element for one or more read comment lines. Does not add
* a comments element if the read lines are only empty lines.
*
* @param comments the read comment lines
*/
private void processTempCommentsList(List<String> comments) {
if (comments.stream().anyMatch(c -> !c.trim().isEmpty())) {
elements.add(new MessageFileComments(new ArrayList<>(comments)));
}
comments.clear();
}
private static boolean isCommentLine(String line) {
return !isTodoComment(line)
&& (line.trim().isEmpty() || line.trim().startsWith("#"));
}
private static boolean isTodoComment(String line) {
return line.startsWith("# TODO");
}
}
@@ -1,85 +0,0 @@
package tools.messages;
import fr.xephi.authme.message.MessageKey;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static java.util.Collections.singletonList;
/**
* Entry in a message file for a message key.
*/
public class MessageFileEntry extends MessageFileElement {
private static final Pattern MESSAGE_ENTRY_REGEX = Pattern.compile("([a-zA-Z_-]+): .*");
private final MessageKey messageKey;
public MessageFileEntry(String line) {
this(singletonList(line), extractMessageKey(line));
}
private MessageFileEntry(List<String> lines, MessageKey messageKey) {
super(lines);
this.messageKey = messageKey;
}
public static boolean isMessageEntry(String line) {
return MESSAGE_ENTRY_REGEX.matcher(line).matches();
}
public MessageKey getMessageKey() {
return messageKey;
}
/**
* Based on this entry, creates a comments element indicating that this message is missing.
*
* @return comments element based on this message element
*/
public MessageFileComments convertToMissingEntryComment() {
List<String> comments = getLines().stream().map(l -> "# TODO " + l).collect(Collectors.toList());
return new MessageFileComments(comments);
}
/**
* Creates an adapted message file entry object with a comment for missing tags.
*
* @param missingTags the tags missing in the message
* @return message file entry with verification comment
*/
public MessageFileEntry convertToEntryWithMissingTagsComment(Collection<String> missingTags) {
List<String> lines = new ArrayList<>(getLines().size() + 1);
lines.add("# TODO: Missing tags " + String.join(", ", missingTags));
lines.addAll(getLines());
return new MessageFileEntry(lines, messageKey);
}
/**
* Returns the {@link MessageKey} this entry is for. Returns {@code null} if the message key could not be matched.
*
* @param line the line to process
* @return the associated message key, or {@code null} if no match was found
*/
private static MessageKey extractMessageKey(String line) {
Matcher matcher = MESSAGE_ENTRY_REGEX.matcher(line);
if (matcher.find()) {
String key = matcher.group(1);
return fromKey(key);
}
throw new IllegalStateException("Could not extract message key from line '" + line + "'");
}
private static MessageKey fromKey(String key) {
for (MessageKey messageKey : MessageKey.values()) {
if (messageKey.getKey().equals(key)) {
return messageKey;
}
}
return null;
}
}
@@ -4,6 +4,7 @@ import com.google.common.base.Preconditions;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import fr.xephi.authme.message.MessageKey;
import org.bukkit.configuration.MemorySection;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.file.YamlConfiguration;
@@ -62,6 +63,13 @@ public class MessageFileVerifier {
return missingTags;
}
/**
* @return true if the verifier has found an issue with the analyzed file, false otherwise
*/
public boolean hasErrors() {
return !missingKeys.isEmpty() || !missingTags.isEmpty() || !unknownKeys.isEmpty();
}
private void verifyKeys() {
FileConfiguration configuration = YamlConfiguration.loadConfiguration(messagesFile);
@@ -77,12 +85,16 @@ public class MessageFileVerifier {
// Check FileConfiguration for all of its keys to find unknown keys
for (String key : configuration.getValues(true).keySet()) {
if (!messageKeyExists(key)) {
if (isNotInnerNode(key, configuration) && !messageKeyExists(key)) {
unknownKeys.add(key);
}
}
}
private static boolean isNotInnerNode(String key, FileConfiguration configuration) {
return !(configuration.get(key) instanceof MemorySection);
}
private void checkTagsInMessage(MessageKey messageKey, String message) {
for (String tag : messageKey.getTags()) {
if (!message.contains(tag)) {
@@ -0,0 +1,148 @@
package tools.messages;
import ch.jalu.configme.SettingsManager;
import ch.jalu.configme.configurationdata.ConfigurationData;
import ch.jalu.configme.properties.Property;
import ch.jalu.configme.resource.PropertyResource;
import ch.jalu.configme.resource.YamlFileResource;
import fr.xephi.authme.message.updater.MessageUpdater;
import fr.xephi.authme.message.updater.MessageUpdater.MigraterYamlFileResource;
import org.bukkit.configuration.file.FileConfiguration;
import tools.utils.FileIoUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Writes to a messages file, adding comments with the default file's message where
* entries are missing.
* <p>
* This writer writes to the file twice: once with ConfigMe to ensure a proper order
* of the properties and comments, and a second time to add any custom comments that
* were at the top of the file and to separate comments by new lines (which ConfigMe
* currently doesn't support).
*/
public class MessagesFileWriter {
/** Marker used inside a text to signal that it should be a comment later on. */
private static final String COMMENT_MARKER = "::COMMENT::";
private static final Pattern SPACES_BEFORE_TEXT_PATTERN = Pattern.compile("(\\s+)\\w.*");
/** The messages file to update. */
private final File file;
/** Messages from the default file. */
private final FileConfiguration defaultFile;
private MessagesFileWriter(File file, FileConfiguration defaultFile) {
this.file = file;
this.defaultFile = defaultFile;
}
public static void writeToFileWithCommentsFromDefault(File file, FileConfiguration configuration) {
new MessagesFileWriter(file, configuration).performWrite();
}
private void performWrite() {
// Store initial comments so we can add them back later
List<String> initialComments = getInitialUserComments();
// Create property resource with new defaults, save with ConfigMe for proper sections & comments
PropertyResource resource = createPropertyResourceWithCommentEntries();
new SettingsManager(resource, null, MessageUpdater.CONFIGURATION_DATA).save();
// Go through the newly saved file and replace texts with comment marker to actual YAML comments
// and add initial comments back to the file
rewriteToFileWithComments(initialComments);
}
/**
* @return any custom comments at the top of the file, for later usage
*/
private List<String> getInitialUserComments() {
final List<String> initialComments = new ArrayList<>();
final String firstCommentByConfigMe = getFirstCommentByConfigMe();
for (String line : FileIoUtils.readLinesFromFile(file.toPath())) {
if (line.isEmpty() || line.startsWith("#") && !line.equals(firstCommentByConfigMe)) {
initialComments.add(line);
} else {
break;
}
}
// Small fix: so we can keep running this writer and get the same result, we need to make sure that any ending
// empty lines are removed
for (int i = initialComments.size() - 1; i >= 0; --i) {
if (initialComments.get(i).isEmpty()) {
initialComments.remove(i);
} else {
break;
}
}
return initialComments;
}
/**
* @return the first comment generated by ConfigMe (comment of the first root path)
*/
private static String getFirstCommentByConfigMe() {
ConfigurationData configurationData = MessageUpdater.CONFIGURATION_DATA;
String firstRootPath = configurationData.getProperties().get(0).getPath().split("\\.")[0];
return "# " + configurationData.getCommentsForSection(firstRootPath)[0];
}
/**
* @return generated {@link PropertyResource} with missing entries taken from the default file and marked
* with the {@link #COMMENT_MARKER}
*/
private PropertyResource createPropertyResourceWithCommentEntries() {
YamlFileResource resource = new MigraterYamlFileResource(file);
for (Property<?> property : MessageUpdater.CONFIGURATION_DATA.getProperties()) {
String text = resource.getString(property.getPath());
if (text == null) {
resource.setValue(property.getPath(), COMMENT_MARKER + defaultFile.getString(property.getPath()));
}
}
return resource;
}
/**
* Writes to the file again, adding the provided initial comments at the top of the file and converting
* any entries marked with {@link #COMMENT_MARKER} to YAML comments.
*
* @param initialComments the comments at the top of the file to add back
*/
private void rewriteToFileWithComments(List<String> initialComments) {
List<String> newLines = new ArrayList<>(initialComments);
for (String line : FileIoUtils.readLinesFromFile(file.toPath())) {
if (line.contains(COMMENT_MARKER)) {
String lineAsYamlComment = convertLineWithCommentMarkerToYamlComment(line);
newLines.add(lineAsYamlComment);
} else if (line.startsWith("#") && !newLines.isEmpty()) {
// ConfigMe doesn't support empty line between comments, so here we check if we have a comment that
// isn't at the very top and sneak in an empty line if so.
newLines.add("");
newLines.add(line);
} else if (!line.isEmpty()) {
// ConfigMe adds an empty line at the beginning, so check here that we don't include any empty lines...
newLines.add(line);
}
}
FileIoUtils.writeToFile(file.toPath(), String.join("\n", newLines));
}
private static String convertLineWithCommentMarkerToYamlComment(String line) {
Matcher matcher = SPACES_BEFORE_TEXT_PATTERN.matcher(line);
if (matcher.matches()) {
String spacesBefore = matcher.group(1);
return spacesBefore + "# TODO " + line.replace(COMMENT_MARKER, "").trim();
} else {
throw new IllegalStateException("Space-counting pattern unexpectedly did not match on line '" + line + "'");
}
}
}
@@ -1,9 +1,10 @@
package tools.messages;
import com.google.common.collect.Multimap;
import de.bananaco.bpermissions.imp.YamlConfiguration;
import fr.xephi.authme.message.MessageKey;
import fr.xephi.authme.util.StringUtils;
import tools.utils.FileIoUtils;
import org.bukkit.configuration.file.FileConfiguration;
import tools.utils.ToolTask;
import tools.utils.ToolsConstants;
@@ -15,7 +16,6 @@ import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static tools.utils.FileIoUtils.listFilesOrThrow;
@@ -54,9 +54,9 @@ public final class VerifyMessagesTask implements ToolTask {
messageFiles = Collections.singletonList(customFile);
}
List<MessageFileElement> defaultFileElements = null;
FileConfiguration defaultFileConfiguration = null;
if (addMissingKeys) {
defaultFileElements = MessageFileElementReader.readFileIntoElements(new File(DEFAULT_MESSAGES_FILE));
defaultFileConfiguration = YamlConfiguration.loadConfiguration(new File(DEFAULT_MESSAGES_FILE));
}
// Verify the given files
@@ -65,7 +65,7 @@ public final class VerifyMessagesTask implements ToolTask {
MessageFileVerifier verifier = new MessageFileVerifier(file);
if (addMissingKeys) {
outputVerificationResults(verifier);
updateMessagesFile(file, verifier, defaultFileElements);
updateMessagesFile(file, verifier, defaultFileConfiguration);
} else {
outputVerificationResults(verifier);
}
@@ -104,18 +104,13 @@ public final class VerifyMessagesTask implements ToolTask {
*
* @param file the file to update
* @param verifier the verifier whose results should be used
* @param defaultFileElements default file elements to base the new file structure on
* @param defaultConfiguration default file configuration to retrieve missing texts from
*/
private static void updateMessagesFile(File file, MessageFileVerifier verifier,
List<MessageFileElement> defaultFileElements) {
List<MessageFileElement> messageFileElements = MessageFileElementReader.readFileIntoElements(file);
String newMessageFileContents = MessageFileElementMerger
.mergeElements(messageFileElements, defaultFileElements, verifier.getMissingTags().asMap())
.stream()
.map(MessageFileElement::getLines)
.flatMap(List::stream)
.collect(Collectors.joining("\n"));
FileIoUtils.writeToFile(file.toPath(), newMessageFileContents + "\n");
FileConfiguration defaultConfiguration) {
if (verifier.hasErrors()) {
MessagesFileWriter.writeToFileWithCommentsFromDefault(file, defaultConfiguration);
}
}