Skip to content

Commit 84f1e70

Browse files
committed
first ConfigurationManager implementation
1 parent fa302fa commit 84f1e70

9 files changed

Lines changed: 469 additions & 53 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.comroid.api.config;
2+
3+
import java.lang.annotation.Retention;
4+
import java.lang.annotation.RetentionPolicy;
5+
6+
/** indicates that a value should not be displayed in editors */
7+
@Retention(RetentionPolicy.RUNTIME)
8+
public @interface Adapt {
9+
/** determines a class that should be {@linkplain org.comroid.api.java.JITAssistant#prepare(Class[]) loaded} first in order to initialize it to cache */
10+
Class<?>[] value() default { };
11+
}
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
package org.comroid.api.config;
2+
3+
import lombok.SneakyThrows;
4+
import lombok.Value;
5+
import lombok.experimental.NonFinal;
6+
import net.dv8tion.jda.api.EmbedBuilder;
7+
import net.dv8tion.jda.api.entities.IMentionable;
8+
import net.dv8tion.jda.api.entities.Role;
9+
import net.dv8tion.jda.api.entities.User;
10+
import net.dv8tion.jda.api.entities.channel.Channel;
11+
import net.dv8tion.jda.api.entities.channel.ChannelType;
12+
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
13+
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
14+
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
15+
import net.dv8tion.jda.api.events.interaction.component.EntitySelectInteractionEvent;
16+
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
17+
import net.dv8tion.jda.api.hooks.ListenerAdapter;
18+
import net.dv8tion.jda.api.interactions.components.buttons.Button;
19+
import net.dv8tion.jda.api.interactions.components.selections.EntitySelectMenu;
20+
import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu;
21+
import net.dv8tion.jda.api.interactions.components.text.TextInput;
22+
import net.dv8tion.jda.api.interactions.components.text.TextInputStyle;
23+
import net.dv8tion.jda.api.interactions.modals.Modal;
24+
import org.comroid.annotations.internal.Annotations;
25+
import org.comroid.api.attr.Aliased;
26+
import org.comroid.api.attr.Named;
27+
import org.comroid.api.config.adapter.TypeAdapter;
28+
import org.comroid.api.data.bind.DataStructure;
29+
import org.comroid.api.data.seri.DataNode;
30+
import org.comroid.api.data.seri.MimeType;
31+
import org.comroid.api.func.exc.ThrowingFunction;
32+
import org.comroid.api.func.exc.ThrowingSupplier;
33+
import org.comroid.api.func.ext.Context;
34+
import org.comroid.api.io.FileHandle;
35+
import org.comroid.api.java.Activator;
36+
import org.comroid.api.java.JITAssistant;
37+
import org.comroid.api.text.Capitalization;
38+
import org.jetbrains.annotations.NotNull;
39+
40+
import java.awt.*;
41+
import java.io.File;
42+
import java.io.FileOutputStream;
43+
import java.lang.reflect.Field;
44+
import java.nio.charset.StandardCharsets;
45+
import java.time.Instant;
46+
import java.util.Arrays;
47+
import java.util.Optional;
48+
import java.util.UUID;
49+
import java.util.stream.Collectors;
50+
import java.util.stream.IntStream;
51+
52+
import static org.comroid.api.Polyfill.*;
53+
import static org.comroid.api.text.Markdown.*;
54+
55+
@Value
56+
public class ConfigurationManager<T extends DataNode> {
57+
UUID uuid = UUID.randomUUID();
58+
Context context;
59+
DataStructure<T> struct;
60+
File file;
61+
MimeType dataType;
62+
T config;
63+
@NonFinal Instant timestamp = Instant.EPOCH;
64+
65+
public ConfigurationManager(Context context, Class<T> type, String filePath) {
66+
this(context, type, filePath, MimeType.JSON);
67+
}
68+
69+
public ConfigurationManager(Context context, Class<T> type, String filePath, MimeType dataType) {
70+
this.context = context;
71+
this.struct = DataStructure.of(type);
72+
this.file = new FileHandle(filePath);
73+
this.dataType = dataType;
74+
this.config = ctor();
75+
76+
invokeAdapters();
77+
}
78+
79+
public T initialize() {
80+
if (!file.exists()) save(); // save default config
81+
reload();
82+
return config;
83+
}
84+
85+
public void reload() {
86+
if (ftime().isBefore(timestamp)) return; // reload is not necessary
87+
this.timestamp = Instant.now();
88+
}
89+
90+
@SneakyThrows
91+
public void save() {
92+
DataNode data = config;
93+
var prefix = dataType.getSerializerPrefix();
94+
if (prefix != null) data = prefix.apply(data);
95+
try (var fos = new FileOutputStream(file)) {
96+
fos.write(data.toSerializedString().getBytes(StandardCharsets.UTF_8));
97+
}
98+
timestamp = ftime();
99+
}
100+
101+
private void invokeAdapters() {
102+
invokePropertyAdaptersRecursive(context, config, struct, config);
103+
}
104+
105+
private Instant ftime() {
106+
return Instant.ofEpochMilli(file.lastModified());
107+
}
108+
109+
private T ctor() {
110+
return uncheckedCast(Activator.get(struct.getType()).createInstance(DataNode.Value.NULL));
111+
}
112+
113+
private void invokePropertyAdaptersRecursive(Context context, DataNode node, DataStructure<?> struct, Object it) {
114+
if (node.isNull()) return;
115+
for (var property : struct.getProperties()) {
116+
var propType = property.getType();
117+
if (propType.isStandard()) continue;
118+
var propClass = propType.getTargetClass();
119+
var propStruct = DataStructure.of(propClass);
120+
121+
// try recurse
122+
invokePropertyAdaptersRecursive(context, node.get(property.getName()), propStruct, property.getFrom(it));
123+
124+
// initialize dependencies
125+
if (!property.isAnnotationPresent(Adapt.class)) continue;
126+
JITAssistant.prepare(property.getAnnotation(Adapt.class).value()).join();
127+
128+
// find & apply adapter
129+
Optional.ofNullable(TypeAdapter.CACHE.getOrDefault(propClass, null)).map(adp -> {
130+
var key = property.getName() + Capitalization.Title_Case.convert(adp.getNameSuffix());
131+
var value = node.get(key).as(adp.getSerialized()).orElseGet(() -> Annotations.defaultValue(property));
132+
return adp.deserialize(context, uncheckedCast(value));
133+
}).ifPresent(value -> property.setFor(it, uncheckedCast(value)));
134+
}
135+
}
136+
137+
public interface Presentation {
138+
void clear();
139+
140+
void refresh();
141+
142+
default void resend() {
143+
clear();
144+
refresh();
145+
}
146+
}
147+
148+
@Value
149+
public class Presentation$JDA implements Presentation {
150+
TextChannel channel;
151+
152+
@Override
153+
public void clear() {
154+
channel.getHistory().retrievePast(100).flatMap(channel::deleteMessages).queue();
155+
}
156+
157+
@Override
158+
public void refresh() {
159+
for (var property : struct.getProperties())
160+
sendAttributeMessageRecursive(property.getName(), property, config, 1);
161+
}
162+
163+
private void sendAttributeMessageRecursive(String fullName, DataStructure<?>.Property<?> property, Object it, int level) {
164+
var title = IntStream.range(0, level).mapToObj($ -> "#").collect(Collectors.joining()) + " Config Value " + Code.apply(fullName);
165+
var menuId = uuid.toString() + ':' + fullName;
166+
var propType = property.getType();
167+
var propClass = propType.getTargetClass();
168+
169+
ifs:
170+
{
171+
if (!propType.isStandard()) {
172+
var from = property.getFrom(it);
173+
var struct = DataStructure.of(propClass);
174+
for (var subProperty : struct.getProperties())
175+
sendAttributeMessageRecursive(fullName + '.' + subProperty.getName(), subProperty, from, level + 1);
176+
} else if (property.isAnnotationPresent(Adapt.class)) {
177+
// prepare dependencies
178+
JITAssistant.prepare(property.getAnnotation(Adapt.class).value()).join();
179+
180+
// try send mentionable selection box
181+
if (IMentionable.class.isAssignableFrom(propClass)) {
182+
// choose SelectTarget
183+
var target = getSelectTarget(property);
184+
var builder = EntitySelectMenu.create(menuId, target).setRequiredRange(1, 1);
185+
186+
// set default value
187+
EntitySelectMenu.DefaultValue def;
188+
var currentId = (long) property.getFrom(it);
189+
if (Channel.class.isAssignableFrom(propClass)) def = EntitySelectMenu.DefaultValue.channel(currentId);
190+
else if (User.class.isAssignableFrom(propClass)) def = EntitySelectMenu.DefaultValue.user(currentId);
191+
else if (Role.class.isAssignableFrom(propClass)) def = EntitySelectMenu.DefaultValue.role(currentId);
192+
else throw new IllegalArgumentException("Invalid mentionable: " + propClass.getCanonicalName());
193+
builder.setDefaultValues(def);
194+
195+
// choose ChannelType if necessary
196+
if (target == EntitySelectMenu.SelectTarget.CHANNEL) builder.setChannelTypes(Arrays.stream(ChannelType.values())
197+
.filter(type -> type.getInterface().isAssignableFrom(propClass))
198+
.toList());
199+
200+
// send mentionable selection box
201+
var listener = new ListenerAdapter[1];
202+
listener[0] = new ListenerAdapter() {
203+
@Override
204+
public void onEntitySelectInteraction(@NotNull EntitySelectInteractionEvent event) {
205+
if (!menuId.equals(event.getComponentId())) return;
206+
event.getValues().stream().findAny().ifPresent(mentionable -> property.setFor(it, uncheckedCast(mentionable)));
207+
//channel.getJDA().removeEventListener(listener[0]);
208+
}
209+
};
210+
channel.getJDA().addEventListener(listener[0]);
211+
channel.sendMessage(title).addActionRow(builder.build()).queue();
212+
} else break ifs;
213+
} else if (propClass.isEnum()) {
214+
// prepare enum selection box
215+
var menu = StringSelectMenu.create(menuId);
216+
Arrays.stream(propClass.getFields())
217+
.filter(Field::isEnumConstant)
218+
.forEach(field -> menu.addOption(Aliased.$(field)
219+
.findAny()
220+
.or(() -> Optional.ofNullable(ThrowingSupplier.sneaky(() -> Named.$(field.get(null))).get()))
221+
.orElseGet(field::getName), field.getName(), Annotations.descriptionText(field)));
222+
223+
// send enum selection box
224+
var listener = new ListenerAdapter[1];
225+
listener[0] = new ListenerAdapter() {
226+
@Override
227+
public void onStringSelectInteraction(@NotNull StringSelectInteractionEvent event) {
228+
if (!menuId.equals(event.getComponentId())) return;
229+
event.getValues()
230+
.stream()
231+
.flatMap(value -> Arrays.stream(propClass.getFields())
232+
.filter(Field::isEnumConstant)
233+
.filter(field -> value.equals(Aliased.$(field)
234+
.findAny()
235+
.or(() -> Optional.ofNullable(ThrowingSupplier.sneaky(() -> Named.$(field.get(null))).get()))
236+
.orElseGet(field::getName))))
237+
.findAny()
238+
.map(ThrowingFunction.sneaky(field -> field.get(null)))
239+
.ifPresent(value -> property.setFor(it, uncheckedCast(value)));
240+
//channel.getJDA().removeEventListener(listener[0]);
241+
}
242+
};
243+
channel.getJDA().addEventListener(listener[0]);
244+
channel.sendMessage(title).addActionRow(menu.build()).queue();
245+
}
246+
return;
247+
}
248+
249+
// just send a simple textbox-based editor message
250+
var listener = new ListenerAdapter[1];
251+
listener[0] = new ListenerAdapter() {
252+
@Override
253+
public void onButtonInteraction(@NotNull ButtonInteractionEvent event) {
254+
if (!(menuId + ":button").equals(event.getComponentId())) return;
255+
event.replyModal(Modal.create(menuId + ":modal", title)
256+
.addActionRow(TextInput.create("newValue", "New Value", TextInputStyle.SHORT).build())
257+
.build()).queue();
258+
}
259+
260+
@Override
261+
public void onModalInteraction(@NotNull ModalInteractionEvent event) {
262+
if (!(menuId + ":modal").equals(event.getModalId())) return;
263+
var newValue = event.getValue("newValue");
264+
var value = propType.parse(String.valueOf(newValue));
265+
property.setFor(it, uncheckedCast(value));
266+
//channel.getJDA().removeEventListener(listener[0]);
267+
}
268+
};
269+
channel.getJDA().addEventListener(listener[0]);
270+
channel.sendMessageEmbeds(new EmbedBuilder().setTitle(title)
271+
.setColor(new Color(86, 98, 246))
272+
.setDescription(String.join("\n", property.getDescription()))
273+
.addField("Current Value [" + Code.apply(propType.getName()) + "]", CodeBlock.apply(String.valueOf(property.getFrom(it))), false)
274+
.build()).addActionRow(Button.primary(menuId + ":button", "Change Value...")).queue();
275+
}
276+
277+
private static EntitySelectMenu.@NotNull SelectTarget getSelectTarget(DataStructure<?>.Property<?> property) {
278+
EntitySelectMenu.SelectTarget target;
279+
if (Channel.class.isAssignableFrom(property.getType().getTargetClass())) target = EntitySelectMenu.SelectTarget.CHANNEL;
280+
else if (User.class.isAssignableFrom(property.getType().getTargetClass())) target = EntitySelectMenu.SelectTarget.USER;
281+
else if (Role.class.isAssignableFrom(property.getType().getTargetClass())) target = EntitySelectMenu.SelectTarget.ROLE;
282+
else throw new IllegalArgumentException("Invalid mentionable: " + property.getType().getTargetClass().getCanonicalName());
283+
return target;
284+
}
285+
}
286+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package org.comroid.api.config;
2+
3+
/** indicates that a value should not be displayed in editors */
4+
public @interface WriteOnly {}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package org.comroid.api.config.adapter;
2+
3+
import lombok.Value;
4+
import lombok.experimental.NonFinal;
5+
import org.comroid.api.data.seri.type.StandardValueType;
6+
import org.comroid.api.func.ext.Context;
7+
import org.jetbrains.annotations.NotNull;
8+
import org.jetbrains.annotations.Nullable;
9+
10+
import java.util.Collections;
11+
import java.util.Map;
12+
import java.util.concurrent.ConcurrentHashMap;
13+
14+
@Value
15+
@NonFinal
16+
public abstract class TypeAdapter<T, S> {
17+
private static final Map<Class<?>, TypeAdapter<?, ?>> cache = new ConcurrentHashMap<>();
18+
public static final Map<Class<?>, TypeAdapter<?, ?>> CACHE = Collections.unmodifiableMap(cache);
19+
20+
Class<T> type;
21+
StandardValueType<S> serialized;
22+
String nameSuffix;
23+
24+
protected TypeAdapter(Class<T> type, StandardValueType<S> serialized, String nameSuffix) {
25+
this.type = type;
26+
this.serialized = serialized;
27+
this.nameSuffix = nameSuffix;
28+
29+
cache.put(type, this);
30+
}
31+
32+
public abstract @NotNull S toSerializable(Context context, T value);
33+
34+
public abstract @Nullable T deserialize(Context context, S serialized);
35+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.comroid.api.config.adapter.impl;
2+
3+
import lombok.Value;
4+
import lombok.experimental.NonFinal;
5+
import net.dv8tion.jda.api.JDA;
6+
import net.dv8tion.jda.api.entities.ISnowflake;
7+
import net.dv8tion.jda.api.entities.channel.concrete.NewsChannel;
8+
import org.comroid.api.config.adapter.TypeAdapter;
9+
import org.comroid.api.data.seri.type.StandardValueType;
10+
import org.comroid.api.func.ext.Context;
11+
import org.jetbrains.annotations.NotNull;
12+
13+
import java.util.Arrays;
14+
import java.util.stream.Stream;
15+
16+
@Value
17+
@NonFinal
18+
public abstract class JdaTypeAdapter<T extends ISnowflake> extends TypeAdapter<T, @NotNull Long> {
19+
public static final JdaTypeAdapter<NewsChannel> NEWS_CHANNEL = new JdaTypeAdapter<>(NewsChannel.class) {
20+
@Override
21+
protected Stream<NewsChannel> findAll(JDA jda) {
22+
return jda.getNewsChannels().stream();
23+
}
24+
25+
@Override
26+
public NewsChannel deserialize(Context context, @NotNull Long id) {
27+
return context.getFromContext(JDA.class, false).assertion().getNewsChannelById(id);
28+
}
29+
};
30+
31+
public JdaTypeAdapter(Class<T> type) {
32+
super(type, StandardValueType.LONG, "id");
33+
}
34+
35+
@Override
36+
public @NotNull Long toSerializable(Context context, T value) {
37+
return value.getIdLong();
38+
}
39+
40+
protected abstract Stream<T> findAll(JDA jda);
41+
42+
protected Stream<T> findAllById(JDA jda, long... id) {
43+
return findAll(jda).filter(i -> Arrays.binarySearch(id, i.getIdLong()) >= 0);
44+
}
45+
}

0 commit comments

Comments
 (0)