diff --git a/src/main/java/mc/protocol/dto/ServerInfo.java b/src/main/java/mc/protocol/dto/ServerInfo.java index 8a0abcd..f88ffa4 100644 --- a/src/main/java/mc/protocol/dto/ServerInfo.java +++ b/src/main/java/mc/protocol/dto/ServerInfo.java @@ -2,15 +2,16 @@ package mc.protocol.dto; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonRawValue; -import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; +import lombok.ToString; +import mc.protocol.text.Text; import java.util.List; import java.util.UUID; @JsonIgnoreProperties(ignoreUnknown = true) @Data +@ToString(exclude = "faviconBase64") public class ServerInfo { private Version version; @@ -18,8 +19,7 @@ public class ServerInfo { @JsonProperty("players") private PlayersInfo playersInfo; - //TODO необходимо реализовать объект типа Chat (см. https://wiki.vg/index.php?title=Chat&oldid=8329) - private JsonNode description; + private Text description; @JsonProperty("favicon") private String faviconBase64; diff --git a/src/main/java/mc/protocol/status/server/StatusServerResponse.java b/src/main/java/mc/protocol/status/server/StatusServerResponse.java index 4d6a6a0..899da96 100644 --- a/src/main/java/mc/protocol/status/server/StatusServerResponse.java +++ b/src/main/java/mc/protocol/status/server/StatusServerResponse.java @@ -1,12 +1,11 @@ package mc.protocol.status.server; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Data; import mc.protocol.Packet; import mc.protocol.dto.ServerInfo; import mc.protocol.io.NetInputStream; import mc.protocol.io.NetOutputStream; +import mc.protocol.utils.json.JsonUtils; /** * Status server packet, response. @@ -58,13 +57,8 @@ public class StatusServerResponse implements Packet { public ServerInfo getServerInfoDto() { if (serverInfoDto == null) { - try { - ObjectMapper mapper = new ObjectMapper(); - serverInfoDto = mapper.readValue(serverInfo, ServerInfo.class); - } catch (JsonProcessingException e) { - e.printStackTrace(); - return new ServerInfo(); - } + JsonUtils.jsonToObject(serverInfo, ServerInfo.class) + .ifPresent(serverInfoDto -> this.serverInfoDto = serverInfoDto); } return serverInfoDto; @@ -78,13 +72,7 @@ public class StatusServerResponse implements Packet { @Override public void writeSelf(NetOutputStream netOutputStream) { if (serverInfo == null) { - try { - ObjectMapper mapper = new ObjectMapper(); - serverInfo = mapper.writeValueAsString(serverInfoDto); - } catch (JsonProcessingException e) { - e.printStackTrace(); - serverInfo = "{}"; - } + serverInfo = JsonUtils.objectToJson(serverInfo); } netOutputStream.writeString(serverInfo); diff --git a/src/main/java/mc/protocol/text/Text.java b/src/main/java/mc/protocol/text/Text.java new file mode 100644 index 0000000..85f308a --- /dev/null +++ b/src/main/java/mc/protocol/text/Text.java @@ -0,0 +1,238 @@ +package mc.protocol.text; + +import com.google.common.collect.ImmutableList; +import lombok.Getter; +import lombok.ToString; + +import java.util.*; + +@Getter +@ToString +public class Text { + private static final Text EMPTY = new Text(); + private static final Text NEW_LINE = new Text("\n", null, null, null); + + private final String content; + private final TextColor color; + private final TextStyle style; + private final ImmutableList children; + + private Text() { + content = ""; + color = null; + style = null; + children = null; + } + + private Text(String content, TextColor color, TextStyle style, ImmutableList children) { + this.content = content; + this.color = color; + this.style = style; + this.children = children; + } + + public boolean isEmpty() { + boolean result = (content == null || content.isEmpty()); + + if (children != null && !children.isEmpty()) { + for (Text child : children) { + result = result && child.isEmpty(); + } + } + + return result; + } + + public String toPlain() { + if (children != null && !children.isEmpty()) { + final StringJoiner sj = new StringJoiner(""); + children.forEach(child -> sj.add(child.toPlain())); + return sj.toString(); + } else { + return content; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Text text = (Text) o; + return Objects.equals(toPlain(), text.toPlain()); + } + + @Override + public int hashCode() { + return Objects.hash(toPlain()); + } + + public static class Builder { + @Getter + private String content; + @Getter + private TextColor color; + @Getter + private TextStyle style; + private List children; + + public Builder() { + this(""); + } + + public Builder(String content) { + this.content = content; + this.color = null; + this.style = null; + this.children = new ArrayList<>(); + } + + public Builder(Object... objects) { + this.children = new ArrayList<>(); + + for(Object obj : objects) { + if (obj instanceof String) { + if (this.content == null) { + this.content = (String) obj; + } else { + this.content = this.content.concat((String) obj); + } + } else if (obj instanceof TextStyle) { + if (this.style == null) { + this.style = TextStyle.none(); + } else { + this.style.merge((TextStyle) obj); + } + } else if (obj instanceof TextColor) { + this.color = (TextColor) obj; + } else if (obj instanceof Text) { + children.add((Text) obj); + } + } + } + + public List getChildren() { + return Collections.unmodifiableList(children); + } + + public Builder color(TextColor color) { + this.color = color; + return this; + } + + public Builder style(TextStyle style) { + if (this.style == null) { + this.style = TextStyle.none(); + } else { + this.style.merge(style); + } + + return this; + } + + public Builder style(TextStyle... styles) { + if (this.style == null) { + this.style = TextStyle.none(); + } + + for(TextStyle style : styles) { + this.style.merge(style); + } + + return this; + } + + public Builder append(String string) { + return append(Text.of(string)); + } + + public Builder append(Text child) { + this.children.add(child); + return this; + } + + public Builder append(Text... children) { + Collections.addAll(this.children, children); + return this; + } + + public Text build() { + if ((children.isEmpty() || (children.size() == 1 && children.get(0) == null)) + && (content == null || content.isEmpty())) { + return Text.EMPTY; + } + + if (children.size() == 1 && children.get(0) != null) { + return children.get(0); + } else { + return new Text( + content, + color, + style, + ImmutableList.copyOf(children) + ); + } + } + } + + public static Builder builder() { + return new Builder(); + } + + public static Builder builder(String content) { + return new Builder(content); + } + + public static Builder builder(Object... objects) { + return new Builder(objects); + } + + public static Text of() { + return EMPTY; + } + + public static Text of(String string) { + if (string == null || string.isEmpty()) { + return EMPTY; + } else if (string.equals("\n")) { + return NEW_LINE; + } else { + return new Text(string, null, null, null); + } + } + + public static Text of(Object... objects) { + TextColor color = null; + TextStyle style = null; + String content = null; + + for(Object obj : objects) { + if (obj instanceof String) { + if (content == null) { + content = (String) obj; + } else { + content = content.concat((String) obj); + } + } else if (obj instanceof TextStyle) { + if (style == null) { + style = (TextStyle) obj; + } else { + style.merge((TextStyle) obj); + } + } else if (obj instanceof TextColor) { + color = (TextColor) obj; + } else if (obj != null){ + if (content == null) { + content = obj.toString(); + } else { + content = content.concat(obj.toString()); + } + } + } + + if (content == null || content.isEmpty()) { + return EMPTY; + } else { + return new Text(content, color, style, null); + } + } +} diff --git a/src/main/java/mc/protocol/text/TextColor.java b/src/main/java/mc/protocol/text/TextColor.java new file mode 100644 index 0000000..3ed5fa4 --- /dev/null +++ b/src/main/java/mc/protocol/text/TextColor.java @@ -0,0 +1,36 @@ +package mc.protocol.text; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.stream.Stream; + +@RequiredArgsConstructor +@Getter +public enum TextColor { + BLACK ("black", '0'), + DARK_BLUE ("dark_blue", '1'), + DARK_GREEN ("dark_green", '2'), + DARK_AQUA ("dark_aqua", '3'), + DARK_RED ("dark_red", '4'), + DARK_PUEPLE("dark_purple", '5'), + GOLD ("gold", '6'), + GRAY ("gray", '7'), + DARK_GRAY ("dark_gray", '8'), + BLUE ("blue", '9'), + GREEN ("green", 'a'), + AQUA ("aqua", 'b'), + RED ("red", 'c'), + PUEPLE ("light_purple",'d'), + YELLOW ("yellow", 'e'), + WHITE ("white", 'f'); + + public static TextColor valueOfColorName(String name) { + return Stream.of(TextColor.values()) + .filter(textColor -> textColor.getName().equals(name)) + .findFirst().orElse(null); + } + + private final String name; + private final char code; +} diff --git a/src/main/java/mc/protocol/text/TextStyle.java b/src/main/java/mc/protocol/text/TextStyle.java new file mode 100644 index 0000000..7537d4e --- /dev/null +++ b/src/main/java/mc/protocol/text/TextStyle.java @@ -0,0 +1,66 @@ +package mc.protocol.text; + +import lombok.Getter; +import lombok.Setter; + +import javax.annotation.Nullable; +import java.util.Optional; + +@SuppressWarnings("OptionalUsedAsFieldOrParameterType") +@Getter +@Setter +public class TextStyle { + public static final TextStyle BOLD = new TextStyle(true, null, null, null, null); + public static final TextStyle ITALIC = new TextStyle(null, true, null, null, null); + public static final TextStyle UNDERLINE = new TextStyle(null, null, true, null, null); + public static final TextStyle STRIKETHOUGH = new TextStyle(null, null, null, true, null); + public static final TextStyle OBFUSCATED = new TextStyle(null, null, null, null, true); + public static final TextStyle RESET = new TextStyle(false, false, false, false, false); + + private static class OptionalBoolean { + private static final Optional TRUE = Optional.of(true); + private static final Optional FALSE = Optional.of(false); + private static final Optional NONE = Optional.empty(); + + static Optional of(boolean bool) { + return bool ? TRUE : FALSE; + } + + static Optional of(@Nullable Boolean bool) { + if (bool != null) { + return of(bool.booleanValue()); + } + return NONE; + } + } + + private Optional bold; + private Optional italic; + private Optional underline; + private Optional strikethrough; + private Optional obfuscated; + + public TextStyle(@Nullable Boolean bold, + @Nullable Boolean italic, + @Nullable Boolean underline, + @Nullable Boolean strikethrough, + @Nullable Boolean obfuscated) { + this.bold = OptionalBoolean.of(bold); + this.italic = OptionalBoolean.of(italic); + this.underline = OptionalBoolean.of(underline); + this.strikethrough = OptionalBoolean.of(strikethrough); + this.obfuscated = OptionalBoolean.of(obfuscated); + } + + public void merge(TextStyle style) { + if (style.bold.isPresent()) this.bold = style.bold; + if (style.italic.isPresent()) this.italic = style.italic; + if (style.underline.isPresent()) this.underline = style.underline; + if (style.strikethrough.isPresent()) this.strikethrough = style.strikethrough; + if (style.obfuscated.isPresent()) this.obfuscated = style.obfuscated; + } + + public static TextStyle none() { + return new TextStyle(null,null,null,null, null); + } +} diff --git a/src/main/java/mc/protocol/text/TextTemplate.java b/src/main/java/mc/protocol/text/TextTemplate.java new file mode 100644 index 0000000..df86c8b --- /dev/null +++ b/src/main/java/mc/protocol/text/TextTemplate.java @@ -0,0 +1,146 @@ +package mc.protocol.text; + +import com.google.common.collect.ImmutableList; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +import java.util.*; + +public class TextTemplate { + private final ImmutableList elements; + + private TextTemplate(ImmutableList elements) { + this.elements = elements; + } + + public Text apply(Object... objects) { + Map variableMap = new HashMap<>((objects.length % 2) == 1 ? (objects.length / 2) + 1 : (objects.length / 2)); + + boolean skipValue = false; + String key = null; + for (Object obj : objects) { + if (skipValue) { + skipValue = false; + continue; + } + + if (key == null) { + if (obj == null || obj.toString().trim().isEmpty()) { + skipValue = true; + continue; + } + + key = obj.toString().trim(); + } else { + variableMap.put(key, obj); + key = null; + } + } + + if (key != null) { + variableMap.put(key, ""); + } + + return apply(variableMap); + } + + public Text apply(Map variables) { + Text.Builder textBuilder = Text.builder(); + + for(Object obj : elements) { + if (obj instanceof Text) { + textBuilder.append((Text) obj); + } else if (obj instanceof Arg) { + Arg arg = (Arg) obj; + if (variables.containsKey(arg.getKey())) { + Object valueObj = variables.get(arg.getKey()); + + if (valueObj instanceof Text) { + textBuilder.append((Text) valueObj); + } else { + textBuilder.append(Text.of(valueObj, arg.getColor(), arg.getStyle())); + } + } else { + textBuilder.append(Text.of(arg.getDefaultValue(), arg.getColor(), arg.getStyle())); + } + } + } + + return textBuilder.build(); + } + + @RequiredArgsConstructor + @Getter + public static class Arg { + private final String key; + private final String defaultValue; + @Setter + private TextColor color; + @Setter + private TextStyle style; + } + + public static class Builder { + private List elements = new ArrayList<>(); + + public Builder append(Text element) { + this.elements.add(element); + return this; + } + + public Builder append(Text... elements) { + Collections.addAll(this.elements, elements); + return this; + } + + public Builder arg(String name) { + this.elements.add(new Arg(name, null)); + return this; + } + + public Builder arg(String name, String defaultValue) { + this.elements.add(new Arg(name, defaultValue)); + return this; + } + + public Builder arg(Object... objects) { + String key = null, + defaultValue = null; + TextColor color = null; + TextStyle style = null; + + for(Object obj : objects) { + if (obj instanceof String) { + if (key == null) { + key = (String) obj; + } else { + defaultValue = (String) obj; + } + } else if (obj instanceof TextColor) { + color = (TextColor) obj; + } else if (obj instanceof TextStyle) { + if (style == null) { + style = TextStyle.none(); + } + style.merge((TextStyle) obj); + } + } + + Arg arg = new Arg(key, defaultValue); + arg.setColor(color); + arg.setStyle(style); + this.elements.add(arg); + + return this; + } + + public TextTemplate build() { + return new TextTemplate(ImmutableList.copyOf(elements)); + } + } + + public static Builder builder() { + return new Builder(); + } +} diff --git a/src/main/java/mc/protocol/Utils.java b/src/main/java/mc/protocol/utils/Utils.java similarity index 91% rename from src/main/java/mc/protocol/Utils.java rename to src/main/java/mc/protocol/utils/Utils.java index d5fbe79..d4ce0c6 100644 --- a/src/main/java/mc/protocol/Utils.java +++ b/src/main/java/mc/protocol/utils/Utils.java @@ -1,4 +1,4 @@ -package mc.protocol; +package mc.protocol.utils; import lombok.experimental.UtilityClass; diff --git a/src/main/java/mc/protocol/utils/json/JsonUtils.java b/src/main/java/mc/protocol/utils/json/JsonUtils.java new file mode 100644 index 0000000..ae01d6e --- /dev/null +++ b/src/main/java/mc/protocol/utils/json/JsonUtils.java @@ -0,0 +1,55 @@ +package mc.protocol.utils.json; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import mc.protocol.text.Text; +import mc.protocol.utils.json.serializer.TextDeserializer; + +import java.util.Optional; + +@Slf4j +@UtilityClass +public class JsonUtils { + + private final String EMPTY_OBJECT = "{}"; + private ObjectMapper objectMapper; + + public String objectToJson(Object object) { + try { + return getObjectMapper().writeValueAsString(object); + } catch (JsonProcessingException e) { + if (log.isDebugEnabled()) { + log.debug("Error serialize object {}", object, e); + } + return EMPTY_OBJECT; + } + } + + public Optional jsonToObject(String json, Class returnType) { + Optional result; + + try { + result = Optional.of(getObjectMapper().readValue(json, returnType)); + } catch (JsonProcessingException e1) { + e1.printStackTrace(); + result = Optional.empty(); + } + + return result; + } + + public ObjectMapper getObjectMapper() { + if (objectMapper == null) { + SimpleModule module = new SimpleModule(); + module.addDeserializer(Text.class, new TextDeserializer()); + + objectMapper = new ObjectMapper(); + objectMapper.registerModule(module); + } + + return objectMapper; + } +} diff --git a/src/main/java/mc/protocol/utils/json/serializer/TextDeserializer.java b/src/main/java/mc/protocol/utils/json/serializer/TextDeserializer.java new file mode 100644 index 0000000..f46cdef --- /dev/null +++ b/src/main/java/mc/protocol/utils/json/serializer/TextDeserializer.java @@ -0,0 +1,69 @@ +package mc.protocol.utils.json.serializer; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import mc.protocol.text.Text; +import mc.protocol.text.TextColor; +import mc.protocol.text.TextStyle; + +import java.io.IOException; +import java.util.Optional; + +public class TextDeserializer extends StdDeserializer { + + public TextDeserializer() { + this(null); + } + + public TextDeserializer(Class t) { + super(t); + } + + @Override + public Text deserialize(JsonParser parser, DeserializationContext ctx) throws IOException { + final Text.Builder builder = Text.builder(); + + final JsonNode jsonNode = parser.getCodec().readTree(parser); + + Optional.ofNullable(jsonNode.get("text")) + .ifPresent(node -> builder.append(node.asText())); + Optional.ofNullable(jsonNode.get("color")) + .ifPresent(node -> builder.color(TextColor.valueOfColorName(node.asText()))); + + if (jsonNode.get("bold") != null && jsonNode.get("bold").isBoolean() + && jsonNode.get("bold").asBoolean()) { + builder.style(TextStyle.BOLD); + } + + if (jsonNode.get("italic") != null && jsonNode.get("italic").isBoolean() + && jsonNode.get("italic").asBoolean()) { + builder.style(TextStyle.ITALIC); + } + + if (jsonNode.get("obfuscated") != null && jsonNode.get("obfuscated").isBoolean() + && jsonNode.get("obfuscated").asBoolean()) { + builder.style(TextStyle.OBFUSCATED); + } + + if (jsonNode.get("strikethrough") != null && jsonNode.get("strikethrough").isBoolean() + && jsonNode.get("strikethrough").asBoolean()) { + builder.style(TextStyle.STRIKETHOUGH); + } + + if (jsonNode.get("underlined") != null && jsonNode.get("underlined").isBoolean() + && jsonNode.get("underlined").asBoolean()) { + builder.style(TextStyle.UNDERLINE); + } + + if (jsonNode.get("extra") != null && jsonNode.get("extra").isArray()) { + final JsonNode nodeExtra = jsonNode.get("extra"); + for (JsonNode node : nodeExtra) { + builder.append(parser.getCodec().treeToValue(node, Text.class)); + } + } + + return builder.build(); + } +} diff --git a/src/test/java/mc/protocol/text/TextTest.java b/src/test/java/mc/protocol/text/TextTest.java new file mode 100644 index 0000000..0b55fc6 --- /dev/null +++ b/src/test/java/mc/protocol/text/TextTest.java @@ -0,0 +1,75 @@ +package mc.protocol.text; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class TextTest { + + @Test + void testToPlain() { + final String m1 = "mes"; + final String m2 = "sage"; + final String message = m1 + m2; + + assertEquals(message, Text.of(message).toPlain()); + assertEquals(message, Text.builder(message).build().toPlain()); + assertEquals(message, Text.builder(Text.of(message)).build().toPlain()); + assertEquals(message, Text.builder().append(message).build().toPlain()); + assertEquals(message, Text.builder().append(Text.of(message)).build().toPlain()); + + assertEquals(message, Text.builder(m1, m2).build().toPlain()); + assertEquals(message, Text.builder(Text.of(m1), Text.of(m2)).build().toPlain()); + assertEquals(message, Text.builder().append(Text.of(m1), Text.of(m2)).build().toPlain()); + assertEquals(message, Text.builder().append(Text.of(m1)).append(Text.of(m2)).build().toPlain()); + } + + @Test + void testEquals() { + assertEquals(Text.of(), Text.of("")); + assertEquals(Text.of(), Text.builder().build()); + assertEquals(Text.of(), Text.builder("").build()); + assertEquals(Text.of(), Text.builder().append().build()); + assertEquals(Text.of(), Text.builder().append("").build()); + + assertNotEquals(Text.of(), Text.of("??")); + assertNotEquals(Text.of(), Text.builder("??").build()); + assertNotEquals(Text.of(), Text.builder().append("??").build()); + + assertEquals(Text.of("message"), Text.builder("message").build()); + assertEquals(Text.of("message"), Text.builder(Text.of("message")).build()); + assertEquals(Text.of("message"), Text.builder().append("message").build()); + assertEquals(Text.of("message"), Text.builder().append(Text.of("message")).build()); + } + + @Test + void testEmpty() { + assertTrue(Text.of().isEmpty()); + assertTrue(Text.of((String) null).isEmpty()); + assertTrue(Text.of((Text) null).isEmpty()); + assertTrue(Text.of("").isEmpty()); + assertTrue(Text.of("", "").isEmpty()); + + assertTrue(Text.builder().build().isEmpty()); + assertTrue(Text.builder((String) null).build().isEmpty()); + assertTrue(Text.builder((Text) null).build().isEmpty()); + assertTrue(Text.builder("").build().isEmpty()); + assertTrue(Text.builder("", "").build().isEmpty()); + assertTrue(Text.builder(Text.of()).build().isEmpty()); + assertTrue(Text.builder(Text.of(), Text.of()).build().isEmpty()); + + assertTrue(Text.builder().append().build().isEmpty()); + assertTrue(Text.builder().append((String) null).build().isEmpty()); + assertTrue(Text.builder().append((Text) null).build().isEmpty()); + assertTrue(Text.builder().append("").build().isEmpty()); + assertTrue(Text.builder().append(Text.of()).build().isEmpty()); + assertTrue(Text.builder().append(Text.of(), Text.of()).build().isEmpty()); + assertTrue(Text.builder().append(Text.of()).append(Text.of()).build().isEmpty()); + + assertFalse(Text.of("??").isEmpty()); + assertFalse(Text.builder("??").build().isEmpty()); + assertFalse(Text.builder(Text.of("??")).build().isEmpty()); + assertFalse(Text.builder().append("??").build().isEmpty()); + assertFalse(Text.builder().append(Text.of("??")).build().isEmpty()); + } +}