mirror of
https://github.com/CarmJos/EasyConfiguration.git
synced 2026-06-04 10:38:19 +08:00
feat(json): Finished json version's provider
This commit is contained in:
@@ -2,27 +2,31 @@ package cc.carm.lib.configuration.source.section;
|
||||
|
||||
import cc.carm.lib.configuration.function.DataFunction;
|
||||
import cc.carm.lib.configuration.source.option.StandardOptions;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
import org.jetbrains.annotations.*;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public interface ConfigureSection {
|
||||
|
||||
@NotNull ConfigureSource<?, ?, ?> source();
|
||||
|
||||
@Nullable ConfigureSection parent();
|
||||
|
||||
default char separator() {
|
||||
return source().holder().options().get(StandardOptions.PATH_SEPARATOR);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@UnmodifiableView
|
||||
default Set<String> getKeys(boolean deep) {
|
||||
return getValues(deep).keySet();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@UnmodifiableView
|
||||
Map<String, Object> getValues(boolean deep);
|
||||
|
||||
void set(@NotNull String path, @Nullable Object value);
|
||||
@@ -195,56 +199,125 @@ public interface ConfigureSection {
|
||||
return get(path, def, String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of values from the section
|
||||
* <p>
|
||||
* If the path does not exist, an empty list will be returned
|
||||
* <br>Any changes please use {@link #set(String, Object)} after changes
|
||||
*
|
||||
* @param path The path to get the list from
|
||||
* @param parser The function to parse the values
|
||||
* @param <V> The type of the values
|
||||
* @return The list of values
|
||||
*/
|
||||
default <V> @NotNull List<V> getList(@NotNull String path, @NotNull DataFunction<Object, V> parser) {
|
||||
return parseList(getList(path), parser);
|
||||
return getCollection(path, ArrayList::new, parser);
|
||||
}
|
||||
|
||||
@Unmodifiable
|
||||
/**
|
||||
* Get a list of strings from the section
|
||||
* <p> Limitations see {@link #getList(String, DataFunction)}
|
||||
*
|
||||
* @param path The path to get the list from
|
||||
* @return The list of strings
|
||||
*/
|
||||
default @NotNull List<String> getStringList(@NotNull String path) {
|
||||
return getList(path, DataFunction.castToString());
|
||||
}
|
||||
|
||||
@Unmodifiable
|
||||
/**
|
||||
* Get a list of integer from the section
|
||||
* <p> Limitations see {@link #getList(String, DataFunction)}
|
||||
*
|
||||
* @param path The path to get the list from
|
||||
* @return The list of int values
|
||||
*/
|
||||
default @NotNull List<Integer> getIntegerList(@NotNull String path) {
|
||||
return getList(path, DataFunction.intValue());
|
||||
}
|
||||
|
||||
@Unmodifiable
|
||||
/**
|
||||
* Get a list of long from the section
|
||||
* <p> Limitations see {@link #getList(String, DataFunction)}
|
||||
*
|
||||
* @param path The path to get the list from
|
||||
* @return The list of long values
|
||||
*/
|
||||
default @NotNull List<Long> getLongList(@NotNull String path) {
|
||||
return getList(path, DataFunction.longValue());
|
||||
}
|
||||
|
||||
@Unmodifiable
|
||||
/**
|
||||
* Get a list of double from the section
|
||||
* <p> Limitations see {@link #getList(String, DataFunction)}
|
||||
*
|
||||
* @param path The path to get the list from
|
||||
* @return The list of doubles
|
||||
*/
|
||||
default @NotNull List<Double> getDoubleList(@NotNull String path) {
|
||||
return getList(path, DataFunction.doubleValue());
|
||||
}
|
||||
|
||||
@Unmodifiable
|
||||
/**
|
||||
* Get a list of floats from the section
|
||||
* <p> Limitations see {@link #getList(String, DataFunction)}
|
||||
*
|
||||
* @param path The path to get the list from
|
||||
* @return The list of floats
|
||||
*/
|
||||
default @NotNull List<Float> getFloatList(@NotNull String path) {
|
||||
return getList(path, DataFunction.floatValue());
|
||||
}
|
||||
|
||||
@Unmodifiable
|
||||
/**
|
||||
* Get a list of bytes from the section
|
||||
* <p> Limitations see {@link #getList(String, DataFunction)}
|
||||
*
|
||||
* @param path The path to get the list from
|
||||
* @return The list of bytes
|
||||
*/
|
||||
default @NotNull List<Byte> getByteList(@NotNull String path) {
|
||||
return getList(path, DataFunction.byteValue());
|
||||
}
|
||||
|
||||
@Unmodifiable
|
||||
/**
|
||||
* Get a list of char from the section
|
||||
* <p> Limitations see {@link #getList(String, DataFunction)}
|
||||
*
|
||||
* @param path The path to get the list from
|
||||
* @return The list of char
|
||||
*/
|
||||
default @NotNull List<Character> getCharList(@NotNull String path) {
|
||||
return getList(path, DataFunction.castObject(Character.class));
|
||||
}
|
||||
|
||||
@Unmodifiable
|
||||
static <T> @NotNull List<T> parseList(@Nullable List<?> list, DataFunction<Object, T> parser) {
|
||||
if (list == null) return Collections.emptyList();
|
||||
List<T> values = new ArrayList<>();
|
||||
for (Object o : list) {
|
||||
default <T, C extends Collection<T>> @NotNull C getCollection(@NotNull String path,
|
||||
@NotNull Supplier<C> constructor,
|
||||
@NotNull DataFunction<Object, T> parser) {
|
||||
return parseCollection(getList(path), constructor, parser);
|
||||
}
|
||||
|
||||
default @NotNull Stream<?> stream(@NotNull String path) {
|
||||
List<?> values = getList(path);
|
||||
return values == null ? Stream.empty() : values.stream();
|
||||
}
|
||||
|
||||
default <T> @NotNull Stream<T> stream(@NotNull String path, @NotNull Function<Object, T> parser) {
|
||||
return stream(path).map(parser);
|
||||
}
|
||||
|
||||
static <T, C extends Collection<T>> @NotNull C parseCollection(
|
||||
@Nullable List<?> data, @NotNull Supplier<C> constructor,
|
||||
@NotNull DataFunction<Object, T> parser
|
||||
) {
|
||||
C values = constructor.get();
|
||||
if (data == null) return values;
|
||||
for (Object obj : data) {
|
||||
try {
|
||||
values.add(parser.handle(o));
|
||||
values.add(parser.handle(obj));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public abstract class ConfigureSource<
|
||||
public abstract @NotNull ORIGINAL original();
|
||||
|
||||
/**
|
||||
* @return Configuration section
|
||||
* @return The root {@link ConfigureSection}
|
||||
*/
|
||||
public abstract @NotNull SECTION section();
|
||||
|
||||
@@ -54,6 +54,11 @@ public abstract class ConfigureSource<
|
||||
return getLastUpdateMillis() > parsedTime;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ConfigureSection parent() {
|
||||
return null; // Source also represents the root section, so it has no parent
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Map<String, Object> getValues(boolean deep) {
|
||||
return section().getValues(deep);
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package cc.carm.lib.configuration.annotation;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Footer Comments.
|
||||
* Add a comment to the bottom of the corresponding configuration for easy reading and viewing.
|
||||
* <p>e.g.
|
||||
* <blockquote><pre>
|
||||
* foo: "bar"
|
||||
* # The first line of the comment
|
||||
* # The second line of the comment
|
||||
* </pre></blockquote>
|
||||
*/
|
||||
@Target({ElementType.TYPE, ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface FooterComment {
|
||||
|
||||
/**
|
||||
* If the content of the note is 0, it will be treated as a blank line.
|
||||
* <p> e.g. <b>{"foo","","bar"}</b>
|
||||
* Will be set as
|
||||
* <blockquote><pre>
|
||||
* foo: "bar"
|
||||
* # foo
|
||||
*
|
||||
* # bar
|
||||
* </pre></blockquote>
|
||||
*
|
||||
* @return The content of this comment
|
||||
*/
|
||||
@NotNull
|
||||
String[] value() default "";
|
||||
|
||||
}
|
||||
+10
@@ -1,5 +1,6 @@
|
||||
package cc.carm.lib.configuration.commentable;
|
||||
|
||||
import cc.carm.lib.configuration.annotation.FooterComment;
|
||||
import cc.carm.lib.configuration.annotation.HeaderComment;
|
||||
import cc.carm.lib.configuration.annotation.InlineComment;
|
||||
import cc.carm.lib.configuration.source.ConfigurationHolder;
|
||||
@@ -18,6 +19,11 @@ public interface CommentableMetaTypes {
|
||||
*/
|
||||
ConfigurationMetadata<List<String>> HEADER_COMMENTS = ConfigurationMetadata.of(Collections.emptyList());
|
||||
|
||||
/**
|
||||
* Configuration's footer comments
|
||||
*/
|
||||
ConfigurationMetadata<List<String>> FOOTER_COMMENTS = ConfigurationMetadata.of(Collections.emptyList());
|
||||
|
||||
/**
|
||||
* Configuration's {@link InlineComment}
|
||||
*/
|
||||
@@ -33,6 +39,10 @@ public interface CommentableMetaTypes {
|
||||
HeaderComment.class, HEADER_COMMENTS,
|
||||
a -> Arrays.asList(a.value())
|
||||
);
|
||||
initializer.registerAnnotation(
|
||||
FooterComment.class, FOOTER_COMMENTS,
|
||||
a -> Arrays.asList(a.value())
|
||||
);
|
||||
initializer.registerAnnotation(InlineComment.class, INLINE_COMMENT, InlineComment::value);
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -4,11 +4,11 @@ import cc.carm.lib.configuration.source.option.ConfigurationOption;
|
||||
|
||||
public interface CommentableOptions {
|
||||
|
||||
/**
|
||||
* Whether to keep modified comments in configuration,
|
||||
* that means we only set comments for values that are not exists in configuration.
|
||||
*/
|
||||
ConfigurationOption<Boolean> KEEP_COMMENTS = ConfigurationOption.of(true);
|
||||
// /**
|
||||
// * Whether to keep modified comments in configuration,
|
||||
// * that means we only set comments for values that are not exists in configuration.
|
||||
// */
|
||||
// ConfigurationOption<Boolean> KEEP_COMMENTS = ConfigurationOption.of(true);
|
||||
|
||||
/**
|
||||
* Whether to comment values name that are not exists in configuration and no default value offered.
|
||||
@@ -19,6 +19,6 @@ public interface CommentableOptions {
|
||||
* # foo:
|
||||
* </pre></blockquote>
|
||||
*/
|
||||
ConfigurationOption<Boolean> COMMENT_NO_DEFAULT = ConfigurationOption.of(true);
|
||||
ConfigurationOption<Boolean> COMMENT_EMPTY_VALUE = ConfigurationOption.of(true);
|
||||
|
||||
}
|
||||
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
package cc.carm.lib.configuration.source;
|
||||
package cc.carm.lib.configuration.source.file;
|
||||
|
||||
import cc.carm.lib.configuration.source.ConfigurationFactory;
|
||||
import cc.carm.lib.configuration.source.ConfigurationHolder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
+3
-2
@@ -1,7 +1,8 @@
|
||||
package cc.carm.lib.configuration.source;
|
||||
package cc.carm.lib.configuration.source.file;
|
||||
|
||||
import cc.carm.lib.configuration.function.DataFunction;
|
||||
import cc.carm.lib.configuration.option.FileConfigOptions;
|
||||
import cc.carm.lib.configuration.source.ConfigurationHolder;
|
||||
import cc.carm.lib.configuration.source.option.FileConfigOptions;
|
||||
import cc.carm.lib.configuration.source.section.ConfigureSection;
|
||||
import cc.carm.lib.configuration.source.section.ConfigureSource;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package cc.carm.lib.configuration.option;
|
||||
package cc.carm.lib.configuration.source.option;
|
||||
|
||||
import cc.carm.lib.configuration.source.option.ConfigurationOption;
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
package cc.carm.lib.configuration.json;
|
||||
package cc.carm.lib.configuration.source.json;
|
||||
|
||||
import cc.carm.lib.configuration.source.ConfigurationHolder;
|
||||
import cc.carm.lib.configuration.source.FileConfigFactory;
|
||||
import cc.carm.lib.configuration.source.file.FileConfigFactory;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
+24
-14
@@ -1,4 +1,4 @@
|
||||
package cc.carm.lib.configuration.json;
|
||||
package cc.carm.lib.configuration.source.json;
|
||||
|
||||
import cc.carm.lib.configuration.source.section.ConfigureSection;
|
||||
import cc.carm.lib.configuration.source.section.ConfigureSource;
|
||||
@@ -9,24 +9,26 @@ import java.util.*;
|
||||
|
||||
public class JSONSection implements ConfigureSection {
|
||||
|
||||
protected final ConfigureSource<? extends JSONSection, ?, ?> source;
|
||||
protected final Map<String, Object> data;
|
||||
protected final @NotNull ConfigureSource<? extends JSONSection, ?, ?> source;
|
||||
protected final @NotNull Map<String, Object> data;
|
||||
protected final @Nullable JSONSection parent;
|
||||
|
||||
public JSONSection(@NotNull ConfigureSource<? extends JSONSection, ?, ?> source,
|
||||
@NotNull Map<?, ?> data) {
|
||||
@NotNull Map<?, ?> data, @Nullable JSONSection parent) {
|
||||
this.source = source;
|
||||
this.parent = parent;
|
||||
this.data = new LinkedHashMap<>();
|
||||
|
||||
for (Map.Entry<?, ?> entry : data.entrySet()) {
|
||||
String key = (entry.getKey() == null) ? "null" : entry.getKey().toString();
|
||||
|
||||
if (entry.getValue() instanceof Map) {
|
||||
this.data.put(key, new JSONSection(source, (Map<?, ?>) entry.getValue()));
|
||||
this.data.put(key, new JSONSection(source, (Map<?, ?>) entry.getValue(), this));
|
||||
} else if (entry.getValue() instanceof List) {
|
||||
List<Object> list = new ArrayList<>();
|
||||
for (Object obj : (List<?>) entry.getValue()) {
|
||||
if (obj instanceof Map) {
|
||||
list.add(new JSONSection(source, (Map<?, ?>) obj));
|
||||
list.add(new JSONSection(source, (Map<?, ?>) obj, this));
|
||||
} else {
|
||||
list.add(obj);
|
||||
}
|
||||
@@ -43,13 +45,12 @@ public class JSONSection implements ConfigureSection {
|
||||
return this.source;
|
||||
}
|
||||
|
||||
public Map<String, Object> data() {
|
||||
public @NotNull Map<String, Object> data() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Set<String> getKeys(boolean deep) {
|
||||
return getValues(deep).keySet();
|
||||
public @Nullable JSONSection parent() {
|
||||
return this.parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -57,14 +58,14 @@ public class JSONSection implements ConfigureSection {
|
||||
if (deep) {
|
||||
Map<String, Object> values = new LinkedHashMap<>();
|
||||
mapChildrenValues(values, this, null, true);
|
||||
return values;
|
||||
} else return new LinkedHashMap<>(this.data);
|
||||
return Collections.unmodifiableMap(values);
|
||||
} else return Collections.unmodifiableMap(data());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(@NotNull String path, @Nullable Object value) {
|
||||
if (value instanceof Map) {
|
||||
value = new JSONSection(source(), (Map<?, ?>) value);
|
||||
value = new JSONSection(source(), (Map<?, ?>) value, this);
|
||||
}
|
||||
|
||||
JSONSection section = getSectionFor(path);
|
||||
@@ -119,7 +120,7 @@ public class JSONSection implements ConfigureSection {
|
||||
String root = path.substring(0, index);
|
||||
Object section = this.data.get(root);
|
||||
if (section == null) {
|
||||
section = new JSONSection(source(), new LinkedHashMap<>());
|
||||
section = new JSONSection(source(), new LinkedHashMap<>(), this);
|
||||
this.data.put(root, section);
|
||||
}
|
||||
|
||||
@@ -132,6 +133,15 @@ public class JSONSection implements ConfigureSection {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Map the values of the children of the section to the output map.
|
||||
*
|
||||
* @param output The map to map the values to
|
||||
* @param section The section to map the values from
|
||||
* @param parent The parent path
|
||||
* @param deep If the mapping should be deep
|
||||
* @author md_5, sk89q
|
||||
*/
|
||||
protected void mapChildrenValues(@NotNull Map<String, Object> output, @NotNull JSONSection section,
|
||||
@Nullable String parent, boolean deep) {
|
||||
for (Map.Entry<String, Object> entry : section.data().entrySet()) {
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
package cc.carm.lib.configuration.json;
|
||||
package cc.carm.lib.configuration.source.json;
|
||||
|
||||
import cc.carm.lib.configuration.source.ConfigurationHolder;
|
||||
import cc.carm.lib.configuration.source.FileConfigSource;
|
||||
import cc.carm.lib.configuration.source.file.FileConfigSource;
|
||||
import cc.carm.lib.configuration.source.section.ConfigureSource;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
@@ -22,7 +22,7 @@ public class JSONSource extends FileConfigSource<JSONSection, Map<?, ?>, JSONSou
|
||||
).create();
|
||||
|
||||
protected final @NotNull Gson gson;
|
||||
protected @Nullable JSONSection section;
|
||||
protected @Nullable JSONSection rootSection;
|
||||
|
||||
protected JSONSource(@NotNull ConfigurationHolder<? extends JSONSource> holder, long lastUpdateMillis,
|
||||
@NotNull File file, @Nullable String resourcePath) {
|
||||
@@ -62,7 +62,7 @@ public class JSONSource extends FileConfigSource<JSONSection, Map<?, ?>, JSONSou
|
||||
|
||||
@Override
|
||||
public @NotNull JSONSection section() {
|
||||
return Objects.requireNonNull(this.section, "Section is not initialized");
|
||||
return Objects.requireNonNull(this.rootSection, "Section is not initialized");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -74,7 +74,7 @@ public class JSONSource extends FileConfigSource<JSONSection, Map<?, ?>, JSONSou
|
||||
protected void onReload() throws Exception {
|
||||
Map<?, ?> data = fileReader(reader -> gson.fromJson(reader, LinkedHashMap.class));
|
||||
if (data == null) data = new LinkedHashMap<>();
|
||||
this.section = new JSONSection(this, data);
|
||||
this.rootSection = new JSONSection(this, data, null);
|
||||
this.lastUpdateMillis = System.currentTimeMillis(); // 更新时间
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
package config;
|
||||
|
||||
import cc.carm.lib.configuration.demo.tests.ConfigurationTest;
|
||||
import cc.carm.lib.configuration.json.JSONConfigFactory;
|
||||
import cc.carm.lib.configuration.source.json.JSONConfigFactory;
|
||||
import cc.carm.lib.configuration.source.ConfigurationHolder;
|
||||
import cc.carm.lib.configuration.source.loader.PathGenerator;
|
||||
import cc.carm.lib.configuration.source.option.StandardOptions;
|
||||
import cc.carm.lib.configuration.value.ConfigValue;
|
||||
import cc.carm.lib.configuration.value.standard.ConfiguredValue;
|
||||
import org.junit.Test;
|
||||
|
||||
+12
-5
@@ -37,18 +37,25 @@
|
||||
|
||||
<dependency>
|
||||
<groupId>${project.parent.groupId}</groupId>
|
||||
<artifactId>easyconfiguration-demo</artifactId>
|
||||
<artifactId>easyconfiguration-feature-file</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
<scope>test</scope>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.bspfsystems</groupId>
|
||||
<artifactId>yamlconfiguration</artifactId>
|
||||
<version>2.0.1</version>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
<version>2.3</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>${project.parent.groupId}</groupId>
|
||||
<artifactId>easyconfiguration-demo</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package cc.carm.lib.configuration;
|
||||
|
||||
import cc.carm.lib.configuration.yaml.YAMLConfigProvider;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public class EasyConfiguration {
|
||||
|
||||
private EasyConfiguration() {
|
||||
}
|
||||
|
||||
public static YAMLConfigProvider from(File file, String source) {
|
||||
YAMLConfigProvider provider = new YAMLConfigProvider(file);
|
||||
try {
|
||||
provider.initializeFile(source);
|
||||
provider.initializeConfig();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
public static YAMLConfigProvider from(File file) {
|
||||
return from(file, file.getName());
|
||||
}
|
||||
|
||||
public static YAMLConfigProvider from(String fileName) {
|
||||
return from(fileName, fileName);
|
||||
}
|
||||
|
||||
public static YAMLConfigProvider from(String fileName, String source) {
|
||||
return from(new File(fileName), source);
|
||||
}
|
||||
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package cc.carm.lib.configuration.source.yaml;
|
||||
|
||||
public class YAMLConfigFactory {
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package cc.carm.lib.configuration.source.yaml;
|
||||
|
||||
import cc.carm.lib.configuration.source.section.ConfigureSection;
|
||||
import cc.carm.lib.configuration.source.section.ConfigureSource;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.UnmodifiableView;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class YAMLSection implements ConfigureSection {
|
||||
|
||||
protected final @NotNull ConfigureSource<? extends YAMLSection, ?, ?> source;
|
||||
protected final @NotNull Map<String, Object> data;
|
||||
protected final @Nullable YAMLSection parent;
|
||||
|
||||
public YAMLSection(@NotNull ConfigureSource<? extends YAMLSection, ?, ?> source,
|
||||
@NotNull Map<String, Object> data, @Nullable YAMLSection parent) {
|
||||
this.source = source;
|
||||
this.data = data;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public @NotNull ConfigureSource<?, ?, ?> source() {
|
||||
return this.source;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable YAMLSection parent() {
|
||||
return this.parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull @UnmodifiableView Map<String, Object> getValues(boolean deep) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(@NotNull String path, @Nullable Object value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(@NotNull String path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isList(@NotNull String path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<?> getList(@NotNull String path) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSection(@NotNull String path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable YAMLSection getSection(@NotNull String path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object get(@NotNull String path) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package cc.carm.lib.configuration.source.yaml;
|
||||
|
||||
public class YAMLSource {
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package cc.carm.lib.configuration.yaml;
|
||||
|
||||
import cc.carm.lib.configuration.source.comment.ConfigurationComments;
|
||||
import cc.carm.lib.configuration.core.source.impl.FileConfigProvider;
|
||||
import cc.carm.lib.yamlcommentupdater.CommentedYAML;
|
||||
import cc.carm.lib.yamlcommentupdater.CommentedYAMLWriter;
|
||||
import org.bspfsystems.yamlconfiguration.configuration.ConfigurationSection;
|
||||
import org.bspfsystems.yamlconfiguration.file.FileConfiguration;
|
||||
import org.bspfsystems.yamlconfiguration.file.YamlConfiguration;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class YAMLConfigProvider extends FileConfigProvider<YAMLSectionWrapper> implements CommentedYAML {
|
||||
|
||||
protected final @NotNull ConfigurationComments comments = new ConfigurationComments();
|
||||
protected YamlConfiguration configuration;
|
||||
protected ConfigInitializer<YAMLConfigProvider> initializer;
|
||||
|
||||
public YAMLConfigProvider(@NotNull File file) {
|
||||
super(file);
|
||||
}
|
||||
|
||||
public void initializeConfig() {
|
||||
ConfigurationOptions
|
||||
this.configuration = YamlConfiguration.loadConfiguration(file);
|
||||
this.initializer = new ConfigInitializer<>(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull YAMLSectionWrapper getConfiguration() {
|
||||
return YAMLSectionWrapper.of(configuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReload() throws Exception {
|
||||
configuration.load(getFile());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ConfigurationComments getComments() {
|
||||
return this.comments;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save() throws Exception {
|
||||
try {
|
||||
CommentedYAMLWriter.writeWithComments(this, this.file);
|
||||
} catch (Exception ex) {
|
||||
configuration.save(file);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ConfigInitializer<YAMLConfigProvider> getInitializer() {
|
||||
return this.initializer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String serializeValue(@NotNull String key, @NotNull Object value) {
|
||||
FileConfiguration temp = new YamlConfiguration();
|
||||
temp.set(key, value);
|
||||
return temp.saveToString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getKeys(@Nullable String sectionKey, boolean deep) {
|
||||
if (sectionKey == null) return configuration.getKeys(deep);
|
||||
|
||||
ConfigurationSection section = configuration.getConfigurationSection(sectionKey);
|
||||
if (section == null) return null;
|
||||
|
||||
return section.getKeys(deep);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object getValue(@NotNull String key) {
|
||||
return configuration.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<String> getHeaderComments(@Nullable String key) {
|
||||
return comments.getHeaderComment(key);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package cc.carm.lib.configuration.yaml;
|
||||
|
||||
public class YAMLConfigSource {
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
package cc.carm.lib.configuration.yaml;
|
||||
|
||||
import org.bspfsystems.yamlconfiguration.configuration.ConfigurationSection;
|
||||
import org.bspfsystems.yamlconfiguration.serialization.ConfigurationSerializable;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class YAMLSectionWrapper implements ConfigurationWrapper<ConfigurationSection> {
|
||||
|
||||
private final ConfigurationSection section;
|
||||
|
||||
private YAMLSectionWrapper(ConfigurationSection section) {
|
||||
this.section = section;
|
||||
}
|
||||
|
||||
@Contract("!null->!null")
|
||||
public static @Nullable YAMLSectionWrapper of(@Nullable ConfigurationSection section) {
|
||||
return section == null ? null : new YAMLSectionWrapper(section);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ConfigurationSection getSource() {
|
||||
return this.section;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Set<String> getKeys(boolean deep) {
|
||||
return new LinkedHashSet<>(section.getKeys(deep));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Map<String, Object> getValues(boolean deep) {
|
||||
return section.getValues(deep);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(@NotNull String path, @Nullable Object value) {
|
||||
this.section.set(path, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(@NotNull String path) {
|
||||
return this.section.contains(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object get(@NotNull String path) {
|
||||
return this.section.get(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isList(@NotNull String path) {
|
||||
return this.section.isList(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable List<?> getList(@NotNull String path) {
|
||||
return this.section.getList(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigurationSection(@NotNull String path) {
|
||||
return this.section.isConfigurationSection(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable YAMLSectionWrapper getConfigurationSection(@NotNull String path) {
|
||||
return of(this.section.getConfigurationSection(path));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public <T extends ConfigurationSerializable> T getSerializable(@NotNull String path, @NotNull Class<T> clazz) {
|
||||
return getSerializable(path, clazz, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Contract("_, _, !null -> !null")
|
||||
public <T extends ConfigurationSerializable> T getSerializable(@NotNull String path, @NotNull Class<T> clazz, @Nullable T defaultValue) {
|
||||
return this.section.getSerializable(path, clazz, defaultValue);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package cc.carm.lib.configuration.yaml;
|
||||
|
||||
import cc.carm.lib.configuration.value.ValueManifest;
|
||||
import cc.carm.lib.configuration.value.impl.CachedConfigValue;
|
||||
import cc.carm.lib.configuration.yaml.builder.YAMLConfigBuilder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public abstract class YAMLValue<T> extends CachedConfigValue<T> {
|
||||
|
||||
public static @NotNull YAMLConfigBuilder builder() {
|
||||
return new YAMLConfigBuilder();
|
||||
}
|
||||
|
||||
public YAMLValue(@NotNull ValueManifest<T> manifest) {
|
||||
super(manifest);
|
||||
}
|
||||
|
||||
public YAMLConfigProvider getYAMLProvider() {
|
||||
ConfigurationProvider<?> provider = getProvider();
|
||||
if (provider instanceof YAMLConfigProvider) return (YAMLConfigProvider) getProvider();
|
||||
else throw new IllegalStateException("Provider is not a YamlConfigProvider");
|
||||
}
|
||||
|
||||
public YAMLSectionWrapper getYAMLConfig() {
|
||||
return getYAMLProvider().getConfiguration();
|
||||
}
|
||||
|
||||
}
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
package cc.carm.lib.configuration.yaml.builder;
|
||||
|
||||
import cc.carm.lib.configuration.core.builder.AbstractConfigBuilder;
|
||||
import cc.carm.lib.configuration.yaml.YAMLConfigProvider;
|
||||
|
||||
public abstract class AbstractYAMLBuilder<T, B extends AbstractYAMLBuilder<T, B>>
|
||||
extends AbstractConfigBuilder<T, B, YAMLConfigProvider> {
|
||||
|
||||
public AbstractYAMLBuilder() {
|
||||
super(YAMLConfigProvider.class);
|
||||
}
|
||||
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
package cc.carm.lib.configuration.yaml.builder;
|
||||
|
||||
import cc.carm.lib.configuration.core.builder.ConfigBuilder;
|
||||
import cc.carm.lib.configuration.yaml.builder.serializable.SerializableBuilder;
|
||||
import org.bspfsystems.yamlconfiguration.serialization.ConfigurationSerializable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class YAMLConfigBuilder extends ConfigBuilder {
|
||||
|
||||
public <V extends ConfigurationSerializable> @NotNull SerializableBuilder<V> ofSerializable(@NotNull Class<V> valueClass) {
|
||||
return new SerializableBuilder<>(valueClass);
|
||||
}
|
||||
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
package cc.carm.lib.configuration.yaml.builder.serializable;
|
||||
|
||||
import cc.carm.lib.configuration.yaml.builder.AbstractYAMLBuilder;
|
||||
import cc.carm.lib.configuration.yaml.value.ConfiguredSerializable;
|
||||
import org.bspfsystems.yamlconfiguration.serialization.ConfigurationSerializable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class SerializableBuilder<T extends ConfigurationSerializable>
|
||||
extends AbstractYAMLBuilder<T, SerializableBuilder<T>> {
|
||||
|
||||
protected final @NotNull Class<T> valueClass;
|
||||
|
||||
public SerializableBuilder(@NotNull Class<T> valueClass) {
|
||||
this.valueClass = valueClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NotNull SerializableBuilder<T> getThis() {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull ConfiguredSerializable<T> build() {
|
||||
return new ConfiguredSerializable<>(buildManifest(), this.valueClass);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
package cc.carm.lib.configuration.yaml.value;
|
||||
|
||||
import cc.carm.lib.configuration.value.ValueManifest;
|
||||
import cc.carm.lib.configuration.yaml.YAMLValue;
|
||||
import org.bspfsystems.yamlconfiguration.serialization.ConfigurationSerializable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class ConfiguredSerializable<T extends ConfigurationSerializable> extends YAMLValue<T> {
|
||||
|
||||
public static <V extends ConfigurationSerializable> ConfiguredSerializable<V> of(@NotNull Class<V> valueClass) {
|
||||
return of(valueClass, null);
|
||||
}
|
||||
|
||||
public static <V extends ConfigurationSerializable> ConfiguredSerializable<V> of(@NotNull Class<V> valueClass,
|
||||
@Nullable V defaultValue) {
|
||||
return builder().ofSerializable(valueClass).defaults(defaultValue).build();
|
||||
}
|
||||
|
||||
protected final @NotNull Class<T> valueClass;
|
||||
|
||||
public ConfiguredSerializable(@NotNull ValueManifest<T> manifest, @NotNull Class<T> valueClass) {
|
||||
super(manifest);
|
||||
this.valueClass = valueClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable T get() {
|
||||
if (!isExpired()) return getCachedOrDefault();
|
||||
|
||||
try {
|
||||
// 若未出现错误,则直接更新缓存并返回。
|
||||
return updateCache(getYAMLConfig().getSerializable(getConfigPath(), valueClass, getDefaultValue()));
|
||||
} catch (Exception e) {
|
||||
// 出现了解析错误,提示并返回默认值。
|
||||
e.printStackTrace();
|
||||
return getDefaultValue();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(@Nullable T value) {
|
||||
updateCache(value);
|
||||
setValue(value);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package config;
|
||||
|
||||
import cc.carm.lib.configuration.EasyConfiguration;
|
||||
import cc.carm.lib.configuration.demo.tests.ConfigurationTest;
|
||||
import cc.carm.lib.configuration.demo.tests.model.AbstractRecord;
|
||||
import cc.carm.lib.configuration.yaml.YAMLConfigProvider;
|
||||
import config.model.AnyModel;
|
||||
import config.model.SomeModel;
|
||||
import config.source.ModelConfiguration;
|
||||
import org.bspfsystems.yamlconfiguration.serialization.ConfigurationSerialization;
|
||||
import org.junit.Test;
|
||||
|
||||
public class DemoConfigTest {
|
||||
|
||||
static {
|
||||
ConfigurationSerialization.registerClass(SomeModel.class);
|
||||
ConfigurationSerialization.registerClass(AnyModel.class);
|
||||
}
|
||||
|
||||
protected final YAMLConfigProvider provider = EasyConfiguration.from("target/config.yml", "test/test2/config.yml");
|
||||
|
||||
@Test
|
||||
public void onTest() {
|
||||
|
||||
ConfigurationTest.testDemo(this.provider);
|
||||
ConfigurationTest.testInner(this.provider);
|
||||
|
||||
testSerialization(this.provider);
|
||||
|
||||
ConfigurationTest.save(this.provider);
|
||||
}
|
||||
|
||||
|
||||
public static void testSerialization(YAMLConfigProvider provider) {
|
||||
provider.initialize(ModelConfiguration.class);
|
||||
System.out.println("----------------------------------------------------");
|
||||
|
||||
AbstractRecord someModel = ModelConfiguration.SOME_MODEL.get();
|
||||
if (someModel != null) System.out.println(someModel.getName());
|
||||
|
||||
AbstractRecord anyModel = ModelConfiguration.ANY_MODEL.get();
|
||||
if (anyModel != null) System.out.println(anyModel.getName());
|
||||
|
||||
ModelConfiguration.MODELS.forEach(System.out::println);
|
||||
ModelConfiguration.MODEL_MAP.forEach((v, anyModel1) -> System.out.println(v + " -> " + anyModel1.toString()));
|
||||
|
||||
|
||||
System.out.println("----------------------------------------------------");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package config.model;
|
||||
|
||||
import cc.carm.lib.configuration.demo.tests.model.AbstractRecord;
|
||||
import org.bspfsystems.yamlconfiguration.serialization.ConfigurationSerializable;
|
||||
import org.bspfsystems.yamlconfiguration.serialization.SerializableAs;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.TestOnly;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@SerializableAs("AnyModel")
|
||||
public class AnyModel extends AbstractRecord implements ConfigurationSerializable {
|
||||
|
||||
public final boolean bool;
|
||||
|
||||
public AnyModel(@NotNull String name, boolean bool) {
|
||||
super(name);
|
||||
this.bool = bool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AnyModel{" +
|
||||
"name='" + name + '\'' +
|
||||
", bool=" + bool +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Map<String, Object> serialize() {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("name", name);
|
||||
map.put("state", bool);
|
||||
return map;
|
||||
}
|
||||
|
||||
public static AnyModel random() {
|
||||
return new AnyModel(UUID.randomUUID().toString().substring(0, 5), Math.random() > 0.5);
|
||||
}
|
||||
|
||||
|
||||
@TestOnly
|
||||
public static AnyModel deserialize(Map<String, ?> args) {
|
||||
return new AnyModel((String) args.get("name"), (Boolean) args.get("state"));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package config.model;
|
||||
|
||||
import cc.carm.lib.configuration.demo.tests.model.AbstractRecord;
|
||||
import org.bspfsystems.yamlconfiguration.serialization.ConfigurationSerializable;
|
||||
import org.bspfsystems.yamlconfiguration.serialization.SerializableAs;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.TestOnly;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@SerializableAs("SomeModel")
|
||||
public class SomeModel extends AbstractRecord implements ConfigurationSerializable {
|
||||
|
||||
public final int num;
|
||||
|
||||
public SomeModel(@NotNull String name, int num) {
|
||||
super(name);
|
||||
this.num = num;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SomeModel{" +
|
||||
"name='" + name + '\'' +
|
||||
", num=" + num +
|
||||
'}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Map<String, Object> serialize() {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("name", name);
|
||||
map.put("num", num);
|
||||
return map;
|
||||
}
|
||||
|
||||
public static SomeModel random() {
|
||||
return new SomeModel(UUID.randomUUID().toString().substring(0, 5), (int) (Math.random() * 1000));
|
||||
}
|
||||
|
||||
|
||||
@TestOnly
|
||||
public static SomeModel deserialize(Map<String, ?> args) {
|
||||
return new SomeModel((String) args.get("name"), (Integer) args.get("num"));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package config.source;
|
||||
|
||||
import cc.carm.lib.configuration.source.Configuration;
|
||||
import cc.carm.lib.configuration.annotation.ConfigPath;
|
||||
import cc.carm.lib.configuration.annotation.HeaderComment;
|
||||
import cc.carm.lib.configuration.value.ConfigValue;
|
||||
import cc.carm.lib.configuration.value.standard.ConfiguredList;
|
||||
import cc.carm.lib.configuration.value.standard.ConfiguredMap;
|
||||
import cc.carm.lib.configuration.value.standard.ConfiguredSectionMap;
|
||||
import cc.carm.lib.configuration.demo.tests.model.AbstractRecord;
|
||||
import cc.carm.lib.configuration.yaml.value.ConfiguredSerializable;
|
||||
import config.model.AnyModel;
|
||||
import config.model.SomeModel;
|
||||
|
||||
@HeaderComment("以下内容用于测试序列化")
|
||||
@ConfigPath("model-test")
|
||||
public class ModelConfiguration implements Configuration {
|
||||
|
||||
public static final ConfigValue<? extends AbstractRecord> SOME_MODEL = ConfiguredSerializable.of(
|
||||
SomeModel.class, SomeModel.random()
|
||||
);
|
||||
|
||||
public static final ConfigValue<? extends AbstractRecord> ANY_MODEL = ConfiguredSerializable.of(
|
||||
AnyModel.class, AnyModel.random()
|
||||
);
|
||||
|
||||
public static final ConfiguredList<AnyModel> MODELS = ConfiguredList.builderOf(AnyModel.class)
|
||||
.fromMap()
|
||||
.parseValue(AnyModel::deserialize).serializeValue(AnyModel::serialize)
|
||||
.defaults(AnyModel.random(), AnyModel.random(), AnyModel.random())
|
||||
.build();
|
||||
|
||||
public static final ConfiguredSectionMap<String, AnyModel> MODEL_MAP = ConfiguredMap.builderOf(String.class, AnyModel.class)
|
||||
.asLinkedMap().fromSection()
|
||||
.parseValue(v -> new AnyModel(v.getString("name", "EMPTY"), v.getBoolean("state", false)))
|
||||
.serializeValue(AnyModel::serialize)
|
||||
.defaults(m -> {
|
||||
m.put("a", AnyModel.random());
|
||||
m.put("b", AnyModel.random());
|
||||
})
|
||||
.build();
|
||||
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package sample;
|
||||
|
||||
import cc.carm.lib.configuration.EasyConfiguration;
|
||||
|
||||
public class Sample {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
Reference in New Issue
Block a user