Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support nested message sections #12

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[*]
end_of_line = lf
2 changes: 1 addition & 1 deletion build-logic/src/main/kotlin/moonshine.api.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
138 changes: 54 additions & 84 deletions core/src/main/java/net/kyori/moonshine/Moonshine.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -48,84 +55,42 @@
* @param <F> the finalised placeholder type, post-resolving
*/
@ThreadSafe
public final class Moonshine<R, I, O, F> {
/**
* The type which is being proxied with this Moonshine instance.
*/
private final TypeToken<?> proxiedType;

/**
* The proxy invocation handler instance.
*/
private final MoonshineInvocationHandler<R, I, O, F> invocationHandler;

/**
* The strategy for resolving placeholders on a method invocation.
*/
private final IPlaceholderResolverStrategy<R, I, F> placeholderResolverStrategy;

/**
* The source of intermediate messages, per receiver.
*/
private final IMessageSource<R, I> messageSource;

/**
* The renderer of all messages, before sent via {@link #messageSender()}.
*/
private final IMessageRenderer<R, I, O, F> messageRenderer;
public abstract sealed class Moonshine<R, I, O, F> 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<R, O> messageSender;
private @MonotonicNonNull Map<Method, MoonshineMethod<? extends R>> scannedMethods;

/**
* A navigable set for iterating through the {@link IReceiverLocatorResolver}s with weight-based ordering.
*/
private final NavigableSet<Weighted<? extends IReceiverLocatorResolver<? extends R>>> 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<Type, NavigableSet<Weighted<? extends IPlaceholderResolver<? extends R, ?, ? extends F>>>> 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<Method, MoonshineMethod<? extends R>> scannedMethods;

Moonshine(final TypeToken<?> proxiedType,
final IPlaceholderResolverStrategy<R, I, F> placeholderResolverStrategy,
final IMessageSource<R, I> messageSource,
final IMessageRenderer<R, I, O, F> messageRenderer,
final IMessageSender<R, O> messageSender,
final NavigableSet<Weighted<? extends IReceiverLocatorResolver<? extends R>>> weightedReceiverLocatorResolvers,
final Map<Type, NavigableSet<Weighted<? extends IPlaceholderResolver<? extends R, ?, ? extends F>>>> 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<Method, MoonshineMethod<? extends R>> scannedMethods = new HashMap<>(methods.length);
for (final Method method : methods) {
if (method.isDefault() || method.getReturnType() == Moonshine.class) {
continue;
}

final MoonshineMethod<? extends R> 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
Expand All @@ -138,42 +103,53 @@ public static <T, R> MoonshineBuilder.Receivers<T, R> builder(final TypeToken<T>
*/
@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<R, I, O, F> invocationHandler() {
return this.invocationHandler;
public @Nullable Moonshine<R, I, O, F> 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<R, I, F> placeholderResolverStrategy() {
return this.placeholderResolverStrategy;
}
public abstract IPlaceholderResolverStrategy<R, I, F> placeholderResolverStrategy();

/**
* @return an unmodifiable view of a navigable set for iterating through the available {@link
* IReceiverLocatorResolver}s with weight-based ordering
*/
@Pure
public NavigableSet<Weighted<? extends IReceiverLocatorResolver<? extends R>>> weightedReceiverLocatorResolvers() {
return this.weightedReceiverLocatorResolvers;
}
public abstract NavigableSet<Weighted<? extends IReceiverLocatorResolver<? extends R>>> 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<Type, NavigableSet<Weighted<? extends IPlaceholderResolver<? extends R, ?, ? extends F>>>> weightedPlaceholderResolvers() {
return this.weightedPlaceholderResolvers;
}
public abstract Map<Type, NavigableSet<Weighted<? extends IPlaceholderResolver<? extends R, ?, ? extends F>>>> weightedPlaceholderResolvers();

/**
* Find a scanned method by the given method mapping.
Expand All @@ -195,21 +171,15 @@ public MoonshineMethod<? extends R> scannedMethod(final Method method) throws Mi
/**
* @return the source of intermediate messages, per receiver
*/
public IMessageSource<R, I> messageSource() {
return this.messageSource;
}
public abstract IMessageSource<R, I> messageSource();

/**
* @return the renderer of messages, used before sending via {@link #messageSender()}
*/
public IMessageRenderer<R, I, O, F> messageRenderer() {
return this.messageRenderer;
}
public abstract IMessageRenderer<R, I, O, F> messageRenderer();

/**
* @return the message sender of intermediate messages to a given receiver with resolved placeholders
*/
public IMessageSender<R, O> messageSender() {
return this.messageSender;
}
public abstract IMessageSender<R, O> messageSender();
}
16 changes: 6 additions & 10 deletions core/src/main/java/net/kyori/moonshine/MoonshineBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -159,7 +157,7 @@ public Resolved<T, R, I, O, F> resolvingWithStrategy(

@NotThreadSafe
public static final class Resolved<T, R, I, O, F> {
private final TypeToken<T> proxiedType;
private final Type proxiedType;
private final NavigableSet<Weighted<? extends IReceiverLocatorResolver<? extends R>>> weightedReceiverLocatorResolvers;
private final IMessageSource<R, I> messageSource;
private final IMessageRenderer<R, I, O, F> messageRenderer;
Expand All @@ -173,7 +171,7 @@ private Resolved(final TypeToken<T> proxiedType,
final IMessageSource<R, I> messageSource, final IMessageRenderer<R, I, O, F> messageRenderer,
final IMessageSender<R, O> messageSender,
final IPlaceholderResolverStrategy<R, I, F> placeholderResolverStrategy) {
this.proxiedType = proxiedType;
this.proxiedType = proxiedType.getType();
this.weightedReceiverLocatorResolvers = weightedReceiverLocatorResolvers;
this.messageSource = messageSource;
this.messageRenderer = messageRenderer;
Expand Down Expand Up @@ -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<R, I, O, F> 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<R, I, O, F> moonshine = new MoonshineRoot<>(this.proxiedType, classLoader,
this.placeholderResolverStrategy, this.messageSource, this.messageRenderer, this.messageSender,
this.weightedReceiverLocatorResolvers, this.weightedPlaceholderResolvers);
return (T) moonshine.proxy();
}
}
}
80 changes: 80 additions & 0 deletions core/src/main/java/net/kyori/moonshine/MoonshineChild.java
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<R, I, O, F> extends Moonshine<R, I, O, F> {
private final Moonshine<R, I, O, F> parent;

public MoonshineChild(
final Type proxiedType,
final ClassLoader proxyClassLoader,
final Moonshine<R, I, O, F> parent,
final String inheritedKey)
throws UnscannableMethodException {
super(proxiedType, proxyClassLoader);
this.parent = parent;
scanMethods(inheritedKey + proxiedTypeKey(), proxyClassLoader);
}

@Override
public Moonshine<R, I, O, F> parent() {
return this.parent;
}

@Override
public IPlaceholderResolverStrategy<R, I, F> placeholderResolverStrategy() {
return this.parent.placeholderResolverStrategy();
}

@Override
public NavigableSet<Weighted<? extends IReceiverLocatorResolver<? extends R>>> weightedReceiverLocatorResolvers() {
return this.parent.weightedReceiverLocatorResolvers();
}

@Override
public Map<Type, NavigableSet<Weighted<? extends IPlaceholderResolver<? extends R, ?, ? extends F>>>> weightedPlaceholderResolvers() {
return this.parent.weightedPlaceholderResolvers();
}

@Override
public IMessageSource<R, I> messageSource() {
return this.parent.messageSource();
}

@Override
public IMessageRenderer<R, I, O, F> messageRenderer() {
return this.parent.messageRenderer();
}

@Override
public IMessageSender<R, O> messageSender() {
return this.parent.messageSender();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Loading