putAll(final @NotNull InheritanceAwareMap extends C, ? extends V> map) {
+ final InheritanceAwareMapImpl, V> impl = (InheritanceAwareMapImpl, V>) map;
+ if (this.strict) {
+ if (!this.values.isEmpty() || !impl.strict) { // validate all
+ for (final Map.Entry extends Class>, V> entry : impl.declaredValues.entrySet()) {
+ validateNoneInHierarchy(entry.getKey(), this.values);
+ this.values.put((Class extends C>) entry.getKey(), entry.getValue());
+ }
+ return this;
+ }
+ }
+
+ // otherwise (simpler)
+ this.values.putAll((Map) impl.declaredValues);
+ return this;
+ }
+ }
+
+ private static void validateNoneInHierarchy(final Class> beingRegistered, final Map extends Class>, ?> entries) {
+ for (final Class> clazz : entries.keySet()) {
+ testHierarchy(clazz, beingRegistered);
+ }
+ }
+
+ private static void testHierarchy(final Class> existing, final Class> beingRegistered) {
+ if (!existing.equals(beingRegistered) && (existing.isAssignableFrom(beingRegistered) || beingRegistered.isAssignableFrom(existing))) {
+ throw new IllegalArgumentException("Conflict detected between already registered type " + existing
+ + " and newly registered type " + beingRegistered + "! Types in a strict inheritance-aware map must not share a common hierarchy!");
+ }
+ }
+}
diff --git a/api/src/main/java/net/kyori/adventure/util/Services.java b/api/src/main/java/net/kyori/adventure/util/Services.java
index bb89f6f8b..3e1ff0e21 100644
--- a/api/src/main/java/net/kyori/adventure/util/Services.java
+++ b/api/src/main/java/net/kyori/adventure/util/Services.java
@@ -23,9 +23,13 @@
*/
package net.kyori.adventure.util;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.Optional;
+import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
+import java.util.Set;
import net.kyori.adventure.internal.properties.AdventureProperties;
import org.jetbrains.annotations.NotNull;
@@ -121,4 +125,31 @@ public interface Fallback {
return Optional.ofNullable(firstFallback);
}
+
+ /**
+ * Locates all providers for a certain service and initializes them.
+ *
+ * @param clazz the service interface
+ * @param the service interface type
+ * @return an unmodifiable set of all known providers of the service
+ * @since 4.17.0
+ */
+ public static
Set
services(final Class extends P> clazz) {
+ final ServiceLoader extends P> loader = Services0.loader(clazz);
+ final Set
providers = new HashSet<>();
+ for (final Iterator extends P> it = loader.iterator(); it.hasNext();) {
+ final P instance;
+ try {
+ instance = it.next();
+ } catch (final ServiceConfigurationError ex) {
+ if (SERVICE_LOAD_FAILURES_ARE_FATAL) {
+ throw new IllegalStateException("Encountered an exception loading a provider for " + clazz + ": ", ex);
+ } else {
+ continue;
+ }
+ }
+ providers.add(instance);
+ }
+ return Collections.unmodifiableSet(providers);
+ }
}
diff --git a/api/src/test/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistryTest.java b/api/src/test/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistryTest.java
new file mode 100644
index 000000000..dbb8b399f
--- /dev/null
+++ b/api/src/test/java/net/kyori/adventure/text/event/DataComponentValueConverterRegistryTest.java
@@ -0,0 +1,159 @@
+/*
+ * This file is part of adventure, licensed under the MIT License.
+ *
+ * Copyright (c) 2017-2024 KyoriPowered
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package net.kyori.adventure.text.event;
+
+import com.google.auto.service.AutoService;
+import java.util.Arrays;
+import net.kyori.adventure.key.Key;
+import net.kyori.adventure.nbt.api.BinaryTagHolder;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.Test;
+
+import static net.kyori.adventure.key.Key.key;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class DataComponentValueConverterRegistryTest {
+ @Test
+ void testKnownSourceToUnknownDest() {
+ final IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () -> {
+ DataComponentValueConverterRegistry.convert(Unregistered.class, key("test"), new DirectValue(3));
+ });
+
+ assertTrue(iae.getMessage().contains("There is no data holder converter registered"));
+ }
+
+ @Test
+ void testUnknownSourceToKnownDest() {
+ final IllegalArgumentException iae = assertThrows(IllegalArgumentException.class, () -> {
+ DataComponentValueConverterRegistry.convert(DirectValue.class, key("test"), new Unregistered());
+ });
+
+ assertTrue(iae.getMessage().contains("There is no data holder converter registered"));
+
+ }
+
+ @Test
+ void testFailedConversionBlamesProvider() {
+ final IllegalStateException ise = assertThrows(IllegalStateException.class, () -> {
+ DataComponentValueConverterRegistry.convert(Failing.class, key("test"), BinaryTagHolder.binaryTagHolder("{}"));
+ });
+
+ assertTrue(ise.getMessage().contains(TestConverterProvider.ID.asString()));
+ }
+
+ @Test
+ void testExactToExact() {
+ final DirectValue input = new DirectValue(new Object());
+ final ItfValueImpl result = DataComponentValueConverterRegistry.convert(ItfValueImpl.class, key("test"), input);
+
+ assertEquals(input.value, result.value);
+ }
+
+ @Test
+ void testSubtypeToExact() {
+ final ItfValue input = new ItfValueImpl(new Object());
+ final DirectValue result = DataComponentValueConverterRegistry.convert(DirectValue.class, key("test"), input);
+
+ assertEquals(input.value(), result.value);
+ }
+
+ @Test
+ void testSubtypeToSupertype() {
+ final ItfValue input = new ItfValueImpl(new Object());
+ final DataComponentValue result = DataComponentValueConverterRegistry.convert(Intermediate.class, key("test"), input);
+
+ assertInstanceOf(DirectValue.class, result);
+ assertEquals(input.value(), ((DirectValue) result).value);
+ }
+
+ @Test
+ void testExactToSupertype() {
+ final DirectValue input = new DirectValue(new Object());
+ final ItfValue result = DataComponentValueConverterRegistry.convert(ItfValue.class, key("test"), input);
+
+ assertEquals(input.value, result.value());
+ }
+
+ @AutoService(DataComponentValueConverterRegistry.Provider.class)
+ public static final class TestConverterProvider implements DataComponentValueConverterRegistry.Provider {
+ static final Key ID = key("adventure", "test/converter_registry");
+
+ @Override
+ public @NotNull Key id() {
+ return ID;
+ }
+
+ @Override
+ public @NotNull Iterable> conversions() {
+ // gah j8
+ return Arrays.asList(
+ DataComponentValueConverterRegistry.Conversion.convert(DirectValue.class, ItfValueImpl.class, (key, dir) -> new ItfValueImpl(dir.value)),
+ DataComponentValueConverterRegistry.Conversion.convert(ItfValue.class, DirectValue.class, (key, itf) -> new DirectValue(itf.value())),
+ DataComponentValueConverterRegistry.Conversion.convert(BinaryTagHolder.class, Failing.class, (key, itf) -> {
+ throw new RuntimeException("hah!");
+ })
+ );
+ }
+ }
+
+ static final class Unregistered implements DataComponentValue {
+ // i shall not be converted
+ }
+
+ static final class Failing implements DataComponentValue {
+ // this is a marker interface to trigger a failure
+ }
+
+ interface Intermediate extends DataComponentValue {
+
+ }
+
+ static final class DirectValue implements Intermediate {
+ final Object value;
+
+ DirectValue(final Object value) {
+ this.value = value;
+ }
+ }
+
+ interface ItfValue extends DataComponentValue {
+ Object value();
+ }
+
+ static final class ItfValueImpl implements ItfValue {
+ final Object value;
+
+ ItfValueImpl(final Object value) {
+ this.value = value;
+ }
+
+ @Override
+ public Object value() {
+ return this.value;
+ }
+ }
+}
diff --git a/api/src/test/java/net/kyori/adventure/text/event/HoverEventTest.java b/api/src/test/java/net/kyori/adventure/text/event/HoverEventTest.java
index e5d429932..19f69b8b8 100644
--- a/api/src/test/java/net/kyori/adventure/text/event/HoverEventTest.java
+++ b/api/src/test/java/net/kyori/adventure/text/event/HoverEventTest.java
@@ -25,6 +25,7 @@
import com.google.common.collect.ImmutableSet;
import com.google.common.testing.EqualsTester;
+import java.util.Collections;
import java.util.UUID;
import java.util.function.UnaryOperator;
import net.kyori.adventure.key.Key;
@@ -113,8 +114,8 @@ void testEquality() {
HoverEvent.hoverEvent(HoverEvent.Action.SHOW_TEXT, Component.empty())
)
.addEqualityGroup(
- HoverEvent.showItem(HoverEvent.ShowItem.showItem(Key.key("air"), 1, null)),
- HoverEvent.hoverEvent(HoverEvent.Action.SHOW_ITEM, HoverEvent.ShowItem.showItem(Key.key("air"), 1, null))
+ HoverEvent.showItem(HoverEvent.ShowItem.showItem(Key.key("air"), 1, Collections.emptyMap())),
+ HoverEvent.hoverEvent(HoverEvent.Action.SHOW_ITEM, HoverEvent.ShowItem.showItem(Key.key("air"), 1, Collections.emptyMap()))
)
.addEqualityGroup(
HoverEvent.showEntity(HoverEvent.ShowEntity.showEntity(Key.key("cat"), entity)),
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 32c3c7bf8..b7303f64d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,6 +2,7 @@
version = "1.0"
[versions]
+autoService = "1.1.1"
checkstyle = "10.15.0"
errorprone = "2.26.1"
examination = "1.3.0"
@@ -15,6 +16,8 @@ truth = "1.4.2"
[libraries]
# shared
+autoService = { module = "com.google.auto.service:auto-service", version.ref = "autoService" }
+autoService-annotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoService" }
examination-api = { module = "net.kyori:examination-api", version.ref = "examination" }
examination-string = { module = "net.kyori:examination-string", version.ref = "examination" }
option = { module = "net.kyori:option", version = "1.0.0" }
@@ -55,8 +58,6 @@ truth-java8 = { module = "com.google.truth.extensions:truth-java8-extension", ve
contractValidator = "ca.stellardrift:contract-validator:1.0.1"
errorprone = { module = "com.google.errorprone:error_prone_core", version.ref = "errorprone" }
stylecheck = "ca.stellardrift:stylecheck:0.2.1"
-autoService-annotations = "com.google.auto.service:auto-service-annotations:1.1.1"
-autoService-processor = "com.google.auto.service:auto-service:1.1.1"
build-errorpronePlugin = "net.ltgt.gradle:gradle-errorprone-plugin:3.1.0"
build-indra = { module = "net.kyori:indra-common", version.ref = "indra" }
diff --git a/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateComponentSerializerImpl.java b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateComponentSerializerImpl.java
index 0dafd26a8..052154dfc 100644
--- a/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateComponentSerializerImpl.java
+++ b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateComponentSerializerImpl.java
@@ -107,6 +107,7 @@ private ConfigurateComponentSerializerImpl(final @NotNull Builder builder) {
.registerExact(new IndexSerializer<>(TypeToken.get(TextDecoration.class), TextDecoration.NAMES))
.registerExact(HoverEvent.ShowEntity.class, HoverEventShowEntitySerializer.INSTANCE)
.registerExact(HoverEvent.ShowItem.class, HoverEventShowItemSerializer.INSTANCE)
+ .register(ConfigurateDataComponentValue.class, ConfigurateDataComponentValueTypeSerializer.INSTANCE)
.build();
}
diff --git a/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateDataComponentValue.java b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateDataComponentValue.java
new file mode 100644
index 000000000..ced228b45
--- /dev/null
+++ b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateDataComponentValue.java
@@ -0,0 +1,54 @@
+/*
+ * This file is part of adventure, licensed under the MIT License.
+ *
+ * Copyright (c) 2017-2024 KyoriPowered
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package net.kyori.adventure.serializer.configurate4;
+
+import net.kyori.adventure.text.event.DataComponentValue;
+import org.jetbrains.annotations.NotNull;
+import org.spongepowered.configurate.ConfigurationNode;
+
+/**
+ * A data component value that can integrate with configuration nodes.
+ *
+ * @since 4.17.0
+ */
+public interface ConfigurateDataComponentValue extends DataComponentValue {
+ /**
+ * Create a data component value capturing the value of an existing node.
+ *
+ * @param existing the existing node
+ * @return the captured value
+ * @since 4.17.0
+ */
+ static @NotNull ConfigurateDataComponentValue capturingDataComponentValue(final @NotNull ConfigurationNode existing) {
+ return SnapshottingConfigurateDataComponentValue.create(existing);
+ }
+
+ /**
+ * Apply the contained value to the supplied node.
+ *
+ * @param node the node to apply this value to
+ * @since 4.17.0
+ */
+ void applyTo(final @NotNull ConfigurationNode node);
+}
diff --git a/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateDataComponentValueTypeSerializer.java b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateDataComponentValueTypeSerializer.java
new file mode 100644
index 000000000..89d3b9c27
--- /dev/null
+++ b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/ConfigurateDataComponentValueTypeSerializer.java
@@ -0,0 +1,52 @@
+/*
+ * This file is part of adventure, licensed under the MIT License.
+ *
+ * Copyright (c) 2017-2024 KyoriPowered
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+package net.kyori.adventure.serializer.configurate4;
+
+import java.lang.reflect.Type;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.spongepowered.configurate.ConfigurationNode;
+import org.spongepowered.configurate.serialize.SerializationException;
+import org.spongepowered.configurate.serialize.TypeSerializer;
+
+final class ConfigurateDataComponentValueTypeSerializer implements TypeSerializer {
+ static final TypeSerializer INSTANCE = new ConfigurateDataComponentValueTypeSerializer();
+
+ private ConfigurateDataComponentValueTypeSerializer() {
+ }
+
+ @Override
+ public ConfigurateDataComponentValue deserialize(final Type type, final ConfigurationNode node) throws SerializationException {
+ return ConfigurateDataComponentValue.capturingDataComponentValue(node);
+ }
+
+ @Override
+ public void serialize(final Type type, final @Nullable ConfigurateDataComponentValue obj, final ConfigurationNode node) throws SerializationException {
+ if (obj == null) {
+ node.set(null);
+ return;
+ }
+
+ obj.applyTo(node);
+ }
+}
diff --git a/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/HoverEventShowItemSerializer.java b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/HoverEventShowItemSerializer.java
index 0dd1b5d98..4b43b7086 100644
--- a/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/HoverEventShowItemSerializer.java
+++ b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/HoverEventShowItemSerializer.java
@@ -23,7 +23,10 @@
*/
package net.kyori.adventure.serializer.configurate4;
+import io.leangen.geantyref.TypeToken;
import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.Map;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.nbt.api.BinaryTagHolder;
import net.kyori.adventure.text.event.HoverEvent;
@@ -36,9 +39,13 @@
final class HoverEventShowItemSerializer implements TypeSerializer {
static final HoverEventShowItemSerializer INSTANCE = new HoverEventShowItemSerializer();
+ private static final TypeToken