diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..874bf8b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,2 @@ +[*] +end_of_line = lf \ No newline at end of file diff --git a/build-logic/src/main/kotlin/moonshine.api.gradle.kts b/build-logic/src/main/kotlin/moonshine.api.gradle.kts index f97c044..fd645e4 100644 --- a/build-logic/src/main/kotlin/moonshine.api.gradle.kts +++ b/build-logic/src/main/kotlin/moonshine.api.gradle.kts @@ -33,7 +33,7 @@ repositories { dependencies { val libs = (project as ExtensionAware).extensions.getByName("libs") as LibrariesForLibs - api(libs.checkerframework) + compileOnlyApi(libs.checkerframework) testImplementation(libs.bundles.testing.api) testRuntimeOnly(libs.bundles.testing.runtime) diff --git a/core/src/main/java/net/kyori/moonshine/Moonshine.java b/core/src/main/java/net/kyori/moonshine/Moonshine.java index d351c93..3684689 100644 --- a/core/src/main/java/net/kyori/moonshine/Moonshine.java +++ b/core/src/main/java/net/kyori/moonshine/Moonshine.java @@ -17,14 +17,18 @@ */ package net.kyori.moonshine; +import static io.leangen.geantyref.GenericTypeReflector.erase; + import io.leangen.geantyref.GenericTypeReflector; import io.leangen.geantyref.TypeToken; import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.lang.reflect.Type; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.NavigableSet; +import net.kyori.moonshine.annotation.MessageSection; import net.kyori.moonshine.annotation.meta.ThreadSafe; import net.kyori.moonshine.exception.MissingMoonshineMethodMappingException; import net.kyori.moonshine.exception.scan.UnscannableMethodException; @@ -36,6 +40,9 @@ import net.kyori.moonshine.receiver.IReceiverLocatorResolver; import net.kyori.moonshine.strategy.IPlaceholderResolverStrategy; import net.kyori.moonshine.util.Weighted; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.dataflow.qual.Pure; import org.checkerframework.dataflow.qual.SideEffectFree; @@ -48,71 +55,31 @@ * @param the finalised placeholder type, post-resolving */ @ThreadSafe -public final class Moonshine { - /** - * The type which is being proxied with this Moonshine instance. - */ - private final TypeToken proxiedType; - - /** - * The proxy invocation handler instance. - */ - private final MoonshineInvocationHandler invocationHandler; - - /** - * The strategy for resolving placeholders on a method invocation. - */ - private final IPlaceholderResolverStrategy placeholderResolverStrategy; - - /** - * The source of intermediate messages, per receiver. - */ - private final IMessageSource messageSource; - - /** - * The renderer of all messages, before sent via {@link #messageSender()}. - */ - private final IMessageRenderer messageRenderer; +public abstract sealed class Moonshine permits MoonshineRoot, MoonshineChild { + private final Type proxiedType; + private final Object proxy; + private final @Nullable MessageSection sectionAnnotation; - /** - * The message sender of intermediate messages to a given receiver with resolved placeholders. - */ - private final IMessageSender messageSender; + private @MonotonicNonNull Map> scannedMethods; - /** - * A navigable set for iterating through the {@link IReceiverLocatorResolver}s with weight-based ordering. - */ - private final NavigableSet>> weightedReceiverLocatorResolvers; + protected Moonshine(final Type proxiedType, final ClassLoader classLoader) { + this.proxiedType = proxiedType; + this.sectionAnnotation = erase(proxiedType).getAnnotation(MessageSection.class); - /** - * A map of types to navigable sets for iterating through the {@link IPlaceholderResolver}s with weight-based - * ordering. - */ - private final Map>>> weightedPlaceholderResolvers; + final var invocationHandler = new MoonshineInvocationHandler<>(this); + this.proxy = Proxy.newProxyInstance(classLoader, + new Class[]{GenericTypeReflector.erase(proxiedType)}, + invocationHandler); + } /** - * All scanned methods of this proxy, excluding special-case methods such as {@code default} methods and any returning - * {@link Moonshine}. + * Scan the proxied type to see which methods are available and store them in a map + * @param fullKey the key inherited by parent message sections (if applicable) + * plus the key and delimiter present on this message section. */ - private final Map> scannedMethods; - - Moonshine(final TypeToken proxiedType, - final IPlaceholderResolverStrategy placeholderResolverStrategy, - final IMessageSource messageSource, - final IMessageRenderer messageRenderer, - final IMessageSender messageSender, - final NavigableSet>> weightedReceiverLocatorResolvers, - final Map>>> weightedPlaceholderResolvers) - throws UnscannableMethodException { - this.proxiedType = proxiedType; - this.placeholderResolverStrategy = placeholderResolverStrategy; - this.messageSource = messageSource; - this.messageRenderer = messageRenderer; - this.messageSender = messageSender; - this.weightedReceiverLocatorResolvers = Collections.unmodifiableNavigableSet(weightedReceiverLocatorResolvers); - this.weightedPlaceholderResolvers = Collections.unmodifiableMap(weightedPlaceholderResolvers); - - final Method[] methods = GenericTypeReflector.erase(proxiedType.getType()).getMethods(); + @EnsuresNonNull("scannedMethods") + protected void scanMethods(final String fullKey, final ClassLoader proxyClassLoader) throws UnscannableMethodException { + final Method[] methods = erase(this.proxiedType).getMethods(); final Map> scannedMethods = new HashMap<>(methods.length); for (final Method method : methods) { if (method.isDefault() || method.getReturnType() == Moonshine.class) { @@ -120,12 +87,10 @@ public final class Moonshine { } final MoonshineMethod moonshineMethod = - new MoonshineMethod<>(this, proxiedType, method); + new MoonshineMethod<>(this, this.proxiedType, proxyClassLoader, method, fullKey, this.proxiedTypeKeyDelimiter()); scannedMethods.put(method, moonshineMethod); } this.scannedMethods = Collections.unmodifiableMap(scannedMethods); - - this.invocationHandler = new MoonshineInvocationHandler<>(this); } @SideEffectFree @@ -138,42 +103,53 @@ public static MoonshineBuilder.Receivers builder(final TypeToken */ @Pure public Type proxiedType() { - return this.proxiedType.getType(); + return this.proxiedType; } /** - * @return the proxy invocation handler instance for the current {@link #proxiedType()} + * Returns the Moonshine instance that represents the parent of this message section (if it has any). + * @return the Moonshine instance if any, otherwise null */ @Pure - public MoonshineInvocationHandler invocationHandler() { - return this.invocationHandler; + public @Nullable Moonshine parent() { + return null; + } + + protected String proxiedTypeKey() { + if (this.sectionAnnotation == null || this.sectionAnnotation.value().isEmpty()) { + return ""; + } + return this.sectionAnnotation.value() + this.sectionAnnotation.delimiter(); + } + + protected char proxiedTypeKeyDelimiter() { + return this.sectionAnnotation != null ? this.sectionAnnotation.delimiter() : MessageSection.DEFAULT_DELIMITER; + } + + @Pure + public Object proxy() { + return this.proxy; } /** * @return the current placeholder resolving strategy */ @Pure - public IPlaceholderResolverStrategy placeholderResolverStrategy() { - return this.placeholderResolverStrategy; - } + public abstract IPlaceholderResolverStrategy placeholderResolverStrategy(); /** * @return an unmodifiable view of a navigable set for iterating through the available {@link * IReceiverLocatorResolver}s with weight-based ordering */ @Pure - public NavigableSet>> weightedReceiverLocatorResolvers() { - return this.weightedReceiverLocatorResolvers; - } + public abstract NavigableSet>> weightedReceiverLocatorResolvers(); /** * @return an unmodifiable view of a map of types to navigable sets for iterating through the available {@link * IPlaceholderResolver}s with weight-based ordering */ @Pure - public Map>>> weightedPlaceholderResolvers() { - return this.weightedPlaceholderResolvers; - } + public abstract Map>>> weightedPlaceholderResolvers(); /** * Find a scanned method by the given method mapping. @@ -195,21 +171,15 @@ public MoonshineMethod scannedMethod(final Method method) throws Mi /** * @return the source of intermediate messages, per receiver */ - public IMessageSource messageSource() { - return this.messageSource; - } + public abstract IMessageSource messageSource(); /** * @return the renderer of messages, used before sending via {@link #messageSender()} */ - public IMessageRenderer messageRenderer() { - return this.messageRenderer; - } + public abstract IMessageRenderer messageRenderer(); /** * @return the message sender of intermediate messages to a given receiver with resolved placeholders */ - public IMessageSender messageSender() { - return this.messageSender; - } + public abstract IMessageSender messageSender(); } diff --git a/core/src/main/java/net/kyori/moonshine/MoonshineBuilder.java b/core/src/main/java/net/kyori/moonshine/MoonshineBuilder.java index 3ca7c10..cdcaa22 100644 --- a/core/src/main/java/net/kyori/moonshine/MoonshineBuilder.java +++ b/core/src/main/java/net/kyori/moonshine/MoonshineBuilder.java @@ -17,9 +17,7 @@ */ package net.kyori.moonshine; -import io.leangen.geantyref.GenericTypeReflector; import io.leangen.geantyref.TypeToken; -import java.lang.reflect.Proxy; import java.lang.reflect.Type; import java.util.HashMap; import java.util.Map; @@ -159,7 +157,7 @@ public Resolved resolvingWithStrategy( @NotThreadSafe public static final class Resolved { - private final TypeToken proxiedType; + private final Type proxiedType; private final NavigableSet>> weightedReceiverLocatorResolvers; private final IMessageSource messageSource; private final IMessageRenderer messageRenderer; @@ -173,7 +171,7 @@ private Resolved(final TypeToken proxiedType, final IMessageSource messageSource, final IMessageRenderer messageRenderer, final IMessageSender messageSender, final IPlaceholderResolverStrategy placeholderResolverStrategy) { - this.proxiedType = proxiedType; + this.proxiedType = proxiedType.getType(); this.weightedReceiverLocatorResolvers = weightedReceiverLocatorResolvers; this.messageSource = messageSource; this.messageRenderer = messageRenderer; @@ -227,12 +225,10 @@ public T create() throws UnscannableMethodException { @SuppressWarnings("unchecked") // Proxy returns Object; we expect T which is provided in #proxiedType. @SideEffectFree public T create(final ClassLoader classLoader) throws UnscannableMethodException { - final Moonshine moonshine = new Moonshine<>(this.proxiedType, this.placeholderResolverStrategy, - this.messageSource, this.messageRenderer, this.messageSender, this.weightedReceiverLocatorResolvers, - this.weightedPlaceholderResolvers); - return (T) Proxy.newProxyInstance(classLoader, - new Class[]{GenericTypeReflector.erase(this.proxiedType.getType())}, - moonshine.invocationHandler()); + final Moonshine moonshine = new MoonshineRoot<>(this.proxiedType, classLoader, + this.placeholderResolverStrategy, this.messageSource, this.messageRenderer, this.messageSender, + this.weightedReceiverLocatorResolvers, this.weightedPlaceholderResolvers); + return (T) moonshine.proxy(); } } } diff --git a/core/src/main/java/net/kyori/moonshine/MoonshineChild.java b/core/src/main/java/net/kyori/moonshine/MoonshineChild.java new file mode 100644 index 0000000..2e5ae74 --- /dev/null +++ b/core/src/main/java/net/kyori/moonshine/MoonshineChild.java @@ -0,0 +1,80 @@ +/* + * moonshine - A localisation library for Java. + * Copyright (C) Mariell Hoversholm + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package net.kyori.moonshine; + +import java.lang.reflect.Type; +import java.util.Map; +import java.util.NavigableSet; +import net.kyori.moonshine.exception.scan.UnscannableMethodException; +import net.kyori.moonshine.message.IMessageRenderer; +import net.kyori.moonshine.message.IMessageSender; +import net.kyori.moonshine.message.IMessageSource; +import net.kyori.moonshine.placeholder.IPlaceholderResolver; +import net.kyori.moonshine.receiver.IReceiverLocatorResolver; +import net.kyori.moonshine.strategy.IPlaceholderResolverStrategy; +import net.kyori.moonshine.util.Weighted; + +public final class MoonshineChild extends Moonshine { + private final Moonshine parent; + + public MoonshineChild( + final Type proxiedType, + final ClassLoader proxyClassLoader, + final Moonshine parent, + final String inheritedKey) + throws UnscannableMethodException { + super(proxiedType, proxyClassLoader); + this.parent = parent; + scanMethods(inheritedKey + proxiedTypeKey(), proxyClassLoader); + } + + @Override + public Moonshine parent() { + return this.parent; + } + + @Override + public IPlaceholderResolverStrategy placeholderResolverStrategy() { + return this.parent.placeholderResolverStrategy(); + } + + @Override + public NavigableSet>> weightedReceiverLocatorResolvers() { + return this.parent.weightedReceiverLocatorResolvers(); + } + + @Override + public Map>>> weightedPlaceholderResolvers() { + return this.parent.weightedPlaceholderResolvers(); + } + + @Override + public IMessageSource messageSource() { + return this.parent.messageSource(); + } + + @Override + public IMessageRenderer messageRenderer() { + return this.parent.messageRenderer(); + } + + @Override + public IMessageSender messageSender() { + return this.parent.messageSender(); + } +} diff --git a/core/src/main/java/net/kyori/moonshine/MoonshineInvocationHandler.java b/core/src/main/java/net/kyori/moonshine/MoonshineInvocationHandler.java index dfd17e5..77c57ad 100644 --- a/core/src/main/java/net/kyori/moonshine/MoonshineInvocationHandler.java +++ b/core/src/main/java/net/kyori/moonshine/MoonshineInvocationHandler.java @@ -84,6 +84,11 @@ } final var moonshineMethod = this.moonshine.scannedMethod(method); + + if (moonshineMethod.messageSectionProxy() != null) { + return moonshineMethod.messageSectionProxy(); + } + final R receiver = moonshineMethod.receiverLocator().locate(method, proxy, args); final I intermediateMessage = this.moonshine.messageSource().messageOf(receiver, moonshineMethod.messageKey()); final var resolvedPlaceholders = diff --git a/core/src/main/java/net/kyori/moonshine/MoonshineRoot.java b/core/src/main/java/net/kyori/moonshine/MoonshineRoot.java new file mode 100644 index 0000000..8699d37 --- /dev/null +++ b/core/src/main/java/net/kyori/moonshine/MoonshineRoot.java @@ -0,0 +1,93 @@ +/* + * moonshine - A localisation library for Java. + * Copyright (C) Mariell Hoversholm + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package net.kyori.moonshine; + +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.Map; +import java.util.NavigableSet; +import net.kyori.moonshine.annotation.meta.ThreadSafe; +import net.kyori.moonshine.exception.scan.UnscannableMethodException; +import net.kyori.moonshine.message.IMessageRenderer; +import net.kyori.moonshine.message.IMessageSender; +import net.kyori.moonshine.message.IMessageSource; +import net.kyori.moonshine.placeholder.IPlaceholderResolver; +import net.kyori.moonshine.receiver.IReceiverLocatorResolver; +import net.kyori.moonshine.strategy.IPlaceholderResolverStrategy; +import net.kyori.moonshine.util.Weighted; + +@ThreadSafe +final class MoonshineRoot extends Moonshine { + private final IPlaceholderResolverStrategy placeholderResolverStrategy; + private final IMessageSource messageSource; + private final IMessageRenderer messageRenderer; + private final IMessageSender messageSender; + private final NavigableSet>> weightedReceiverLocatorResolvers; + private final Map>>> weightedPlaceholderResolvers; + + MoonshineRoot( + final Type proxiedType, + final ClassLoader classLoader, + final IPlaceholderResolverStrategy placeholderResolverStrategy, + final IMessageSource messageSource, + final IMessageRenderer messageRenderer, + final IMessageSender messageSender, + final NavigableSet>> weightedReceiverLocatorResolvers, + final Map>>> weightedPlaceholderResolvers) + throws UnscannableMethodException { + super(proxiedType, classLoader); + this.placeholderResolverStrategy = placeholderResolverStrategy; + this.messageSource = messageSource; + this.messageRenderer = messageRenderer; + this.messageSender = messageSender; + this.weightedReceiverLocatorResolvers = Collections.unmodifiableNavigableSet(weightedReceiverLocatorResolvers); + this.weightedPlaceholderResolvers = Collections.unmodifiableMap(weightedPlaceholderResolvers); + scanMethods(proxiedTypeKey(), classLoader); + } + + @Override + public IPlaceholderResolverStrategy placeholderResolverStrategy() { + return this.placeholderResolverStrategy; + } + + @Override + public NavigableSet>> + weightedReceiverLocatorResolvers() { + return this.weightedReceiverLocatorResolvers; + } + + @Override + public Map>>> weightedPlaceholderResolvers() { + return this.weightedPlaceholderResolvers; + } + + @Override + public IMessageSource messageSource() { + return this.messageSource; + } + + @Override + public IMessageRenderer messageRenderer() { + return this.messageRenderer; + } + + @Override + public IMessageSender messageSender() { + return this.messageSender; + } +} diff --git a/core/src/main/java/net/kyori/moonshine/annotation/MessageSection.java b/core/src/main/java/net/kyori/moonshine/annotation/MessageSection.java new file mode 100644 index 0000000..d95327a --- /dev/null +++ b/core/src/main/java/net/kyori/moonshine/annotation/MessageSection.java @@ -0,0 +1,92 @@ +/* + * moonshine - A localisation library for Java. + * Copyright (C) Mariell Hoversholm + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package net.kyori.moonshine.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This allows developers to have nested messages and configure given properties of a message + * section.
+ * For the root section adding this annotation is optional, but for nested sections it's not.
+ *
+ * Examples:
+ * A message with key 'hello.world-moonshine' could be added as follows: + * + *
{@code
+ * @MessageSection("hello")
+ * public interface HelloSection {
+ *     WorldSection world();
+ *
+ *     @MessageSection("world", delimiter='-')
+ *     interface WorldSection {
+ *         @Message("moonshine")
+ *         String moonshine(@Receiver User user);
+ *     }
+ * }
+ * }
+ * + * An example where there is no message section annotation on the root section. The following + * example results in the key 'hello.world': + * + *
{@code
+ * public interface RootSection {
+ *     HelloSection hello();
+ *
+ *     @MessageSection("hello")
+ *     interface HelloSection {
+ *         @Message("world")
+ *         String world(@Receiver User user);
+ *     }
+ * }
+ * }
+ * + * Adding the {@link Message @Message} annotation to the method referencing a message section is + * supported. As well as having an empty key to prepend. The following example results in the key + * 'hello.world-moonshine': + * + *
{@code
+ * @MessageSection("hello")
+ * public interface HelloSection {
+ *     @Message("world")
+ *     WorldSection world();
+ *
+ *     @MessageSection(delimiter='-')
+ *     interface WorldSection {
+ *         @Message("moonshine")
+ *         String moonshine(@Receiver User user);
+ *     }
+ * }
+ * }
+ */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface MessageSection { + /** The default delimiter */ + char DEFAULT_DELIMITER = '.'; + + /** Returns the message key to prepend to all messages in this section. */ + String value() default ""; + + /** Returns the delimiter. */ + char delimiter() default DEFAULT_DELIMITER; +} diff --git a/core/src/main/java/net/kyori/moonshine/model/MoonshineMethod.java b/core/src/main/java/net/kyori/moonshine/model/MoonshineMethod.java index e9592b1..69cd829 100644 --- a/core/src/main/java/net/kyori/moonshine/model/MoonshineMethod.java +++ b/core/src/main/java/net/kyori/moonshine/model/MoonshineMethod.java @@ -17,16 +17,17 @@ */ package net.kyori.moonshine.model; -import io.leangen.geantyref.TypeToken; import java.lang.reflect.Method; +import java.lang.reflect.Type; import java.util.Iterator; import net.kyori.moonshine.Moonshine; +import net.kyori.moonshine.MoonshineChild; import net.kyori.moonshine.annotation.Message; +import net.kyori.moonshine.annotation.MessageSection; import net.kyori.moonshine.annotation.meta.ThreadSafe; import net.kyori.moonshine.exception.scan.MissingMessageAnnotationException; import net.kyori.moonshine.exception.scan.NoReceiverLocatorFoundException; import net.kyori.moonshine.exception.scan.UnscannableMethodException; -import net.kyori.moonshine.message.IMessageSource; import net.kyori.moonshine.receiver.IReceiverLocator; import net.kyori.moonshine.receiver.IReceiverLocatorResolver; import net.kyori.moonshine.util.Weighted; @@ -40,65 +41,96 @@ */ @ThreadSafe public final class MoonshineMethod { - /** - * The owning/declaring type of this method. - */ - private final TypeToken owner; - - /** - * The {@link Method reflected method} of this method. - */ + private final Type owner; private final Method reflectMethod; - - /** - * The key for the message to pass to a {@link IMessageSource message source}. - */ + private final @Nullable Object messageSectionProxy; private final String messageKey; - /** - * The locator for a given receiver of this message. - */ - private final IReceiverLocator receiverLocator; + private final @Nullable IReceiverLocator receiverLocator; - public MoonshineMethod(final Moonshine moonshine, final TypeToken owner, final Method reflectMethod) + public MoonshineMethod( + final Moonshine moonshine, + final Type owner, + final ClassLoader proxyClassLoader, + final Method reflectMethod, + final String sectionFullKey, + final char delimiter) throws UnscannableMethodException { this.owner = owner; this.reflectMethod = reflectMethod; - final Message message = this.findMessageAnnotation(); - this.messageKey = message.value(); + final boolean isMessageSection = this.reflectMethod.getReturnType().isAnnotationPresent(MessageSection.class); + + this.messageKey = this.findMessageKey(sectionFullKey, delimiter, isMessageSection); + // todo is IReceiverLocatorResolver needed? + this.receiverLocator = isMessageSection ? null : this.findReceiverLocator(moonshine); - this.receiverLocator = this.findReceiverLocator(moonshine); + Object messageSectionProxy = null; + if (isMessageSection) { + messageSectionProxy = new MoonshineChild<>( + this.reflectMethod.getGenericReturnType(), + proxyClassLoader, + moonshine, + this.messageKey + ).proxy(); + } + this.messageSectionProxy = messageSectionProxy; } + /** + * Returns the owning/declaring type of this method. + */ @Pure - public TypeToken owner() { + public Type owner() { return this.owner; } + /** + * Returns the {@link Method reflected method} of this method. + */ @Pure public Method reflectMethod() { return this.reflectMethod; } + /** + * Returns the message section proxy if this method is a message section, otherwise null. + */ + public @Nullable Object messageSectionProxy() { + return this.messageSectionProxy; + } + + /** + * The full key of the message. So this includes the parent key parts if it has any. + */ @Pure public String messageKey() { return this.messageKey; } + /** + * The locator for a given receiver of this message. + */ @Pure - public IReceiverLocator receiverLocator() { + public @Nullable IReceiverLocator receiverLocator() { return this.receiverLocator; } - private Message findMessageAnnotation() throws MissingMessageAnnotationException { + private String findMessageKey(final String sectionFullKey, final char delimiter, final boolean isMessageSection) throws MissingMessageAnnotationException { final @Nullable Message annotation = this.reflectMethod.getAnnotation(Message.class); + //noinspection ConstantConditions -- this is completely not true. It may be null, per its Javadocs. if (annotation == null) { - throw new MissingMessageAnnotationException(this.owner.getType(), this.reflectMethod); + if (isMessageSection) { + return sectionFullKey; + } + throw new MissingMessageAnnotationException(this.owner, this.reflectMethod); } - return annotation; + if (isMessageSection) { + return sectionFullKey + annotation.value() + delimiter; + } + return sectionFullKey + annotation.value(); } private IReceiverLocator findReceiverLocator(final Moonshine moonshine) @@ -110,13 +142,13 @@ private IReceiverLocator findReceiverLocator(final Moonshine receiverLocatorResolver = receiverLocatorResolverIterator.next().value(); final @Nullable IReceiverLocator resolvedLocator = - receiverLocatorResolver.resolve(this.reflectMethod, this.owner.getType()); + receiverLocatorResolver.resolve(this.reflectMethod, this.owner); if (resolvedLocator != null) { return resolvedLocator; } } - throw new NoReceiverLocatorFoundException(this.owner.getType(), this.reflectMethod); + throw new NoReceiverLocatorFoundException(this.owner, this.reflectMethod); } } diff --git a/core/src/test/java/net/kyori/moonshine/NestedMoonshineTest.java b/core/src/test/java/net/kyori/moonshine/NestedMoonshineTest.java new file mode 100644 index 0000000..5e8546c --- /dev/null +++ b/core/src/test/java/net/kyori/moonshine/NestedMoonshineTest.java @@ -0,0 +1,106 @@ +/* + * moonshine - A localisation library for Java. + * Copyright (C) Mariell Hoversholm + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +package net.kyori.moonshine; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.leangen.geantyref.TypeToken; +import net.kyori.moonshine.annotation.Message; +import net.kyori.moonshine.annotation.MessageSection; +import net.kyori.moonshine.exception.scan.UnscannableMethodException; +import net.kyori.moonshine.message.IMessageSource; +import net.kyori.moonshine.strategy.StandardPlaceholderResolverStrategy; +import net.kyori.moonshine.strategy.supertype.StandardSupertypeThenInterfaceSupertypeStrategy; +import net.kyori.moonshine.util.Unit; +import org.junit.jupiter.api.Test; + +class NestedMoonshineTest { + @Test + void simpleNestedMoonshine() throws UnscannableMethodException { + final SimpleNestedMoonshineRoot root = + this.moonshineWith( + SimpleNestedMoonshineRoot.class, + (receiver, messageKey) -> { + if ("test".equals(messageKey)) { + return "ok"; + } + return null; + }); + assertEquals("ok", root.child().test()); + } + + @Test + void complexNestedMoonshine() throws UnscannableMethodException { + final ComplexNestedMoonshineRoot root = + this.moonshineWith( + ComplexNestedMoonshineRoot.class, + (receiver, messageKey) -> { + if ("complex.child.child-test".equals(messageKey)) { + return "ok"; + } + if ("complex.child.child-oke-ok.done".equals(messageKey)) { + return "yes"; + } + return null; + }); + assertEquals("ok", root.child().test()); + assertEquals("yes", root.child().oke().done()); + } + + private T moonshineWith(final Class type, final IMessageSource sourced) + throws UnscannableMethodException { + return Moonshine.builder(TypeToken.get(type)) + .receiverLocatorResolver((method, proxy) -> (method1, proxy1, arguments) -> null, 1) + .sourced(sourced) + .rendered((receiver, intermediateMessage, resolvedPlaceholders, method, owner) -> intermediateMessage) + .sent((receiver, renderedMessage) -> {}) + .resolvingWithStrategy(new StandardPlaceholderResolverStrategy<>(new StandardSupertypeThenInterfaceSupertypeStrategy(false))) + .create(); + } + + interface SimpleNestedMoonshineRoot { + SimpleNestedMoonshineChild child(); + + @MessageSection + interface SimpleNestedMoonshineChild { + @Message("test") + String test(); + } + } + + @MessageSection("complex") + interface ComplexNestedMoonshineRoot { + @Message("child") + ComplexNestedMoonshineChild child(); + + @MessageSection(value = "child", delimiter = '-') + interface ComplexNestedMoonshineChild { + @Message("test") + String test(); + + @Message("oke") + ComplexNestedMoonshineChildChild oke(); + + @MessageSection("ok") + interface ComplexNestedMoonshineChildChild { + @Message("done") + String done(); + } + } + } +} diff --git a/standard/src/main/java/net/kyori/moonshine/exception/UnfinishedPlaceholderException.java b/standard/src/main/java/net/kyori/moonshine/exception/UnfinishedPlaceholderException.java index 19e0ca1..d30a017 100644 --- a/standard/src/main/java/net/kyori/moonshine/exception/UnfinishedPlaceholderException.java +++ b/standard/src/main/java/net/kyori/moonshine/exception/UnfinishedPlaceholderException.java @@ -30,7 +30,7 @@ public UnfinishedPlaceholderException(final MoonshineMethod moonshineMethod, super("The placeholder " + placeholderName + " was unfinished in method: " - + ReflectiveUtils.formatMethodName(moonshineMethod.owner().getType(), moonshineMethod.reflectMethod())); + + ReflectiveUtils.formatMethodName(moonshineMethod.owner(), moonshineMethod.reflectMethod())); this.moonshineMethod = moonshineMethod; this.placeholderName = placeholderName; this.placeholderValue = placeholderValue; diff --git a/standard/src/main/java/net/kyori/moonshine/strategy/StandardPlaceholderResolverStrategy.java b/standard/src/main/java/net/kyori/moonshine/strategy/StandardPlaceholderResolverStrategy.java index 29ef4af..e89a602 100644 --- a/standard/src/main/java/net/kyori/moonshine/strategy/StandardPlaceholderResolverStrategy.java +++ b/standard/src/main/java/net/kyori/moonshine/strategy/StandardPlaceholderResolverStrategy.java @@ -130,7 +130,7 @@ private void resolvePlaceholder(final Moonshine moonshine, final R r final var resolverResult = placeholderResolver.resolve(continuancePlaceholderName, value, receiver, - moonshineMethod.owner().getType(), + moonshineMethod.owner(), moonshineMethod.reflectMethod(), parameters); if (resolverResult == null) { // The resolver did not want to resolve this; pass it on.