diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftCommandBlock.java b/src/main/java/org/bukkit/craftbukkit/block/CraftCommandBlock.java index 5c9bfe951..791500bd0 100644 --- a/src/main/java/org/bukkit/craftbukkit/block/CraftCommandBlock.java +++ b/src/main/java/org/bukkit/craftbukkit/block/CraftCommandBlock.java @@ -8,9 +8,6 @@ import org.bukkit.craftbukkit.util.CraftChatMessage; public class CraftCommandBlock extends CraftBlockEntityState implements CommandBlock { - private String command; - private String name; - public CraftCommandBlock(Block block) { super(block, TileEntityCommand.class); } @@ -19,39 +16,23 @@ public class CraftCommandBlock extends CraftBlockEntityState super(material, te); } - @Override - public void load(TileEntityCommand commandBlock) { - super.load(commandBlock); - - command = commandBlock.getCommandBlock().getCommand(); - name = CraftChatMessage.fromComponent(commandBlock.getCommandBlock().getName()); - } - @Override public String getCommand() { - return command; + return getSnapshot().getCommandBlock().getCommand(); } @Override public void setCommand(String command) { - this.command = command != null ? command : ""; + getSnapshot().getCommandBlock().setCommand(command != null ? command : ""); } @Override public String getName() { - return name; + return CraftChatMessage.fromComponent(getSnapshot().getCommandBlock().getName()); } @Override public void setName(String name) { - this.name = name != null ? name : "@"; - } - - @Override - public void applyTo(TileEntityCommand commandBlock) { - super.applyTo(commandBlock); - - commandBlock.getCommandBlock().setCommand(command); - commandBlock.getCommandBlock().setName(CraftChatMessage.fromStringOrNull(name)); + getSnapshot().getCommandBlock().setName(CraftChatMessage.fromStringOrNull(name != null ? name : "@")); } } diff --git a/src/main/java/org/bukkit/craftbukkit/block/CraftSign.java b/src/main/java/org/bukkit/craftbukkit/block/CraftSign.java index 15022ada0..81f6bf553 100644 --- a/src/main/java/org/bukkit/craftbukkit/block/CraftSign.java +++ b/src/main/java/org/bukkit/craftbukkit/block/CraftSign.java @@ -12,8 +12,9 @@ import org.bukkit.craftbukkit.util.CraftChatMessage; public class CraftSign extends CraftBlockEntityState implements Sign { - private String[] lines; - private boolean editable; + // Lazily initialized only if requested: + private String[] originalLines = null; + private String[] lines = null; public CraftSign(final Block block) { super(block, TileEntitySign.class); @@ -23,38 +24,37 @@ public class CraftSign extends CraftBlockEntityState implements super(material, te); } - @Override - public void load(TileEntitySign sign) { - super.load(sign); - - lines = new String[sign.lines.length]; - System.arraycopy(revertComponents(sign.lines), 0, lines, 0, lines.length); - editable = sign.isEditable; - } - @Override public String[] getLines() { + if (lines == null) { + // Lazy initialization: + TileEntitySign sign = this.getSnapshot(); + lines = new String[sign.lines.length]; + System.arraycopy(revertComponents(sign.lines), 0, lines, 0, lines.length); + originalLines = new String[lines.length]; + System.arraycopy(lines, 0, originalLines, 0, originalLines.length); + } return lines; } @Override public String getLine(int index) throws IndexOutOfBoundsException { - return lines[index]; + return getLines()[index]; } @Override public void setLine(int index, String line) throws IndexOutOfBoundsException { - lines[index] = line; + getLines()[index] = line; } @Override public boolean isEditable() { - return this.editable; + return getSnapshot().isEditable; } @Override public void setEditable(boolean editable) { - this.editable = editable; + getSnapshot().isEditable = editable; } @Override @@ -71,9 +71,15 @@ public class CraftSign extends CraftBlockEntityState implements public void applyTo(TileEntitySign sign) { super.applyTo(sign); - IChatBaseComponent[] newLines = sanitizeLines(lines); - System.arraycopy(newLines, 0, sign.lines, 0, 4); - sign.isEditable = editable; + if (lines != null) { + for (int i = 0; i < lines.length; i++) { + String line = (lines[i] == null) ? "" : lines[i]; + if (line.equals(originalLines[i])) { + continue; // The line contents are still the same, skip. + } + sign.lines[i] = CraftChatMessage.fromString(line)[0]; + } + } } public static IChatBaseComponent[] sanitizeLines(String[] lines) { diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java index 71b5d0a5b..f9d90a454 100644 --- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java @@ -14,8 +14,6 @@ import java.util.stream.Collectors; import javax.annotation.Nullable; import net.minecraft.server.BlockPosition; import net.minecraft.server.BlockPropertyInstrument; -import net.minecraft.server.ChatMessage; -import net.minecraft.server.ChatModifier; import net.minecraft.server.Container; import net.minecraft.server.ContainerMerchant; import net.minecraft.server.DamageSource; @@ -1290,10 +1288,6 @@ public class CraftEventFactory { itemInHand.setItem(Items.WRITTEN_BOOK); } CraftMetaBook meta = (CraftMetaBook) editBookEvent.getNewBookMeta(); - List pages = meta.pages; - for (int i = 0; i < pages.size(); i++) { - pages.set(i, stripEvents(pages.get(i))); - } CraftItemStack.setItemMeta(itemInHand, meta); } } @@ -1301,31 +1295,6 @@ public class CraftEventFactory { return itemInHand; } - private static IChatBaseComponent stripEvents(IChatBaseComponent c) { - ChatModifier modi = c.getChatModifier(); - if (modi != null) { - modi = modi.setChatClickable(null); - modi = modi.setChatHoverable(null); - } - if (c instanceof ChatMessage) { - ChatMessage cm = (ChatMessage) c; - Object[] oo = cm.getArgs(); - for (int i = 0; i < oo.length; i++) { - Object o = oo[i]; - if (o instanceof IChatBaseComponent) { - oo[i] = stripEvents((IChatBaseComponent) o); - } - } - } - List ls = c.getSiblings(); - if (ls != null) { - for (int i = 0; i < ls.size(); i++) { - ls.set(i, stripEvents(ls.get(i))); - } - } - return c.mutableCopy().setChatModifier(modi); - } - public static PlayerUnleashEntityEvent callPlayerUnleashEntityEvent(EntityInsentient entity, EntityHuman player) { PlayerUnleashEntityEvent event = new PlayerUnleashEntityEvent(entity.getBukkitEntity(), (Player) player.getBukkitEntity()); entity.world.getServer().getPluginManager().callEvent(event); diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBook.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBook.java index 6a044c345..06fb34731 100644 --- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBook.java +++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBook.java @@ -1,12 +1,14 @@ package org.bukkit.craftbukkit.inventory; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; import com.google.common.collect.ImmutableMap.Builder; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; import net.minecraft.server.IChatBaseComponent; -import net.minecraft.server.IChatBaseComponent.ChatSerializer; import net.minecraft.server.NBTTagCompound; import net.minecraft.server.NBTTagList; import net.minecraft.server.NBTTagString; @@ -31,7 +33,11 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta { protected String title; protected String author; - public List pages = new ArrayList(); + // We store the pages in their raw original text representation. See SPIGOT-5063, SPIGOT-5350, SPIGOT-3206 + // For writable books (CraftMetaBook) the pages are stored as plain Strings. + // For written books (CraftMetaBookSigned) the pages are stored in Minecraft's JSON format. + protected List pages; // null and empty are two different states internally + protected Boolean resolved = null; protected Integer generation; CraftMetaBook(CraftMetaItem meta) { @@ -41,16 +47,36 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta { CraftMetaBook bookMeta = (CraftMetaBook) meta; this.title = bookMeta.title; this.author = bookMeta.author; - pages.addAll(bookMeta.pages); + this.resolved = bookMeta.resolved; this.generation = bookMeta.generation; + + if (bookMeta.pages != null) { + this.pages = new ArrayList(bookMeta.pages.size()); + if (meta instanceof CraftMetaBookSigned) { + if (this instanceof CraftMetaBookSigned) { + pages.addAll(bookMeta.pages); + } else { + // Convert from JSON to plain Strings: + pages.addAll(Lists.transform(bookMeta.pages, CraftChatMessage::fromJSONComponent)); + } + } else { + if (this instanceof CraftMetaBookSigned) { + // Convert from plain Strings to JSON: + // This happens for example during book signing. + for (String page : bookMeta.pages) { + // We don't insert any non-plain text features (such as clickable links) during this conversion. + IChatBaseComponent component = CraftChatMessage.fromString(page, true, true)[0]; + pages.add(CraftChatMessage.toJSON(component)); + } + } else { + pages.addAll(bookMeta.pages); + } + } + } } } CraftMetaBook(NBTTagCompound tag) { - this(tag, true); - } - - CraftMetaBook(NBTTagCompound tag, boolean handlePages) { super(tag); if (tag.hasKey(BOOK_TITLE.NBT)) { @@ -61,29 +87,32 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta { this.author = tag.getString(BOOK_AUTHOR.NBT); } - boolean resolved = false; if (tag.hasKey(RESOLVED.NBT)) { - resolved = tag.getBoolean(RESOLVED.NBT); + this.resolved = tag.getBoolean(RESOLVED.NBT); } if (tag.hasKey(GENERATION.NBT)) { generation = tag.getInt(GENERATION.NBT); } - if (tag.hasKey(BOOK_PAGES.NBT) && handlePages) { + if (tag.hasKey(BOOK_PAGES.NBT)) { NBTTagList pages = tag.getList(BOOK_PAGES.NBT, CraftMagicNumbers.NBT.TAG_STRING); + this.pages = new ArrayList(pages.size()); + boolean expectJson = (this instanceof CraftMetaBookSigned); + // Note: We explicitly check for and truncate oversized books and pages, + // because they can come directly from clients when handling book edits. for (int i = 0; i < Math.min(pages.size(), MAX_PAGES); i++) { String page = pages.getString(i); - if (resolved) { - try { - this.pages.add(ChatSerializer.a(page)); - continue; - } catch (Exception e) { - // Ignore and treat as an old book - } + // There was an issue on previous Spigot versions which would + // result in book items with pages in the wrong text + // representation. See SPIGOT-182, SPIGOT-164 + if (expectJson) { + page = CraftChatMessage.fromJSONOrStringToJSON(page, false, true, MAX_PAGE_LENGTH, false); + } else { + page = validatePage(page); } - addPage(page); + this.pages.add(page); } } } @@ -97,22 +126,35 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta { Iterable pages = SerializableMeta.getObject(Iterable.class, map, BOOK_PAGES.BUKKIT, true); if (pages != null) { + this.pages = new ArrayList(); for (Object page : pages) { if (page instanceof String) { - addPage((String) page); + internalAddPage(deserializePage((String) page)); } } } + resolved = SerializableMeta.getObject(Boolean.class, map, RESOLVED.BUKKIT, true); generation = SerializableMeta.getObject(Integer.class, map, GENERATION.BUKKIT, true); } + protected String deserializePage(String pageData) { + // We expect the page data to already be a plain String. + return validatePage(pageData); + } + + protected String convertPlainPageToData(String page) { + // Writable books store their data as plain Strings, so we don't need to convert anything. + return page; + } + + protected String convertDataToPlainPage(String pageData) { + // pageData is expected to already be a plain String. + return pageData; + } + @Override void applyToItem(NBTTagCompound itemData) { - applyToItem(itemData, true); - } - - void applyToItem(NBTTagCompound itemData, boolean handlePages) { super.applyToItem(itemData); if (hasTitle()) { @@ -123,16 +165,16 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta { itemData.setString(BOOK_AUTHOR.NBT, this.author); } - if (handlePages) { - if (hasPages()) { - NBTTagList list = new NBTTagList(); - for (IChatBaseComponent page : pages) { - list.add(NBTTagString.a(page == null ? "" : CraftChatMessage.fromComponent(page))); - } - itemData.set(BOOK_PAGES.NBT, list); + if (pages != null) { + NBTTagList list = new NBTTagList(); + for (String page : pages) { + list.add(NBTTagString.a(page)); } + itemData.set(BOOK_PAGES.NBT, list); + } - itemData.remove(RESOLVED.NBT); + if (resolved != null) { + itemData.setBoolean(RESOLVED.NBT, resolved); } if (generation != null) { @@ -146,7 +188,7 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta { } boolean isBookEmpty() { - return !(hasPages() || hasAuthor() || hasTitle()); + return !((pages != null) || hasAuthor() || hasTitle() || hasGeneration() || (resolved != null)); } @Override @@ -172,7 +214,7 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta { @Override public boolean hasPages() { - return !pages.isEmpty(); + return (pages != null) && !pages.isEmpty(); } @Override @@ -221,69 +263,98 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta { @Override public String getPage(final int page) { Validate.isTrue(isValidPage(page), "Invalid page number"); - return CraftChatMessage.fromComponent(pages.get(page - 1)); + // assert: pages != null + return convertDataToPlainPage(pages.get(page - 1)); } @Override public void setPage(final int page, final String text) { if (!isValidPage(page)) { - throw new IllegalArgumentException("Invalid page number " + page + "/" + pages.size()); + throw new IllegalArgumentException("Invalid page number " + page + "/" + getPageCount()); } + // assert: pages != null - String newText = text == null ? "" : text.length() > MAX_PAGE_LENGTH ? text.substring(0, MAX_PAGE_LENGTH) : text; - pages.set(page - 1, CraftChatMessage.fromString(newText, true)[0]); + String newText = validatePage(text); + pages.set(page - 1, convertPlainPageToData(newText)); } @Override public void setPages(final String... pages) { - this.pages.clear(); - - addPage(pages); + setPages(Arrays.asList(pages)); } @Override public void addPage(final String... pages) { for (String page : pages) { - if (this.pages.size() >= MAX_PAGES) { - return; - } - - if (page == null) { - page = ""; - } else if (page.length() > MAX_PAGE_LENGTH) { - page = page.substring(0, MAX_PAGE_LENGTH); - } - - this.pages.add(CraftChatMessage.fromString(page, true)[0]); + page = validatePage(page); + internalAddPage(convertPlainPageToData(page)); } } + String validatePage(String page) { + if (page == null) { + page = ""; + } else if (page.length() > MAX_PAGE_LENGTH) { + page = page.substring(0, MAX_PAGE_LENGTH); + } + return page; + } + + private void internalAddPage(String page) { + // asserted: page != null + if (this.pages == null) { + this.pages = new ArrayList(); + } else if (this.pages.size() >= MAX_PAGES) { + return; + } + this.pages.add(page); + } + @Override public int getPageCount() { - return pages.size(); + return (pages == null) ? 0 : pages.size(); } @Override public List getPages() { - return pages.stream().map(CraftChatMessage::fromComponent).collect(ImmutableList.toImmutableList()); + if (pages == null) return ImmutableList.of(); + return pages.stream().map(this::convertDataToPlainPage).collect(ImmutableList.toImmutableList()); } @Override public void setPages(List pages) { - this.pages.clear(); + if (pages.isEmpty()) { + this.pages = null; + return; + } + + if (this.pages != null) { + this.pages.clear(); + } for (String page : pages) { addPage(page); } } private boolean isValidPage(int page) { - return page > 0 && page <= pages.size(); + return page > 0 && page <= getPageCount(); + } + + // TODO Expose this attribute in Bukkit? + public boolean isResolved() { + return (resolved == null) ? false : resolved; + } + + public void setResolved(boolean resolved) { + this.resolved = resolved; } @Override public CraftMetaBook clone() { CraftMetaBook meta = (CraftMetaBook) super.clone(); - meta.pages = new ArrayList(pages); + if (this.pages != null) { + meta.pages = new ArrayList(this.pages); + } return meta; } @@ -297,9 +368,12 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta { if (hasAuthor()) { hash = 61 * hash + 13 * this.author.hashCode(); } - if (hasPages()) { + if (this.pages != null) { hash = 61 * hash + 17 * this.pages.hashCode(); } + if (this.resolved != null) { + hash = 61 * hash + 17 * this.resolved.hashCode(); + } if (hasGeneration()) { hash = 61 * hash + 19 * this.generation.hashCode(); } @@ -316,7 +390,8 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta { return (hasTitle() ? that.hasTitle() && this.title.equals(that.title) : !that.hasTitle()) && (hasAuthor() ? that.hasAuthor() && this.author.equals(that.author) : !that.hasAuthor()) - && (hasPages() ? that.hasPages() && this.pages.equals(that.pages) : !that.hasPages()) + && (Objects.equals(this.pages, that.pages)) + && (Objects.equals(this.resolved, that.resolved)) && (hasGeneration() ? that.hasGeneration() && this.generation.equals(that.generation) : !that.hasGeneration()); } return true; @@ -339,12 +414,12 @@ public class CraftMetaBook extends CraftMetaItem implements BookMeta { builder.put(BOOK_AUTHOR.BUKKIT, author); } - if (hasPages()) { - List pagesString = new ArrayList(); - for (IChatBaseComponent comp : pages) { - pagesString.add(CraftChatMessage.fromComponent(comp)); - } - builder.put(BOOK_PAGES.BUKKIT, pagesString); + if (pages != null) { + builder.put(BOOK_PAGES.BUKKIT, ImmutableList.copyOf(pages)); + } + + if (resolved != null) { + builder.put(RESOLVED.BUKKIT, resolved); } if (generation != null) { diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBookSigned.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBookSigned.java index 0ea7a9606..0bd396ebe 100644 --- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBookSigned.java +++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaBookSigned.java @@ -2,15 +2,11 @@ package org.bukkit.craftbukkit.inventory; import com.google.common.collect.ImmutableMap.Builder; import java.util.Map; -import net.minecraft.server.IChatBaseComponent; -import net.minecraft.server.IChatBaseComponent.ChatSerializer; import net.minecraft.server.NBTTagCompound; -import net.minecraft.server.NBTTagList; -import net.minecraft.server.NBTTagString; import org.bukkit.Material; import org.bukkit.configuration.serialization.DelegateDeserialization; import org.bukkit.craftbukkit.inventory.CraftMetaItem.SerializableMeta; -import org.bukkit.craftbukkit.util.CraftMagicNumbers; +import org.bukkit.craftbukkit.util.CraftChatMessage; import org.bukkit.inventory.meta.BookMeta; @DelegateDeserialization(SerializableMeta.class) @@ -21,61 +17,31 @@ class CraftMetaBookSigned extends CraftMetaBook implements BookMeta { } CraftMetaBookSigned(NBTTagCompound tag) { - super(tag, false); - - boolean resolved = true; - if (tag.hasKey(RESOLVED.NBT)) { - resolved = tag.getBoolean(RESOLVED.NBT); - } - - if (tag.hasKey(BOOK_PAGES.NBT)) { - NBTTagList pages = tag.getList(BOOK_PAGES.NBT, CraftMagicNumbers.NBT.TAG_STRING); - - for (int i = 0; i < Math.min(pages.size(), MAX_PAGES); i++) { - String page = pages.getString(i); - if (resolved) { - try { - this.pages.add(ChatSerializer.a(page)); - continue; - } catch (Exception e) { - // Ignore and treat as an old book - } - } - addPage(page); - } - } + super(tag); } CraftMetaBookSigned(Map map) { super(map); } + @Override + protected String deserializePage(String pageData) { + return CraftChatMessage.fromJSONOrStringToJSON(pageData, false, true, MAX_PAGE_LENGTH, false); + } + + @Override + protected String convertPlainPageToData(String page) { + return CraftChatMessage.fromStringToJSON(page, true); + } + + @Override + protected String convertDataToPlainPage(String pageData) { + return CraftChatMessage.fromJSONComponent(pageData); + } + @Override void applyToItem(NBTTagCompound itemData) { - super.applyToItem(itemData, false); - - if (hasTitle()) { - itemData.setString(BOOK_TITLE.NBT, this.title); - } - - if (hasAuthor()) { - itemData.setString(BOOK_AUTHOR.NBT, this.author); - } - - if (hasPages()) { - NBTTagList list = new NBTTagList(); - for (IChatBaseComponent page : pages) { - list.add(NBTTagString.a( - ChatSerializer.a(page) - )); - } - itemData.set(BOOK_PAGES.NBT, list); - } - itemData.setBoolean(RESOLVED.NBT, true); - - if (generation != null) { - itemData.setInt(GENERATION.NBT, generation); - } + super.applyToItem(itemData); } @Override diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java index 340ad1809..5263e258a 100644 --- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java +++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftMetaItem.java @@ -10,7 +10,6 @@ import com.google.common.collect.Lists; import com.google.common.collect.Multimap; import com.google.common.collect.SetMultimap; import com.google.common.collect.Sets; -import com.google.gson.JsonParseException; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -31,6 +30,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Objects; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -38,7 +38,6 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; import net.minecraft.server.ChatComponentText; import net.minecraft.server.EnumItemSlot; -import net.minecraft.server.IChatBaseComponent; import net.minecraft.server.ItemBlock; import net.minecraft.server.NBTBase; import net.minecraft.server.NBTCompressedStreamTools; @@ -262,9 +261,10 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { static final ItemMetaKey BLOCK_DATA = new ItemMetaKey("BlockStateTag"); static final ItemMetaKey BUKKIT_CUSTOM_TAG = new ItemMetaKey("PublicBukkitValues"); - private IChatBaseComponent displayName; - private IChatBaseComponent locName; - private List lore; + // We store the raw original JSON representation of all text data. See SPIGOT-5063, SPIGOT-5656, SPIGOT-5304 + private String displayName; + private String locName; + private List lore; // null and empty are two different states internally private Integer customModelData; private NBTTagCompound blockData; private Map enchantments; @@ -291,8 +291,8 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { this.displayName = meta.displayName; this.locName = meta.locName; - if (meta.hasLore()) { - this.lore = new ArrayList(meta.lore); + if (meta.lore != null) { + this.lore = new ArrayList(meta.lore); } this.customModelData = meta.customModelData; @@ -326,32 +326,19 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { NBTTagCompound display = tag.getCompound(DISPLAY.NBT); if (display.hasKey(NAME.NBT)) { - try { - displayName = IChatBaseComponent.ChatSerializer.a(display.getString(NAME.NBT)); - } catch (JsonParseException ex) { - // Ignore (stripped like Vanilla) - } + displayName = display.getString(NAME.NBT); } if (display.hasKey(LOCNAME.NBT)) { - try { - locName = IChatBaseComponent.ChatSerializer.a(display.getString(LOCNAME.NBT)); - } catch (JsonParseException ex) { - // Ignore (stripped like Vanilla) - } + locName = display.getString(LOCNAME.NBT); } if (display.hasKey(LORE.NBT)) { NBTTagList list = display.getList(LORE.NBT, CraftMagicNumbers.NBT.TAG_STRING); - lore = new ArrayList(list.size()); - + lore = new ArrayList(list.size()); for (int index = 0; index < list.size(); index++) { String line = list.getString(index); - try { - lore.add(IChatBaseComponent.ChatSerializer.a(line)); - } catch (JsonParseException ex) { - // Ignore (stripped like Vanilla) - } + lore.add(line); } } } @@ -474,12 +461,13 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { } CraftMetaItem(Map map) { - setDisplayName(SerializableMeta.getString(map, NAME.BUKKIT, true)); - setLocalizedName(SerializableMeta.getString(map, LOCNAME.BUKKIT, true)); + displayName = CraftChatMessage.fromJSONOrStringOrNullToJSON(SerializableMeta.getString(map, NAME.BUKKIT, true)); + + locName = CraftChatMessage.fromJSONOrStringOrNullToJSON(SerializableMeta.getString(map, LOCNAME.BUKKIT, true)); Iterable lore = SerializableMeta.getObject(Iterable.class, map, LORE.BUKKIT, true); if (lore != null) { - safelyAdd(lore, this.lore = new ArrayList(), Integer.MAX_VALUE); + safelyAdd(lore, this.lore = new ArrayList(), true); } Integer customModelData = SerializableMeta.getObject(Integer.class, map, CUSTOM_MODEL_DATA.BUKKIT, true); @@ -615,13 +603,13 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { @Overridden void applyToItem(NBTTagCompound itemTag) { if (hasDisplayName()) { - setDisplayTag(itemTag, NAME.NBT, NBTTagString.a(CraftChatMessage.toJSON(displayName))); + setDisplayTag(itemTag, NAME.NBT, NBTTagString.a(displayName)); } if (hasLocalizedName()) { - setDisplayTag(itemTag, LOCNAME.NBT, NBTTagString.a(CraftChatMessage.toJSON(locName))); + setDisplayTag(itemTag, LOCNAME.NBT, NBTTagString.a(locName)); } - if (hasLore()) { + if (lore != null) { setDisplayTag(itemTag, LORE.NBT, createStringList(lore)); } @@ -667,15 +655,15 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { } } - NBTTagList createStringList(List list) { - if (list == null || list.isEmpty()) { + NBTTagList createStringList(List list) { + if (list == null) { return null; } NBTTagList tagList = new NBTTagList(); - for (IChatBaseComponent value : list) { + for (String value : list) { // SPIGOT-5342 - horrible hack as 0 version does not go through the Mojang updater - tagList.add(NBTTagString.a(version <= 0 || version >= 1803 ? CraftChatMessage.toJSON(value) : CraftChatMessage.fromComponent(value))); // SPIGOT-4935 + tagList.add(NBTTagString.a(version <= 0 || version >= 1803 ? value : CraftChatMessage.fromJSONComponent(value))); // SPIGOT-4935 } return tagList; @@ -750,17 +738,17 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { @Overridden boolean isEmpty() { - return !(hasDisplayName() || hasLocalizedName() || hasEnchants() || hasLore() || hasCustomModelData() || hasBlockData() || hasRepairCost() || !unhandledTags.isEmpty() || !persistentDataContainer.isEmpty() || hideFlag != 0 || isUnbreakable() || hasDamage() || hasAttributeModifiers()); + return !(hasDisplayName() || hasLocalizedName() || hasEnchants() || (lore != null) || hasCustomModelData() || hasBlockData() || hasRepairCost() || !unhandledTags.isEmpty() || !persistentDataContainer.isEmpty() || hideFlag != 0 || isUnbreakable() || hasDamage() || hasAttributeModifiers()); } @Override public String getDisplayName() { - return CraftChatMessage.fromComponent(displayName); + return CraftChatMessage.fromJSONComponent(displayName); } @Override public final void setDisplayName(String name) { - this.displayName = CraftChatMessage.fromStringOrNull(name); + this.displayName = CraftChatMessage.fromStringOrNullToJSON(name); } @Override @@ -770,12 +758,12 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { @Override public String getLocalizedName() { - return CraftChatMessage.fromComponent(locName); + return CraftChatMessage.fromJSONComponent(locName); } @Override public void setLocalizedName(String name) { - this.locName = CraftChatMessage.fromStringOrNull(name); + this.locName = CraftChatMessage.fromStringOrNullToJSON(name); } @Override @@ -883,20 +871,20 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { @Override public List getLore() { - return this.lore == null ? null : new ArrayList(Lists.transform(this.lore, CraftChatMessage::fromComponent)); + return this.lore == null ? null : new ArrayList(Lists.transform(this.lore, CraftChatMessage::fromJSONComponent)); } @Override - public void setLore(List lore) { // too tired to think if .clone is better - if (lore == null) { + public void setLore(List lore) { + if (lore == null || lore.isEmpty()) { this.lore = null; } else { if (this.lore == null) { - safelyAdd(lore, this.lore = new ArrayList(lore.size()), Integer.MAX_VALUE); + this.lore = new ArrayList(lore.size()); } else { this.lore.clear(); - safelyAdd(lore, this.lore, Integer.MAX_VALUE); } + safelyAdd(lore, this.lore, false); } } @@ -1133,7 +1121,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { return ((this.hasDisplayName() ? that.hasDisplayName() && this.displayName.equals(that.displayName) : !that.hasDisplayName())) && (this.hasLocalizedName() ? that.hasLocalizedName() && this.locName.equals(that.locName) : !that.hasLocalizedName()) && (this.hasEnchants() ? that.hasEnchants() && this.enchantments.equals(that.enchantments) : !that.hasEnchants()) - && (this.hasLore() ? that.hasLore() && this.lore.equals(that.lore) : !that.hasLore()) + && (Objects.equals(this.lore, that.lore)) && (this.hasCustomModelData() ? that.hasCustomModelData() && this.customModelData.equals(that.customModelData) : !that.hasCustomModelData()) && (this.hasBlockData() ? that.hasBlockData() && this.blockData.equals(that.blockData) : !that.hasBlockData()) && (this.hasRepairCost() ? that.hasRepairCost() && this.repairCost == that.repairCost : !that.hasRepairCost()) @@ -1166,7 +1154,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { int hash = 3; hash = 61 * hash + (hasDisplayName() ? this.displayName.hashCode() : 0); hash = 61 * hash + (hasLocalizedName() ? this.locName.hashCode() : 0); - hash = 61 * hash + (hasLore() ? this.lore.hashCode() : 0); + hash = 61 * hash + ((lore != null) ? this.lore.hashCode() : 0); hash = 61 * hash + (hasCustomModelData() ? this.customModelData.hashCode() : 0); hash = 61 * hash + (hasBlockData() ? this.blockData.hashCode() : 0); hash = 61 * hash + (hasEnchants() ? this.enchantments.hashCode() : 0); @@ -1187,7 +1175,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { try { CraftMetaItem clone = (CraftMetaItem) super.clone(); if (this.lore != null) { - clone.lore = new ArrayList(this.lore); + clone.lore = new ArrayList(this.lore); } clone.customModelData = this.customModelData; clone.blockData = this.blockData; @@ -1219,14 +1207,14 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { @Overridden ImmutableMap.Builder serialize(ImmutableMap.Builder builder) { if (hasDisplayName()) { - builder.put(NAME.BUKKIT, CraftChatMessage.fromComponent(displayName)); + builder.put(NAME.BUKKIT, displayName); } if (hasLocalizedName()) { - builder.put(LOCNAME.BUKKIT, CraftChatMessage.fromComponent(locName)); + builder.put(LOCNAME.BUKKIT, locName); } - if (hasLore()) { - builder.put(LORE.BUKKIT, ImmutableList.copyOf(Lists.transform(lore, CraftChatMessage::fromComponent))); + if (lore != null) { + builder.put(LORE.BUKKIT, ImmutableList.copyOf(lore)); } if (hasCustomModelData()) { @@ -1321,7 +1309,7 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { builder.put(key.BUKKIT, mods); } - static void safelyAdd(Iterable addFrom, Collection addTo, int maxItemLength) { + static void safelyAdd(Iterable addFrom, Collection addTo, boolean possiblyJsonInput) { if (addFrom == null) { return; } @@ -1332,15 +1320,15 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta { throw new IllegalArgumentException(addFrom + " cannot contain non-string " + object.getClass().getName()); } - addTo.add(new ChatComponentText("")); + addTo.add(CraftChatMessage.toJSON(new ChatComponentText(""))); } else { - String page = object.toString(); + String entry = object.toString(); - if (page.length() > maxItemLength) { - page = page.substring(0, maxItemLength); + if (possiblyJsonInput) { + addTo.add(CraftChatMessage.fromJSONOrStringToJSON(entry)); + } else { + addTo.add(CraftChatMessage.fromStringToJSON(entry)); } - - addTo.add(CraftChatMessage.fromString(page)[0]); } } } diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftChatMessage.java b/src/main/java/org/bukkit/craftbukkit/util/CraftChatMessage.java index e796e9561..50a85af76 100644 --- a/src/main/java/org/bukkit/craftbukkit/util/CraftChatMessage.java +++ b/src/main/java/org/bukkit/craftbukkit/util/CraftChatMessage.java @@ -2,6 +2,7 @@ package org.bukkit.craftbukkit.util; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap.Builder; +import com.google.gson.JsonParseException; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -23,6 +24,7 @@ public final class CraftChatMessage { private static final Pattern LINK_PATTERN = Pattern.compile("((?:(?:https?):\\/\\/)?(?:[-\\w_\\.]{2,}\\.[a-z]{2,4}.*?(?=[\\.\\?!,;:]?(?:[" + String.valueOf(org.bukkit.ChatColor.COLOR_CHAR) + " \\n]|$))))"); private static final Map formatMap; + private static final String COLOR_CHAR_STRING = String.valueOf(ChatColor.COLOR_CHAR); static { Builder builder = ImmutableMap.builder(); @@ -55,7 +57,7 @@ public final class CraftChatMessage { private StringBuilder hex; private final String message; - private StringMessage(String message, boolean keepNewlines) { + private StringMessage(String message, boolean keepNewlines, boolean plain) { this.message = message; if (message == null) { output = new IChatBaseComponent[]{currentChatComponent}; @@ -116,12 +118,16 @@ public final class CraftChatMessage { needsAdd = true; break; case 2: - if (!(match.startsWith("http://") || match.startsWith("https://"))) { - match = "http://" + match; + if (plain) { + appendNewComponent(matcher.end(groupId)); + } else { + if (!(match.startsWith("http://") || match.startsWith("https://"))) { + match = "http://" + match; + } + modifier = modifier.setChatClickable(new ChatClickable(EnumClickAction.OPEN_URL, match)); + appendNewComponent(matcher.end(groupId)); + modifier = modifier.setChatClickable((ChatClickable) null); } - modifier = modifier.setChatClickable(new ChatClickable(EnumClickAction.OPEN_URL, match)); - appendNewComponent(matcher.end(groupId)); - modifier = modifier.setChatClickable((ChatClickable) null); break; case 3: if (needsAdd) { @@ -168,13 +174,135 @@ public final class CraftChatMessage { } public static IChatBaseComponent[] fromString(String message, boolean keepNewlines) { - return new StringMessage(message, keepNewlines).getOutput(); + return fromString(message, keepNewlines, false); + } + + public static IChatBaseComponent[] fromString(String message, boolean keepNewlines, boolean plain) { + return new StringMessage(message, keepNewlines, plain).getOutput(); } public static String toJSON(IChatBaseComponent component) { return IChatBaseComponent.ChatSerializer.a(component); } + public static String toJSONOrNull(IChatBaseComponent component) { + if (component == null) return null; + return toJSON(component); + } + + public static IChatBaseComponent fromJSON(String jsonMessage) throws JsonParseException { + // Note: This also parses plain Strings to text components. + return IChatBaseComponent.ChatSerializer.a(jsonMessage); + } + + public static IChatBaseComponent fromJSONOrNull(String jsonMessage) { + // Note: An empty message is parsed to an empty text component instead of null. + if (jsonMessage == null) return null; + try { + return fromJSON(jsonMessage); + } catch (JsonParseException ex) { + return null; + } + } + + public static IChatBaseComponent fromJSONOrString(String message) { + return fromJSONOrString(message, false); + } + + public static IChatBaseComponent fromJSONOrString(String message, boolean keepNewlines) { + return fromJSONOrString(message, false, keepNewlines); + } + + private static IChatBaseComponent fromJSONOrString(String message, boolean nullable, boolean keepNewlines) { + if (message == null) message = ""; + if (nullable && message.isEmpty()) return null; + // If the message contains color codes, we convert it ourselves: + if (containsColorCodes(message)) { + return fromString(message, keepNewlines)[0]; + } else { + try { + return fromJSON(message); + } catch (JsonParseException ex) { + return fromString(message, keepNewlines)[0]; + } + } + } + + public static String fromJSONOrStringToJSON(String message) { + return fromJSONOrStringToJSON(message, false); + } + + public static String fromJSONOrStringToJSON(String message, boolean keepNewlines) { + return fromJSONOrStringToJSON(message, false, keepNewlines, Integer.MAX_VALUE, false); + } + + public static String fromJSONOrStringOrNullToJSON(String message) { + return fromJSONOrStringOrNullToJSON(message, false); + } + + public static String fromJSONOrStringOrNullToJSON(String message, boolean keepNewlines) { + return fromJSONOrStringToJSON(message, true, keepNewlines, Integer.MAX_VALUE, false); + } + + public static String fromJSONOrStringToJSON(String message, boolean nullable, boolean keepNewlines, int maxLength, boolean checkJsonContentLength) { + if (message == null) message = ""; + if (nullable && message.isEmpty()) return null; + // If the message contains color codes, we convert it ourselves: + if (containsColorCodes(message)) { + message = trimMessage(message, maxLength); + return fromStringToJSON(message, keepNewlines); + } else { + try { + // If the input can be parsed as JSON, we use that: + IChatBaseComponent component = fromJSON(message); + if (checkJsonContentLength) { + String content = fromComponent(component); + String trimmedContent = trimMessage(content, maxLength); + if (content != trimmedContent) { // identity comparison is fine here + // Note: The resulting text has all non-plain text features stripped. + return fromStringToJSON(trimmedContent, keepNewlines); + } + } + return message; + } catch (JsonParseException ex) { + // Else we convert the input: + message = trimMessage(message, maxLength); + return fromStringToJSON(message, keepNewlines); + } + } + } + + public static String trimMessage(String message, int maxLength) { + if (message != null && message.length() > maxLength) { + return message.substring(0, maxLength); + } else { + return message; + } + } + + public static boolean containsColorCodes(String message) { + return message != null && message.contains(COLOR_CHAR_STRING); + } + + public static String fromStringToJSON(String message) { + return fromStringToJSON(message, false); + } + + public static String fromStringToJSON(String message, boolean keepNewlines) { + IChatBaseComponent component = CraftChatMessage.fromString(message, keepNewlines)[0]; + return CraftChatMessage.toJSON(component); + } + + public static String fromStringOrNullToJSON(String message) { + IChatBaseComponent component = CraftChatMessage.fromStringOrNull(message); + return CraftChatMessage.toJSONOrNull(component); + } + + public static String fromJSONComponent(String jsonMessage) { + IChatBaseComponent component = CraftChatMessage.fromJSONOrNull(jsonMessage); + return CraftChatMessage.fromComponent(component); + } + public static String fromComponent(IChatBaseComponent component) { if (component == null) return ""; StringBuilder out = new StringBuilder(); diff --git a/src/test/java/org/bukkit/craftbukkit/util/CraftChatMessageTest.java b/src/test/java/org/bukkit/craftbukkit/util/CraftChatMessageTest.java index e4a98f441..9169a2b4d 100644 --- a/src/test/java/org/bukkit/craftbukkit/util/CraftChatMessageTest.java +++ b/src/test/java/org/bukkit/craftbukkit/util/CraftChatMessageTest.java @@ -1,6 +1,8 @@ package org.bukkit.craftbukkit.util; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import net.minecraft.server.ChatComponentText; import net.minecraft.server.IChatBaseComponent; import net.minecraft.server.IChatMutableComponent; import org.junit.Test; @@ -50,6 +52,15 @@ public class CraftChatMessageTest { testComponent("F§foo§bBar§rBaz", create("F§foo", "§bBar", "Baz")); } + @Test + public void testPlainText() { + testPlainString(""); + testPlainString("Foo§f§mBar§0"); + testPlainString("Link to https://www.spigotmc.org/ ..."); + testPlainString("Link to http://www.spigotmc.org/ ..."); + testPlainString("Link to www.spigotmc.org ..."); + } + private IChatBaseComponent create(String txt, String... rest) { IChatMutableComponent cmp = CraftChatMessage.fromString(txt, false)[0].mutableCopy(); for (String s : rest) { @@ -77,6 +88,22 @@ public class CraftChatMessageTest { assertEquals("\nComponent: " + cmp + "\n", expected, actual); } + private void testPlainString(String expected) { + IChatBaseComponent component = CraftChatMessage.fromString(expected, false, true)[0]; + String actual = CraftChatMessage.fromComponent(component); + assertEquals("fromComponent does not match input: " + component, expected, actual); + assertTrue("Non-plain component: " + component, !containsNonPlainComponent(component)); + } + + private boolean containsNonPlainComponent(IChatBaseComponent component) { + for (IChatBaseComponent c : component) { + if (!(c instanceof ChatComponentText)) { + return true; + } + } + return false; + } + private void testComponent(String expected, IChatBaseComponent cmp) { String actual = CraftChatMessage.fromComponent(cmp); assertEquals("\nComponent: " + cmp + "\n", expected, actual);