37 | * Internal use only.
38 | */
39 | @ApiStatus.Internal
40 | @Target(ElementType.ANNOTATION_TYPE)
41 | @Retention(RetentionPolicy.RUNTIME)
42 | public @interface HandledByProxy {
43 | }
44 |
--------------------------------------------------------------------------------
/src/main/java/revxrsal/spec/annotation/Memoize.java:
--------------------------------------------------------------------------------
1 | package revxrsal.spec.annotation;
2 |
3 | import java.lang.annotation.ElementType;
4 | import java.lang.annotation.Retention;
5 | import java.lang.annotation.RetentionPolicy;
6 | import java.lang.annotation.Target;
7 |
8 | /**
9 | * An annotation that allows to compute certain values and cache their result.
10 | *
11 | * This is very useful for heavy, repetitive computations that depend
12 | * on the configuration values.
13 | *
14 | * Note: {@link Memoize @Memoize} does not (yet) consider arguments
15 | * when caching values. Therefore, it is best to just use it to compute the
16 | * parts that depend on the configuration values
17 | *
18 | * Reloading, resetting, or calling a setter will re-compute
19 | * all memoized values.
20 | *
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package revxrsal.spec.annotation;
25 |
26 | import org.jetbrains.annotations.NotNull;
27 |
28 | import java.lang.annotation.*;
29 |
30 | /**
31 | * Represents a config specification interface
32 | */
33 | @Inherited
34 | @Target(ElementType.TYPE)
35 | @Retention(RetentionPolicy.RUNTIME)
36 | public @interface ConfigSpec {
37 |
38 | /**
39 | * The comments to add at the very beginning of the file. Each value
40 | * is a separate line.
41 | *
42 | * Note that every line will be preceded by a '# ' automatically,
43 | * except entries that start with '#', which will not be preceded by a
44 | * space (for creating visual separators).
45 | *
46 | * @return The comments
47 | */
48 | @NotNull String[] header() default {};
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/revxrsal/spec/annotation/Key.java:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Cream, licensed under the MIT License.
3 | *
4 | * Copyright (c) Revxrsal
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package revxrsal.spec.annotation;
25 |
26 | import org.jetbrains.annotations.NotNull;
27 |
28 | import java.lang.annotation.ElementType;
29 | import java.lang.annotation.Retention;
30 | import java.lang.annotation.RetentionPolicy;
31 | import java.lang.annotation.Target;
32 |
33 | /**
34 | * Sets the key of the property in the configuration file
35 | *
36 | * Example:
37 | *
{@code @ConfigSpec
38 | * public interface GameSettings {
39 | *
40 | * @Comment("The cooldown message. Use %cooldown% as a placeholder.")
41 | * @Key("cooldown-message")
42 | * default String cooldownMessage() {
43 | * return "Starting in %cooldown%s";
44 | * }
45 | * }}
46 | */
47 | @Target(ElementType.METHOD)
48 | @Retention(RetentionPolicy.RUNTIME)
49 | public @interface Key {
50 |
51 | /**
52 | * The key value
53 | *
54 | * @return The key
55 | */
56 | @NotNull String value();
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/java/revxrsal/spec/annotation/IgnoreMethod.java:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Cream, licensed under the MIT License.
3 | *
4 | * Copyright (c) Revxrsal
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package revxrsal.spec.annotation;
25 |
26 | import java.lang.annotation.ElementType;
27 | import java.lang.annotation.Retention;
28 | import java.lang.annotation.RetentionPolicy;
29 | import java.lang.annotation.Target;
30 |
31 | /**
32 | * Marks a method as ignored by the property scanner of {@link ConfigSpec config specs}. It
33 | * must be a default method, otherwise an error will be thrown.
34 | *
35 | * Example:
36 | *
{@code @ConfigSpec
37 | * public interface GameSettings {
38 | *
39 | * @Comment("The cooldown message. Use %cooldown% as a placeholder.")
40 | * default String cooldownMessage() {
41 | * return "Starting in %cooldown%s";
42 | * }
43 | *
44 | * @IgnoreMethod
45 | * default String getCooldownMessage(int cooldown) {
46 | * return cooldownMessage().replace("%cooldown%", String.valueOf(cooldown));
47 | * }
48 | * }}
49 | */
50 | @Target(ElementType.METHOD)
51 | @Retention(RetentionPolicy.RUNTIME)
52 | public @interface IgnoreMethod {
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/java/revxrsal/spec/annotation/Comment.java:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Cream, licensed under the MIT License.
3 | *
4 | * Copyright (c) Revxrsal
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package revxrsal.spec.annotation;
25 |
26 | import java.lang.annotation.ElementType;
27 | import java.lang.annotation.Retention;
28 | import java.lang.annotation.RetentionPolicy;
29 | import java.lang.annotation.Target;
30 |
31 | /**
32 | * Adds a comment to the given property.
33 | *
34 | * Example:
35 | *
{@code @ConfigSpec
36 | * public interface GameSettings {
37 | *
38 | * @Comment(
39 | * "The game cooldown",
40 | * "",
41 | * "Default value: 20"
42 | * )
43 | * default int cooldown() {
44 | * return 20;
45 | * }
46 | * }}
47 | */
48 |
49 | @Target(ElementType.METHOD)
50 | @Retention(RetentionPolicy.RUNTIME)
51 | public @interface Comment {
52 |
53 | /**
54 | * The comments to add. Each value is a separate line.
55 | *
56 | * Note that every line will be preceded by a '# ' automatically,
57 | * except entries that start with '#', which will not be preceded by a
58 | * space (for creating visual separators).
59 | *
60 | * @return The comments
61 | */
62 | String[] value();
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/src/main/java/revxrsal/spec/annotation/AsMap.java:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Cream, licensed under the MIT License.
3 | *
4 | * Copyright (c) Revxrsal
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package revxrsal.spec.annotation;
25 |
26 | import org.jetbrains.annotations.NotNull;
27 |
28 | import java.lang.annotation.ElementType;
29 | import java.lang.annotation.Retention;
30 | import java.lang.annotation.RetentionPolicy;
31 | import java.lang.annotation.Target;
32 |
33 | /**
34 | * Returns the {@link java.util.Map} representation of a given
35 | * {@link ConfigSpec} interface.
36 | *
37 | * Example:
38 | *
{@code @ConfigSpec
39 | * public interface GameSettings {
40 | *
41 | * @Comment("The game cooldown")
42 | * default int cooldown() {
43 | * return 20;
44 | * }
45 | *
46 | * @Comment("The cooldown message")
47 | * default String countdownMessage() {
48 | * return "Game starts in %countdown%s";
49 | * }
50 | *
51 | * @AsMap(AsMap.Behavior.CLONE)
52 | * Map asMap();
53 | *
54 | * }}
55 | */
56 | @HandledByProxy
57 | @Target(ElementType.METHOD)
58 | @Retention(RetentionPolicy.RUNTIME)
59 | public @interface AsMap {
60 |
61 | /**
62 | * Decides what to do with the internal map
63 | *
64 | * @return The behavior of this {@link AsMap} method
65 | */
66 | @NotNull Behavior value() default Behavior.IMMUTABLE_VIEW;
67 |
68 | enum Behavior {
69 |
70 | /**
71 | * Creates a (shallow) copy of the underlying map
72 | */
73 | CLONE,
74 |
75 | /**
76 | * Creates an immutable view of the underlying map. This is the default
77 | * behavior
78 | */
79 | IMMUTABLE_VIEW,
80 |
81 | /**
82 | * Returns the underlying map as-is. Modifying this map will modify
83 | * the actual config spec.
84 | */
85 | UNDERLYING_MAP
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/main/java/revxrsal/spec/MHLookup.java:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Cream, licensed under the MIT License.
3 | *
4 | * Copyright (c) Revxrsal
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package revxrsal.spec;
25 |
26 | import lombok.SneakyThrows;
27 | import org.jetbrains.annotations.NotNull;
28 | import org.jetbrains.annotations.Nullable;
29 |
30 | import java.lang.invoke.MethodHandles;
31 | import java.lang.invoke.MethodHandles.Lookup;
32 | import java.lang.reflect.Constructor;
33 | import java.lang.reflect.Method;
34 |
35 | import static java.lang.invoke.MethodHandles.lookup;
36 |
37 | /**
38 | * A utility for generating private {@link Lookup}s. These are not supported
39 | * natively in Java 8, so we have to use reflection hacks to simulate them.
40 | */
41 | final class MHLookup {
42 |
43 | private static @Nullable Constructor constructor;
44 | private static @Nullable Method privateLookupIn;
45 |
46 | static {
47 | try {
48 | privateLookupIn = MethodHandles.class.getDeclaredMethod("privateLookupIn", Class.class, MethodHandles.Lookup.class);
49 | } catch (NoSuchMethodException e) {
50 | try {
51 | constructor = Lookup.class.getDeclaredConstructor(Class.class);
52 | constructor.setAccessible(true);
53 | } catch (NoSuchMethodException ex) {
54 | throw new RuntimeException(ex);
55 | }
56 | }
57 | }
58 |
59 | private MHLookup() {
60 | }
61 |
62 | /**
63 | * Generates a {@link Lookup} that can access private members in the given
64 | * class.
65 | *
66 | * @param cl The class to access
67 | * @return The created {@link Lookup}
68 | */
69 | @SneakyThrows
70 | public static @NotNull Lookup privateLookupIn(Class> cl) {
71 | if (privateLookupIn != null) {
72 | return (Lookup) privateLookupIn.invoke(null, cl, lookup());
73 | }
74 | if (constructor != null) {
75 | return constructor.newInstance(cl);
76 | }
77 | throw new IllegalArgumentException("Failed to create a private lookup!");
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/main/java/revxrsal/spec/SpecProxy.java:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Cream, licensed under the MIT License.
3 | *
4 | * Copyright (c) Revxrsal
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package revxrsal.spec;
25 |
26 | import lombok.RequiredArgsConstructor;
27 | import revxrsal.spec.annotation.Reload;
28 | import revxrsal.spec.annotation.Save;
29 |
30 | import java.lang.invoke.MethodHandle;
31 | import java.lang.reflect.InvocationHandler;
32 | import java.lang.reflect.Method;
33 | import java.lang.reflect.Proxy;
34 | import java.util.function.Supplier;
35 |
36 | @RequiredArgsConstructor
37 | final class SpecProxy implements InvocationHandler {
38 |
39 | public static T proxy(Class type, Supplier supplier, Runnable onReload, Runnable onSave) {
40 | //noinspection unchecked
41 | return (T) Proxy.newProxyInstance(
42 | type.getClassLoader(),
43 | new Class>[]{type},
44 | new SpecProxy<>(type, supplier, onReload, onSave)
45 | );
46 | }
47 |
48 | private final Class> type;
49 | private final Supplier supplier;
50 | private final Runnable onReload;
51 | private final Runnable onSave;
52 |
53 | private MethodHandle reloadDef, saveDef;
54 |
55 | @Override
56 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
57 | if (method.isAnnotationPresent(Reload.class)) {
58 | onReload.run();
59 | if (method.isDefault()) {
60 | if (reloadDef == null) {
61 | reloadDef = MHLookup.privateLookupIn(type)
62 | .in(type)
63 | .unreflectSpecial(method, type);
64 | }
65 | return reloadDef.bindTo(proxy).invokeWithArguments(args);
66 | }
67 | return null;
68 | }
69 | if (method.isAnnotationPresent(Save.class)) {
70 | onSave.run();
71 | if (method.isDefault()) {
72 | if (saveDef == null) {
73 | saveDef = MHLookup.privateLookupIn(type)
74 | .in(type)
75 | .unreflectSpecial(method, type);
76 | }
77 | return saveDef.bindTo(proxy).invokeWithArguments(args);
78 | }
79 | return null;
80 | }
81 | T instance = supplier.get();
82 | return method.invoke(instance, args);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/test/java/revxrsal/spec/test/ServerConfig.java:
--------------------------------------------------------------------------------
1 | package revxrsal.spec.test;
2 |
3 | import revxrsal.spec.annotation.*;
4 |
5 | @ConfigSpec(header = {
6 | "========================================",
7 | " Server Configuration ",
8 | "========================================",
9 | "This file controls basic server settings.",
10 | "Edit with care to avoid misconfiguration."
11 | })
12 | public interface ServerConfig {
13 |
14 | @Key("name") // <--- optional
15 | @Comment("The name that will be displayed to players.")
16 | default String serverName() {
17 | return "My Awesome Server";
18 | }
19 |
20 | @Key("max-players")
21 | @Comment("Maximum number of players allowed online at once.")
22 | default int maxPlayers() {
23 | return 100;
24 | }
25 |
26 | @Key("game-mode")
27 | @Comment({
28 | "Server operating mode:",
29 | "- SURVIVAL = Normal gameplay",
30 | "- CREATIVE = Build freely",
31 | "- ADVENTURE = Limited interactions",
32 | " ",
33 | "Default: SURVIVAL"
34 | })
35 | default Mode serverMode() {
36 | return Mode.SURVIVAL;
37 | }
38 |
39 | @Comment("The message shown in the multiplayer server list.")
40 | default String motd() {
41 | return "Welcome to the Adventure!";
42 | }
43 |
44 | @Key("whitelist-enabled")
45 | @Comment("Enable/disable whitelist mode. Only approved players can join.")
46 | default boolean whitelistEnabled() {
47 | return false;
48 | }
49 |
50 | // You can leave it like this with no implementation
51 | @Save
52 | void save();
53 |
54 | // Or write as a default function which will be called
55 | // after the plugin is reloaded.
56 | //
57 | // Your function can have any arguments as needed
58 | @Reload
59 | default void reload() {
60 | System.out.println("Configuration reloaded!");
61 | }
62 |
63 | // We can nest ConfigSpecs too!
64 | //
65 | // You can also have them inside Maps, Lists, Sets, arrays, etc.
66 | @Comment("The server messages")
67 | Messages messages();
68 |
69 | @Comment("Includes numerical values")
70 | ServerNumbers numbers();
71 |
72 | @ConfigSpec
73 | interface ServerNumbers {
74 |
75 | @Comment("The chunk radius")
76 | default int chunkRadius() {
77 | return 13;
78 | }
79 |
80 | // @Memoize allows us to compute certain values and cache their result
81 | //
82 | // This is very useful for heavy, repetitive computations.
83 | //
84 | // Note: @Memoize does not (yet) consider arguments when caching values.
85 | // Therefore, it is best to just use it to compute the parts that depend on the
86 | // configuration values
87 | //
88 | // Reloading, resetting, or calling a setter will re-compute all memoized values.
89 | //
90 | // See the docs of @Memoize for more info
91 | @Memoize
92 | default int chunkRadiusCubed() {
93 | System.out.println("Computing chunkRadius^3");
94 | return chunkRadius() * chunkRadius() * chunkRadius();
95 | }
96 | }
97 |
98 | @ConfigSpec
99 | interface Messages {
100 |
101 | @Comment("Sent when a player joins")
102 | default String playerJoined() {
103 | return "Player %player% joined the server!";
104 | }
105 |
106 | @Comment("Sent when a player leaves")
107 | default String playerLeft() {
108 | return "Player %player% left the server!";
109 | }
110 | }
111 |
112 | enum Mode {
113 | SURVIVAL,
114 | CREATIVE,
115 | ADVENTURE
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/main/java/revxrsal/spec/SpecReference.java:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Cream, licensed under the MIT License.
3 | *
4 | * Copyright (c) Revxrsal
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package revxrsal.spec;
25 |
26 | import org.jetbrains.annotations.NotNull;
27 | import revxrsal.spec.annotation.ConfigSpec;
28 | import revxrsal.spec.annotation.Reload;
29 | import revxrsal.spec.annotation.Save;
30 |
31 | import java.lang.reflect.Proxy;
32 | import java.util.Objects;
33 |
34 | /**
35 | * A utility object wrapper that creates a {@link Proxy} for {@link ConfigSpec}
36 | * classes and handles the invocation of {@link Save} and {@link Reload} methods.
37 | *
38 | * Using this allows the user to store instances of the {@link ConfigSpec} interfaces
39 | * while at the same time making sure they always have the latest value
40 | * if it gets reloaded.
41 | *
42 | * It also allows specs to include methods like {@link Reload} and {@link Save},
43 | * which we intercept in the proxy.
44 | *
45 | * @param The property type
46 | */
47 | public final class SpecReference {
48 |
49 | /**
50 | * The spec type
51 | */
52 | private final @NotNull Class type;
53 |
54 | /**
55 | * The configuration file containing the data
56 | */
57 | private final @NotNull CommentedConfiguration config;
58 |
59 | /**
60 | * The underlying value. This can get changed at any time
61 | */
62 | private T value;
63 |
64 | /**
65 | * The proxy (reference) that redirects calls to this object or the underlying
66 | * value
67 | */
68 | private final T proxy;
69 |
70 | public SpecReference(@NotNull Class type, @NotNull CommentedConfiguration config) {
71 | this.type = type;
72 | this.config = config;
73 | this.proxy = SpecProxy.proxy(type, this::value, this::reload, this::save);
74 | reload();
75 | }
76 |
77 | /**
78 | * Returns the type of the interface this reference
79 | * is pointing to
80 | *
81 | * @return the interface type
82 | */
83 | public @NotNull Class> type() {
84 | return type;
85 | }
86 |
87 | /**
88 | * Returns the actual value that is being wrapped. This value
89 | * cannot be reloaded or saved, as these are handled by {@link #proxy}.
90 | *
91 | * @return The underlying value
92 | */
93 | private @NotNull T value() {
94 | return value;
95 | }
96 |
97 | /**
98 | * Returns the top-level proxy. This proxy allows reloading and saving.
99 | *
100 | * @return The top-level wrapper proxy.
101 | */
102 | public @NotNull T get() {
103 | return proxy;
104 | }
105 |
106 | /**
107 | * Reloads the content of the object.
108 | */
109 | public void reload() {
110 | config.load();
111 | SpecClass from = Specs.from(type);
112 | config.setComments(from.comments());
113 | config.setHeaders(from.headers());
114 | this.value = config.getAs(type);
115 | }
116 |
117 | /**
118 | * Saves the current object to the config
119 | */
120 | public void save() {
121 | config.setTo(this.value, this.type);
122 | config.save();
123 | }
124 |
125 | /**
126 | * Sets the value this reference is pointing to, to the given value.
127 | *
128 | * @param value The new value
129 | */
130 | public void set(@NotNull T value) {
131 | Objects.requireNonNull(value, "value cannot be null!");
132 | this.value = value;
133 | }
134 |
135 | }
136 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Spec
2 |
3 | [](https://discord.gg/pEGGF785zp)
4 | [](https://search.maven.org/artifact/io.github.revxrsal/spec)
5 | [](https://opensource.org/licenses/MIT)
6 | [](https://github.com/Revxrsal/spec/actions/workflows/gradle.yml)
7 | [](https://www.codefactor.io/repository/github/revxrsal/spec)
8 |
9 | A library for generating beautiful, commented, type-safe YML through interfaces
10 |
11 | ## 🚀 Features
12 |
13 | - **Define your configs with clean interfaces** 🎨
14 | Just write an interface — no messy boilerplate!
15 |
16 | - **Default values? Easy.** ✍️
17 | Simply use default methods to provide fallback values.
18 |
19 | - **Built-in comments with `@Comment`** 💬
20 | Generate beautiful, documented configuration files automatically.
21 |
22 | - **Fully customizable keys** 🗝️
23 | Use `@Key` or `@SerializedName` to control config names precisely.
24 |
25 | - **Add new properties anytime** ➕
26 | Evolve your specs without breaking existing configs — full backwards compatibility!
27 |
28 | - **Recursive specs support** ♻️
29 | Nest specs inside arrays, lists, maps, or other specs — no limits!
30 |
31 | - **Powerful setters support** 🛠️
32 | Update values programmatically at runtime without effort.
33 |
34 | - **Self-saving and self-reloading** 🔥
35 | Call `save()` or `reload()` right from your spec — it's automatic!
36 |
37 | - **Custom `@AsMap` support** 📜
38 | Define your own methods to expose a `Map` view of the spec.
39 |
40 | - **Instant resets with `@Reset`** 🔄
41 | Roll back any spec instance to its default state in a single call.
42 |
43 | - **Powered by Gson** ⚡
44 | Use all Gson features: custom type adapters, fine-tuned serialization, and more.
45 |
46 | - **Ultra lightweight** 🧹
47 | Only **40 KB** in size — no bloat, no slowdown.
48 |
49 | ## ✨ Why you'll love it:
50 |
51 | - Super clean APIs
52 | - Zero learning curve
53 | - Infinite flexibility with Gson
54 | - Handles nested, complex configs effortlessly
55 | - Tiny footprint
56 |
57 | ## Usage
58 |
59 | #### Maven
60 |
61 | ```xml
62 |
63 |
64 |
65 | io.github.revxrsal
66 | spec
67 | [VERSION]
68 |
69 |
70 | ```
71 |
72 | Latest version: 
73 |
74 | #### Gradle
75 |
76 | ```groovy
77 | dependencies {
78 | implementation("io.github.revxrsal:spec:[VERSION]")
79 | }
80 | ```
81 |
82 | Latest version: 
83 |
84 | ### Example
85 |
86 | ```java
87 |
88 | @ConfigSpec(header = {
89 | "========================================",
90 | " Server Configuration ",
91 | "========================================",
92 | "This file controls basic server settings.",
93 | "Edit with care to avoid misconfiguration."
94 | })
95 | public interface ServerConfig {
96 |
97 | @Key("name") // <--- optional
98 | @Comment("The name that will be displayed to players.")
99 | default String serverName() {
100 | return "My Awesome Server";
101 | }
102 |
103 | @Key("max-players")
104 | @Comment("Maximum number of players allowed online at once.")
105 | default int maxPlayers() {
106 | return 100;
107 | }
108 |
109 | @Key("game-mode")
110 | @Comment({
111 | "Server operating mode:",
112 | "- SURVIVAL = Normal gameplay",
113 | "- CREATIVE = Build freely",
114 | "- ADVENTURE = Limited interactions",
115 | " ",
116 | "Default: SURVIVAL"
117 | })
118 | default Mode serverMode() {
119 | return Mode.SURVIVAL;
120 | }
121 |
122 | @Comment("The message shown in the multiplayer server list.")
123 | default String motd() {
124 | return "Welcome to the Adventure!";
125 | }
126 |
127 | @Key("whitelist-enabled")
128 | @Comment("Enable/disable whitelist mode. Only approved players can join.")
129 | default boolean whitelistEnabled() {
130 | return false;
131 | }
132 |
133 | @Save
134 | void save();
135 |
136 | @Reload
137 | void reload();
138 |
139 | enum Mode {
140 | SURVIVAL,
141 | CREATIVE,
142 | ADVENTURE
143 | }
144 | }
145 | ```
146 |
147 | ```java
148 | import java.nio.file.Paths;
149 |
150 | public static void main(String[] args) {
151 | ServerConfig config = Specs.fromFile(ServerConfig.class, Paths.get("server.yml"));
152 | // The Specs class includes many more utilities. Check it out!
153 |
154 | // Saves the configuration to config.yml
155 | config.save();
156 |
157 | // Reloads the configuration from config.yml
158 | config.reload();
159 |
160 | System.out.println(config);
161 | }
162 | ```
163 |
164 | Output YML:
165 |
166 | ```yml
167 | # ========================================
168 | # Server Configuration
169 | # ========================================
170 | # This file controls basic server settings.
171 | # Edit with care to avoid misconfiguration.
172 |
173 | # Server operating mode:
174 | # - SURVIVAL = Normal gameplay
175 | # - CREATIVE = Build freely
176 | # - ADVENTURE = Limited interactions
177 | #
178 | # Default: SURVIVAL
179 | game-mode: SURVIVAL
180 |
181 | # The message shown in the multiplayer server list.
182 | motd: Welcome to the Adventure!
183 |
184 | # The name that will be displayed to players.
185 | name: My Awesome Server
186 |
187 | # Enable/disable whitelist mode. Only approved players can join.
188 | whitelist-enabled: false
189 |
190 | # Maximum number of players allowed online at once.
191 | max-players: 100.0
192 | ```
193 |
--------------------------------------------------------------------------------
/src/main/java/revxrsal/spec/SpecClass.java:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Cream, licensed under the MIT License.
3 | *
4 | * Copyright (c) Revxrsal
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package revxrsal.spec;
25 |
26 | import org.jetbrains.annotations.NotNull;
27 | import org.jetbrains.annotations.Nullable;
28 | import org.jetbrains.annotations.Unmodifiable;
29 | import revxrsal.spec.annotation.ConfigSpec;
30 |
31 | import java.util.*;
32 |
33 | import static revxrsal.spec.SpecProperty.headerOf;
34 | import static revxrsal.spec.SpecProperty.propertiesOf;
35 |
36 | public final class SpecClass {
37 |
38 | public static final String ARRAY_INDEX = "";
39 |
40 | private final Class> type;
41 | private final @Unmodifiable Map properties;
42 | private @Nullable Map comments;
43 | private final @NotNull List headers;
44 |
45 | SpecClass(
46 | @NotNull Class> type,
47 | @NotNull Map properties,
48 | @NotNull List headers
49 | ) {
50 | this.type = type;
51 | this.properties = properties;
52 | this.headers = headers;
53 | }
54 |
55 | private @NotNull Map computeComments() {
56 | Map comments = new HashMap<>();
57 | computeCommentsRecursively(comments, properties.values(), "", 0);
58 | return comments;
59 | }
60 |
61 | private static void computeCommentsRecursively(
62 | @NotNull Map comments,
63 | @NotNull Collection properties,
64 | @NotNull String parentPath,
65 | int indent
66 | ) {
67 | for (SpecProperty property : properties) {
68 | boolean isSpec = Specs.isConfigSpec(property.type());
69 | if (!property.hasComments() && !isSpec)
70 | continue;
71 | String indentStr = spaces(indent);
72 | String commentPath = parentPath.isEmpty() ? property.key() : parentPath + '.' + property.key();
73 | StringJoiner commentsString = new StringJoiner(System.lineSeparator(), "\n", "");
74 | for (String comment : property.comments()) {
75 | commentsString.add(indentStr + "# " + comment);
76 | }
77 | comments.put(commentPath, commentsString.toString());
78 | if (isSpec) {
79 | SpecClass bpc = Specs.from(property.type());
80 | computeCommentsRecursively(
81 | comments,
82 | bpc.properties().values(),
83 | commentPath,
84 | indent + 2
85 | );
86 | } else if (isCollection(property.type())) {
87 | Class> type = getCollectionType(property.getter().getGenericReturnType());
88 | if (Specs.isConfigSpec(type)) {
89 | SpecClass bpc = Specs.from(type);
90 | computeCommentsRecursively(
91 | comments,
92 | bpc.properties().values(),
93 | commentPath + "." + ARRAY_INDEX,
94 | indent + 2
95 | );
96 | }
97 | }
98 | }
99 | }
100 |
101 | private static Class> getCollectionType(java.lang.reflect.Type returnType) {
102 | Class> rawType = Util.getRawType(returnType);
103 | if (Collection.class.isAssignableFrom(rawType)) {
104 | return Util.getRawType(Util.getFirstGeneric(returnType, Object.class));
105 | } else {
106 | return rawType.getComponentType();
107 | }
108 | }
109 |
110 | private static boolean isCollection(Class> aClass) {
111 | return Collection.class.isAssignableFrom(aClass) || aClass.isArray();
112 | }
113 |
114 | private static String spaces(int times) {
115 | char[] c = new char[times];
116 | Arrays.fill(c, ' ');
117 | return new String(c);
118 | }
119 |
120 | static @NotNull SpecClass from(@NotNull Class> type) {
121 | Objects.requireNonNull(type, "interface cannot be null!");
122 | if (!type.isInterface())
123 | throw new IllegalArgumentException("Class is not an interface: " + type.getName());
124 | if (!type.isAnnotationPresent(ConfigSpec.class))
125 | throw new IllegalArgumentException("Interface does not have @ConfigSpec on it!");
126 | List headers = headerOf(type);
127 | Map properties = propertiesOf(type);
128 | return new SpecClass(type, properties, headers);
129 | }
130 |
131 | public @NotNull Map comments() {
132 | if (comments == null)
133 | comments = computeComments();
134 | return comments;
135 | }
136 |
137 | public @NotNull List headers() {
138 | return headers;
139 | }
140 |
141 | public @NotNull @Unmodifiable Map properties() {
142 | return properties;
143 | }
144 |
145 | @SuppressWarnings("unchecked")
146 | public @NotNull T createDefault() {
147 | return (T) Specs.createDefault(type);
148 | }
149 |
150 | @SuppressWarnings("unchecked")
151 | public @NotNull T createUnsafe(Map map) {
152 | return (T) Specs.createUnsafe(type, map);
153 | }
154 |
155 | }
156 |
--------------------------------------------------------------------------------
/src/main/java/revxrsal/spec/SpecAdapterFactory.java:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Cream, licensed under the MIT License.
3 | *
4 | * Copyright (c) Revxrsal
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package revxrsal.spec;
25 |
26 | import com.google.gson.Gson;
27 | import com.google.gson.TypeAdapter;
28 | import com.google.gson.TypeAdapterFactory;
29 | import com.google.gson.annotations.JsonAdapter;
30 | import com.google.gson.internal.ConstructorConstructor;
31 | import com.google.gson.reflect.TypeToken;
32 | import com.google.gson.stream.JsonReader;
33 | import com.google.gson.stream.JsonWriter;
34 | import lombok.SneakyThrows;
35 | import org.jetbrains.annotations.NotNull;
36 | import org.jetbrains.annotations.Nullable;
37 |
38 | import java.io.IOException;
39 | import java.lang.invoke.MethodHandle;
40 | import java.lang.invoke.MethodHandles;
41 | import java.lang.reflect.Method;
42 | import java.util.LinkedHashMap;
43 | import java.util.Map;
44 |
45 | import static revxrsal.spec.MHLookup.privateLookupIn;
46 | import static revxrsal.spec.Specs.createDefault;
47 | import static revxrsal.spec.Specs.isConfigSpec;
48 |
49 | @SuppressWarnings({"unchecked"})
50 | public final class SpecAdapterFactory implements TypeAdapterFactory {
51 |
52 | private static @Nullable MethodHandle CTR_CTR = null;
53 |
54 | static {
55 | try {
56 | MethodHandles.Lookup lookup = privateLookupIn(Gson.class);
57 | CTR_CTR = lookup
58 | .findGetter(Gson.class, "constructorConstructor", ConstructorConstructor.class);
59 | } catch (Throwable ignored) {
60 | }
61 | }
62 |
63 | public static final SpecAdapterFactory INSTANCE = new SpecAdapterFactory();
64 |
65 | @Override
66 | @SneakyThrows
67 | public TypeAdapter create(Gson gson, TypeToken type) {
68 | Class> rawType = type.getRawType();
69 | if (!isConfigSpec(rawType)) {
70 | return null;
71 | }
72 | SpecClass impl = Specs.from(rawType);
73 | Map fieldsMap = new LinkedHashMap<>();
74 | for (SpecProperty value : impl.properties().values()) {
75 | if (value.isHandledByProxy())
76 | continue;
77 | Method getter = value.getter();
78 | TypeToken> fieldType = TypeToken.get(getter.getGenericReturnType());
79 |
80 | TypeAdapter> adapter = null;
81 | JsonAdapter annotation = getter.getAnnotation(JsonAdapter.class);
82 | if (CTR_CTR != null) {
83 | ConstructorConstructor constructorConstructor = (ConstructorConstructor) CTR_CTR.invoke(gson);
84 | if (annotation != null) {
85 | adapter = getTypeAdapter(constructorConstructor, gson, fieldType, annotation);
86 | }
87 | }
88 | if (adapter == null)
89 | adapter = gson.getAdapter(fieldType);
90 |
91 | BoundField field = new BoundField(value.key(), adapter);
92 | fieldsMap.put(value.key(), field);
93 | }
94 |
95 | return new TypeAdapter() {
96 | @Override
97 | public void write(JsonWriter out, T value) throws IOException {
98 | out.beginObject();
99 | Map map = MapProxy.getInternalMap(value);
100 | for (BoundField boundField : fieldsMap.values()) {
101 | out.name(boundField.name);
102 | Object fieldValue = map.get(boundField.name);
103 | boundField.adapter().write(out, fieldValue);
104 | }
105 | out.endObject();
106 | }
107 |
108 | @SneakyThrows
109 | @Override
110 | public T read(JsonReader in) {
111 | in.beginObject();
112 | T proxy = (T) createDefault(rawType);
113 | Map map = MapProxy.getInternalMap(proxy);
114 | while (in.hasNext()) {
115 | String name = in.nextName();
116 | BoundField field = fieldsMap.get(name);
117 | if (field == null) {
118 | in.skipValue();
119 | } else {
120 | Object readValue = field.adapter.read(in);
121 | map.put(field.name, readValue);
122 | }
123 | }
124 | in.endObject();
125 | return proxy;
126 | }
127 | };
128 | }
129 |
130 | private static class BoundField {
131 | private final @NotNull String name;
132 | private final @NotNull TypeAdapter> adapter;
133 |
134 | @SneakyThrows
135 | public BoundField(@NotNull String name, @NotNull TypeAdapter> adapter) {
136 | this.name = name;
137 | this.adapter = adapter;
138 | }
139 |
140 | public @NotNull TypeAdapter adapter() {
141 | return (TypeAdapter) adapter;
142 | }
143 | }
144 |
145 | static TypeAdapter> getTypeAdapter(ConstructorConstructor constructorConstructor, Gson gson,
146 | TypeToken> fieldType, JsonAdapter annotation) {
147 | Class> value = annotation.value();
148 | if (TypeAdapter.class.isAssignableFrom(value)) {
149 | Class> typeAdapter = (Class>) value;
150 | return constructorConstructor.get(TypeToken.get(typeAdapter)).construct();
151 | }
152 | if (TypeAdapterFactory.class.isAssignableFrom(value)) {
153 | Class typeAdapterFactory = (Class) value;
154 | return constructorConstructor.get(TypeToken.get(typeAdapterFactory))
155 | .construct()
156 | .create(gson, fieldType);
157 | }
158 |
159 | throw new IllegalArgumentException("@JsonAdapter value must be TypeAdapter or TypeAdapterFactory reference.");
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/main/java/revxrsal/spec/Util.java:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Cream, licensed under the MIT License.
3 | *
4 | * Copyright (c) Revxrsal
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package revxrsal.spec;
25 |
26 | import org.jetbrains.annotations.Contract;
27 | import org.jetbrains.annotations.NotNull;
28 | import org.jetbrains.annotations.Nullable;
29 |
30 | import java.lang.reflect.*;
31 | import java.util.Iterator;
32 | import java.util.NoSuchElementException;
33 |
34 | /**
35 | * A utility class with small helper functions
36 | */
37 | final class Util {
38 |
39 | private Util() {
40 | }
41 |
42 | /**
43 | * Returns the {@link Class} object representing the class or interface
44 | * that declared this type.
45 | *
46 | * @return the {@link Class} object representing the class or interface
47 | * that declared this type
48 | */
49 | public static Class> getRawType(Type type) {
50 | if (type instanceof Class>) {
51 | // type is a normal class.
52 | return (Class>) type;
53 |
54 | } else if (type instanceof ParameterizedType) {
55 | ParameterizedType parameterizedType = (ParameterizedType) type;
56 |
57 | // I'm not exactly sure why getRawType() returns Type instead of Class.
58 | // Neal isn't either but suspects some pathological case related
59 | // to nested classes exists.
60 | Type rawType = parameterizedType.getRawType();
61 | if (!(rawType instanceof Class)) {
62 | throw new IllegalStateException("Expected a Class, found a " + rawType);
63 | }
64 | return (Class>) rawType;
65 |
66 | } else if (type instanceof GenericArrayType) {
67 | Type componentType = ((GenericArrayType) type).getGenericComponentType();
68 | return Array.newInstance(getRawType(componentType), 0).getClass();
69 |
70 | } else if (type instanceof TypeVariable) {
71 | // we could use the variable's bounds, but that won't work if there are multiple.
72 | // having a raw type that's more general than necessary is okay
73 | return Object.class;
74 |
75 | } else if (type instanceof WildcardType) {
76 | return getRawType(((WildcardType) type).getUpperBounds()[0]);
77 |
78 | } else {
79 | String className = type == null ? "null" : type.getClass().getName();
80 | throw new IllegalArgumentException("Expected a Class, ParameterizedType, or "
81 | + "GenericArrayType, but <" + type + "> is of type " + className);
82 | }
83 | }
84 |
85 | /**
86 | * Returns the first generic type of the given class. Because
87 | * classes do not have generics, this function emits a warning
88 | * to inform them that they probably passed the wrong {@code type}
89 | * argument, and meant to invoke {@link #getFirstGeneric(Type, Type)} instead.
90 | *
91 | * @param cl The class. This parameter is ignored
92 | * @param fallback The fallback to return
93 | * @return The fallback type
94 | * @see #getFirstGeneric(Type, Type)
95 | * @deprecated Classes do not have generics. You might have passed
96 | * the wrong parameters.
97 | */
98 | @Deprecated
99 | @Contract("_,_ -> param2")
100 | public static Type getFirstGeneric(@NotNull Class> cl, @NotNull Type fallback) {
101 | return fallback;
102 | }
103 |
104 | /**
105 | * Returns the first generic type of the given (possibly parameterized)
106 | * type {@code genericType}. If the type is not parameterized,
107 | * this will return {@code fallback}.
108 | *
109 | * @param genericType The generic type
110 | * @param fallback The fallback to return
111 | * @return The generic type
112 | */
113 | public static Type getFirstGeneric(@NotNull Type genericType, @NotNull Type fallback) {
114 | try {
115 | return ((ParameterizedType) genericType).getActualTypeArguments()[0];
116 | } catch (ClassCastException e) {
117 | return fallback;
118 | }
119 | }
120 |
121 | /**
122 | * Legally stolen and re-adapted from Guava's PeekingImpl class
123 | *
124 | * A {@link Iterator} wrapper that allows peeking at the next element
125 | * without advancing the iterator.
126 | *
127 | * @param The element type
128 | */
129 | public static final class PeekingIterator implements Iterator {
130 |
131 | private final @NotNull Iterator extends E> iterator;
132 | private @Nullable E peekedElement;
133 | private boolean hasPeeked;
134 |
135 | PeekingIterator(@NotNull Iterator extends E> iterator) {
136 | this.iterator = iterator;
137 | }
138 |
139 | /**
140 | * Returns {@code true} if there are more elements in the iteration.
141 | *
142 | * @return {@code true} if the iteration has more elements.
143 | */
144 | public boolean hasNext() {
145 | return this.hasPeeked || this.iterator.hasNext();
146 | }
147 |
148 | /**
149 | * Returns the next element in the iteration. If peeked, returns the peeked element.
150 | *
151 | * @return The next element.
152 | * @throws NoSuchElementException If no more elements.
153 | */
154 | public E next() {
155 | if (!this.hasPeeked) {
156 | return this.iterator.next();
157 | } else {
158 | E result = this.peekedElement;
159 | this.hasPeeked = false;
160 | this.peekedElement = null;
161 | return result;
162 | }
163 | }
164 |
165 | /**
166 | * Removes the last element returned by {@code next()}.
167 | *
168 | * @throws IllegalStateException If {@code peek()} was called after the last {@code next()}.
169 | */
170 | public void remove() {
171 | if (hasPeeked)
172 | throw new IllegalStateException("Can't remove after you've peeked at next");
173 | this.iterator.remove();
174 | }
175 |
176 | /**
177 | * Peeks at the next element without advancing the iterator.
178 | *
179 | * @return The next element.
180 | * @throws NoSuchElementException If no more elements.
181 | */
182 | public E peek() {
183 | if (!this.hasPeeked) {
184 | this.peekedElement = this.iterator.next();
185 | this.hasPeeked = true;
186 | }
187 |
188 | return this.peekedElement;
189 | }
190 |
191 | /**
192 | * Creates a new {@code PeekingIterator} from the given iterator.
193 | *
194 | * @param The type of elements.
195 | * @param iterator The iterator to wrap.
196 | * @return A new {@code PeekingIterator}.
197 | */
198 | public static @NotNull PeekingIterator from(@NotNull Iterator iterator) {
199 | return new PeekingIterator<>(iterator);
200 | }
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/src/main/java/revxrsal/spec/MapProxy.java:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Cream, licensed under the MIT License.
3 | *
4 | * Copyright (c) Revxrsal
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package revxrsal.spec;
25 |
26 | import lombok.SneakyThrows;
27 | import org.jetbrains.annotations.Contract;
28 | import org.jetbrains.annotations.NotNull;
29 | import revxrsal.spec.annotation.*;
30 |
31 | import java.lang.invoke.MethodHandle;
32 | import java.lang.reflect.InvocationHandler;
33 | import java.lang.reflect.Method;
34 | import java.lang.reflect.Proxy;
35 | import java.util.*;
36 | import java.util.concurrent.ConcurrentHashMap;
37 |
38 | import static revxrsal.spec.SpecProperty.impliesSetter;
39 | import static revxrsal.spec.SpecProperty.keyOf;
40 | import static revxrsal.spec.Specs.createDefaultMap;
41 | import static revxrsal.spec.Specs.isConfigSpec;
42 |
43 | /**
44 | * Generates proxies that are backed by {@link Map maps}.
45 | */
46 | final class MapProxy implements InvocationHandler {
47 |
48 | @SuppressWarnings("unchecked")
49 | public static @NotNull T generate(@NotNull Class type, @NotNull Map map) {
50 | return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, new MapProxy<>(type, map));
51 | }
52 |
53 | @SuppressWarnings({"unchecked"})
54 | @Contract("null -> fail")
55 | static @NotNull Map getInternalMap(T value) {
56 | Objects.requireNonNull(value, "value is null!");
57 | if (!Proxy.isProxyClass(value.getClass())) {
58 | for (Class> cInterface : value.getClass().getInterfaces()) {
59 | if (isConfigSpec(cInterface))
60 | throw new IllegalArgumentException("Don't try to create an instance of a ConfigSpec directly! " + "Use Specs.createDefault() or Specs.createUnsafe() instead. " + "Tried to create an instance of " + cInterface + ".");
61 | }
62 | throw new IllegalArgumentException("Not a proxy instance: " + value);
63 | }
64 | InvocationHandler handler = Proxy.getInvocationHandler(value);
65 | if (!(handler instanceof MapProxy)) {
66 | throw new IllegalArgumentException("Not a config spec: " + value + " (proxy is handled by " + handler + ")");
67 | }
68 | return ((MapProxy) handler).map;
69 | }
70 |
71 | private static final Method TO_STRING;
72 | private static final Method EQUALS;
73 | private static final Method HASH_CODE;
74 |
75 | private final Class type;
76 | private final Map map;
77 |
78 | private Map defaultMethodHandles;
79 | private Map memoized;
80 |
81 | public MapProxy(Class type, Map map) {
82 | this.type = type;
83 | this.map = map;
84 | }
85 |
86 | @Override
87 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
88 | if (method.equals(TO_STRING)) {
89 | return generateToString();
90 | }
91 | if (method.equals(EQUALS)) {
92 | Object other = args[0];
93 | if (!Proxy.isProxyClass(other.getClass())) {
94 | return false;
95 | }
96 | if (isConfigSpec(other.getClass())) {
97 | MapProxy> otherHandler = (MapProxy>) Proxy.getInvocationHandler(args);
98 | return map.equals(otherHandler.map);
99 | }
100 | }
101 | if (method.equals(HASH_CODE)) {
102 | return map.hashCode();
103 | }
104 | if (method.isAnnotationPresent(IgnoreMethod.class)) {
105 | return asMethodHandle(method).bindTo(proxy).invokeWithArguments(args);
106 | }
107 | if (method.isAnnotationPresent(Memoize.class)) {
108 | if (!method.isDefault())
109 | throw new IllegalArgumentException("@Memoize methods must be default!");
110 | if (memoized == null) memoized = new ConcurrentHashMap<>();
111 | return memoized.computeIfAbsent(method, m -> {
112 | try {
113 | return asMethodHandle(m).bindTo(proxy).invokeWithArguments(args);
114 | } catch (Throwable e) {
115 | sneakyThrow(e);
116 | return null;
117 | }
118 | });
119 | }
120 | if (method.isAnnotationPresent(AsMap.class)) {
121 | AsMap asMap = method.getAnnotation(AsMap.class);
122 | switch (Objects.requireNonNull(asMap).value()) {
123 | case CLONE:
124 | return new LinkedHashMap<>(map);
125 | case IMMUTABLE_VIEW:
126 | return Collections.unmodifiableMap(map);
127 | case UNDERLYING_MAP:
128 | return map;
129 | }
130 | }
131 | if (method.isAnnotationPresent(Reload.class)) {
132 | throw new IllegalStateException("You cannot reload this! Try to reload the top entity.");
133 | }
134 | if (method.isAnnotationPresent(Save.class)) {
135 | throw new IllegalStateException("You cannot save this! Try to save the top entity.");
136 | }
137 | if (method.isAnnotationPresent(Reset.class)) {
138 | this.map.clear();
139 | if (memoized != null) memoized.clear();
140 | //noinspection unchecked
141 | createDefaultMap(type, (T) proxy, this.map);
142 | return null;
143 | }
144 | String key = keyOf(method);
145 | if (method.getReturnType() == Void.TYPE || impliesSetter(method)) {
146 | map.put(key, args[0]);
147 | if (memoized != null) memoized.clear();
148 | return null;
149 | } else {
150 | return map.get(key);
151 | }
152 | }
153 |
154 | @SneakyThrows
155 | private MethodHandle asMethodHandle(Method m) {
156 | if (defaultMethodHandles == null) defaultMethodHandles = new HashMap<>();
157 | MethodHandle mh = defaultMethodHandles.get(m);
158 | if (mh == null) {
159 | mh = MHLookup.privateLookupIn(type).in(type).unreflectSpecial(m, type);
160 | defaultMethodHandles.put(m, mh);
161 | }
162 | return mh;
163 | }
164 |
165 | private String generateToString() {
166 | StringBuilder sb = new StringBuilder(type.getSimpleName() + "(");
167 | Iterator> it = map.entrySet().iterator();
168 |
169 | while (it.hasNext()) {
170 | Map.Entry entry = it.next();
171 | sb.append(entry.getKey()).append("=").append(entry.getValue());
172 | if (it.hasNext()) {
173 | sb.append(", ");
174 | }
175 | }
176 |
177 | sb.append(")");
178 | return sb.toString();
179 | }
180 |
181 | static {
182 | try {
183 | TO_STRING = Object.class.getDeclaredMethod("toString");
184 | EQUALS = Object.class.getDeclaredMethod("equals", Object.class);
185 | HASH_CODE = Object.class.getDeclaredMethod("hashCode");
186 | } catch (NoSuchMethodException e) {
187 | throw new RuntimeException(e);
188 | }
189 | }
190 |
191 | private static RuntimeException sneakyThrow(Throwable t) {
192 | if (t == null) throw new NullPointerException("t");
193 | return sneakyThrow0(t);
194 | }
195 |
196 | @SuppressWarnings("unchecked")
197 | @SneakyThrows
198 | private static T sneakyThrow0(Throwable t) throws T {
199 | throw (T) t;
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/src/main/java/revxrsal/spec/Specs.java:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Cream, licensed under the MIT License.
3 | *
4 | * Copyright (c) Revxrsal
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package revxrsal.spec;
25 |
26 | import lombok.SneakyThrows;
27 | import org.jetbrains.annotations.NotNull;
28 | import revxrsal.spec.annotation.ConfigSpec;
29 |
30 | import java.io.File;
31 | import java.lang.invoke.MethodHandle;
32 | import java.lang.reflect.Array;
33 | import java.lang.reflect.Method;
34 | import java.nio.file.Path;
35 | import java.util.*;
36 | import java.util.concurrent.ConcurrentHashMap;
37 |
38 | public final class Specs {
39 |
40 | private static final Map, SpecClass> IMPLEMENTATIONS = new ConcurrentHashMap<>();
41 |
42 | /**
43 | * Tests whether the given class is a spec interface or not
44 | *
45 | * @param cl The class to check for
46 | * @return true if it's a spec
47 | */
48 | public static boolean isConfigSpec(@NotNull Class> cl) {
49 | return cl.isInterface() && cl.isAnnotationPresent(ConfigSpec.class);
50 | }
51 |
52 | /**
53 | * Generates a {@link SpecReference} for the specified config spec interface.
54 | *
55 | * A reference offers a flexible wrapper around the config spec value.
56 | *
57 | * @param type The interface type
58 | * @param config The config file
59 | * @param The type
60 | * @return The newly created {@link SpecReference}.
61 | */
62 | public static @NotNull SpecReference reference(@NotNull Class type, @NotNull CommentedConfiguration config) {
63 | if (!isConfigSpec(type))
64 | throw new IllegalArgumentException(type + " must be a spec class!");
65 | return new SpecReference<>(type, config);
66 | }
67 |
68 | /**
69 | * Generates a config spec from the specified config.
70 | *
71 | * @param type The interface type
72 | * @param config The config file
73 | * @param The type
74 | * @return The newly created config spec.
75 | */
76 | public static @NotNull T fromConfig(@NotNull Class type, @NotNull CommentedConfiguration config) {
77 | return reference(type, config).get();
78 | }
79 |
80 | /**
81 | * Generates a config spec from the specified file.
82 | *
83 | * @param type The interface type
84 | * @param config The config file
85 | * @param The type
86 | * @return The newly created config spec.
87 | */
88 | public static @NotNull T fromFile(@NotNull Class type, @NotNull Path config) {
89 | return reference(type, CommentedConfiguration.from(config)).get();
90 | }
91 |
92 | /**
93 | * Generates a config spec from the specified file.
94 | *
95 | * @param type The interface type
96 | * @param config The config file
97 | * @param The type
98 | * @return The newly created config spec.
99 | */
100 | public static @NotNull T fromFile(@NotNull Class type, @NotNull File config) {
101 | return reference(type, CommentedConfiguration.from(config.toPath())).get();
102 | }
103 |
104 | /**
105 | * Loads or generates (if necessary) all the information needed for the
106 | * given spec
107 | *
108 | * @param interfaceType The spec type
109 | * @return The generated {@link SpecClass}
110 | */
111 | public static @NotNull SpecClass from(@NotNull Class> interfaceType) {
112 | if (!interfaceType.isInterface())
113 | throw new IllegalArgumentException("Class is not an interface.");
114 | if (!interfaceType.isAnnotationPresent(ConfigSpec.class))
115 | throw new IllegalArgumentException("Interface must have @ConfigSpec");
116 |
117 | return IMPLEMENTATIONS.computeIfAbsent(interfaceType, SpecClass::from);
118 | }
119 |
120 | /**
121 | * Creates a spec with the default values for the given spec type.
122 | *
123 | * If the spec contains other nested specs, they will be generated with their
124 | * default values as well.
125 | *
126 | * Lists, maps, sets, arrays, and primitive types will be initialized to
127 | * empty values. Everything else will be null.
128 | *
129 | * @param interfaceType The spec interface
130 | * @param The spec type
131 | * @return The newly created instance.
132 | */
133 | @SneakyThrows
134 | public static @NotNull T createDefault(@NotNull Class interfaceType) {
135 | if (!isConfigSpec(interfaceType))
136 | throw new IllegalArgumentException(interfaceType + " must be a spec class!");
137 | Map properties = new LinkedHashMap<>();
138 | T proxy = MapProxy.generate(interfaceType, properties);
139 | createDefaultMap(interfaceType, proxy, properties);
140 | return proxy;
141 | }
142 |
143 | /**
144 | * Creates a spec with the given values from the map. Note that this
145 | * method does not respect default values, so it is the user's responsibility
146 | * to guarantee those.
147 | *
148 | * @param interfaceType The spec interface
149 | * @param The spec type
150 | * @return The newly created instance.
151 | */
152 | @SneakyThrows
153 | public static @NotNull T createUnsafe(
154 | @NotNull Class interfaceType,
155 | @NotNull Map properties
156 | ) {
157 | return MapProxy.generate(interfaceType, properties);
158 | }
159 |
160 | /**
161 | * Returns the internal map of the given spec. Modifying this map
162 | * will immediately modify the spec, so be careful with it!
163 | *
164 | * @return The internal map
165 | */
166 | public static @NotNull Map getInternalMap(@NotNull Object configSpec) {
167 | return MapProxy.getInternalMap(configSpec);
168 | }
169 |
170 | @SneakyThrows
171 | static void createDefaultMap(@NotNull Class interfaceType, T proxy, @NotNull Map properties) {
172 | SpecClass specClass = from(interfaceType);
173 | for (SpecProperty value : specClass.properties().values()) {
174 | if (value.isHandledByProxy())
175 | continue;
176 | if (value.hasDefault()) {
177 | Method getter = value.getter();
178 | MethodHandle getterHandle = MHLookup.privateLookupIn(interfaceType)
179 | .in(interfaceType)
180 | .unreflectSpecial(getter, interfaceType);
181 | properties.put(value.key(), getterHandle.invoke(proxy));
182 | } else {
183 | Class> type = value.type();
184 | if (isConfigSpec(type)) {
185 | Object v = createDefault(type);
186 | properties.put(value.key(), v);
187 | } else if (type == List.class
188 | || type == Iterable.class
189 | || type == Collection.class
190 | ) {
191 | properties.put(value.key(), new ArrayList<>());
192 | } else if (type == Set.class) {
193 | properties.put(value.key(), new LinkedHashSet<>());
194 | } else if (type == Map.class) {
195 | properties.put(value.key(), new LinkedHashMap<>());
196 | } else if (type.isArray()) {
197 | Object array = Array.newInstance(type.getComponentType(), 0);
198 | properties.put(value.key(), array);
199 | } else if (type == boolean.class) {
200 | properties.put(value.key(), false);
201 | } else if (type == byte.class) {
202 | properties.put(value.key(), (byte) 0);
203 | } else if (type == char.class) {
204 | properties.put(value.key(), '\u0000');
205 | } else if (type == short.class) {
206 | properties.put(value.key(), (short) 0);
207 | } else if (type == int.class) {
208 | properties.put(value.key(), 0);
209 | } else if (type == long.class) {
210 | properties.put(value.key(), 0L);
211 | } else if (type == float.class) {
212 | properties.put(value.key(), 0.0f);
213 | } else if (type == double.class) {
214 | properties.put(value.key(), 0.0d);
215 | } else {
216 | properties.put(value.key(), null);
217 | }
218 | }
219 | }
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/src/main/java/revxrsal/spec/SpecProperty.java:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Cream, licensed under the MIT License.
3 | *
4 | * Copyright (c) Revxrsal
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package revxrsal.spec;
25 |
26 | import com.google.gson.annotations.SerializedName;
27 | import lombok.Getter;
28 | import lombok.RequiredArgsConstructor;
29 | import org.jetbrains.annotations.NotNull;
30 | import org.jetbrains.annotations.Nullable;
31 | import org.jetbrains.annotations.Unmodifiable;
32 | import revxrsal.spec.annotation.*;
33 |
34 | import java.lang.annotation.Annotation;
35 | import java.lang.reflect.AnnotatedElement;
36 | import java.lang.reflect.Method;
37 | import java.lang.reflect.Modifier;
38 | import java.util.*;
39 |
40 | import static java.util.stream.Collectors.toList;
41 | import static revxrsal.spec.CommentedConfiguration.NEW_LINE;
42 |
43 | /**
44 | * Represents a property in a {@link ConfigSpec}
45 | */
46 | @RequiredArgsConstructor
47 | public final class SpecProperty {
48 |
49 | /**
50 | * The key of the property, set by {@link Key @Key}
51 | */
52 | private final @NotNull String key;
53 |
54 | /**
55 | * The property type
56 | */
57 | private Class> type;
58 |
59 | /**
60 | * The property getter method
61 | */
62 | private Method getter;
63 |
64 | /**
65 | * The property setter method. Could be null
66 | */
67 | private @Nullable Method setter;
68 |
69 | /**
70 | * The comments on this property
71 | */
72 | private @Unmodifiable List comments = Collections.emptyList();
73 |
74 | /**
75 | * Tests whether is that property handled by the proxy, and does
76 | * not represent an actual property.
77 | *
78 | * See {@link HandledByProxy}
79 | */
80 | @Getter
81 | private boolean isHandledByProxy;
82 |
83 | /**
84 | * The key of the property, set by {@link Key @Key} or {@link SerializedName @SerializedName}.
85 | *
86 | * If none of the given annotations is specified, it will use the field
87 | * name (strips is-, get- and set- prefixes)
88 | *
89 | * @return The property name
90 | */
91 | public @NotNull String key() {
92 | return key;
93 | }
94 |
95 | /**
96 | * Returns the getter method of this property
97 | *
98 | * @return The getter method
99 | */
100 | public @NotNull Method getter() {
101 | return getter;
102 | }
103 |
104 | /**
105 | * Tests whether this property has a default value or not
106 | *
107 | * @return if this property has a default value or not
108 | */
109 | public boolean hasDefault() {
110 | return getter.isDefault();
111 | }
112 |
113 | /**
114 | * Returns the setter of this property. Could be null if the
115 | * property does not define a setter.
116 | *
117 | * @return The property setter
118 | */
119 | public @Nullable Method setter() {
120 | return setter;
121 | }
122 |
123 | /**
124 | * Returns the comments on this property, specified by {@link Comment @Comment}.
125 | *
126 | * @return The comments
127 | */
128 | public @NotNull @Unmodifiable List comments() {
129 | return comments;
130 | }
131 |
132 | /**
133 | * Tests if this property has any comments on it or not
134 | *
135 | * @return if the property has comments
136 | */
137 | public boolean hasComments() {
138 | return !comments.isEmpty();
139 | }
140 |
141 | /**
142 | * Sets the type of this property. This performs type checks to ensure
143 | * the two types do not conflict
144 | *
145 | * @param type The new type
146 | */
147 | private void setType(@Nullable Class> type) {
148 | if (this.type == null)
149 | this.type = type;
150 | else if (!this.type.equals(type))
151 | throw new IllegalArgumentException("Inconsistent types for property " + key + ". Received " + this.type + " and " + type + ".");
152 | }
153 |
154 | /**
155 | * Tests if the method implies a setter or not. This simply
156 | * checks if the method name starts with 'set'.
157 | *
158 | * @param method The method to check for
159 | * @return if it implies a setter
160 | */
161 | public static boolean impliesSetter(@NotNull Method method) {
162 | return method.getName().startsWith("set");
163 | }
164 |
165 | /**
166 | * Returns the name of the property that is represented
167 | * by this method
168 | *
169 | * @param method The method to get for
170 | * @return The property name
171 | */
172 | public static @NotNull String keyOf(@NotNull Method method) {
173 | Key key = method.getAnnotation(Key.class);
174 | if (key != null)
175 | return key.value();
176 | SerializedName sn = method.getAnnotation(SerializedName.class);
177 | if (sn != null)
178 | return sn.value();
179 | return fromName(method.getName());
180 | }
181 |
182 | /**
183 | * Strips get-, is- and set- prefixes from the given name if necessary.
184 | *
185 | * @param name The name to strip from
186 | * @return The new name
187 | */
188 | private static @NotNull String fromName(@NotNull String name) {
189 | if (name.startsWith("get") || name.startsWith("set"))
190 | return lowerFirst(name.substring(3));
191 | else if (name.startsWith("is"))
192 | return lowerFirst(name.substring(2));
193 | return name;
194 | }
195 |
196 | /**
197 | * Lower-cases the first letter of the given string
198 | *
199 | * @param name The string
200 | * @return The new string
201 | */
202 | private static @NotNull String lowerFirst(@NotNull String name) {
203 | if (name.isEmpty())
204 | return name;
205 | return Character.toLowerCase(name.charAt(0)) + name.substring(1);
206 | }
207 |
208 | static @NotNull @Unmodifiable Map propertiesOf(@NotNull Class> interfaceType) {
209 | Objects.requireNonNull(interfaceType, "interface cannot be null!");
210 | if (!interfaceType.isInterface())
211 | throw new IllegalArgumentException("Class is not an interface: " + interfaceType.getName());
212 | if (!interfaceType.isAnnotationPresent(ConfigSpec.class))
213 | throw new IllegalArgumentException("Interface does not have @ConfigSpec on it!");
214 | Map properties = new LinkedHashMap<>();
215 | Method[] methods = interfaceType.getMethods();
216 |
217 | sortByAnnotation(methods);
218 | for (Method method : methods) {
219 | if (Modifier.isStatic(method.getModifiers()))
220 | continue;
221 | if (method.isAnnotationPresent(IgnoreMethod.class)) {
222 | if (method.isDefault())
223 | continue;
224 | else
225 | throw new IllegalArgumentException("Cannot ignore a non-default method! Ignored methods must be default");
226 | }
227 | parse(method, properties);
228 | }
229 | for (SpecProperty value : properties.values()) {
230 | if (value.type == null)
231 | throw new IllegalArgumentException("Failed to infer the type of property '" + value.key + "'!");
232 | if (value.getter == null)
233 | throw new IllegalArgumentException("No getter exists for property '" + value.key + "'!");
234 | }
235 | return Collections.unmodifiableMap(properties);
236 | }
237 |
238 | private static void sortByAnnotation(T[] methods) {
239 | Arrays.sort(methods, (o1, o2) -> {
240 | Order order1 = o1.getAnnotation(Order.class);
241 | Order order2 = o2.getAnnotation(Order.class);
242 | if (order1 == null && order2 == null)
243 | return 0; // Both methods are unannotated
244 | if (order1 == null)
245 | return -1; // o1 is unannotated, so it comes first
246 | if (order2 == null)
247 | return 1; // o2 is unannotated, so it comes first
248 | // Both methods have the annotation, compare their values
249 | return Integer.compare(order1.value(), order2.value());
250 | });
251 | }
252 |
253 | private static void parse(
254 | @NotNull Method method,
255 | @NotNull Map properties
256 | ) {
257 | String key = keyOf(method);
258 | SpecProperty existing = properties.computeIfAbsent(key, SpecProperty::new);
259 | @Nullable List comments = commentsOf(method);
260 | if (Arrays.stream(method.getAnnotations()).anyMatch(SpecProperty::isHandledByProxy)) {
261 | existing.isHandledByProxy = true;
262 | existing.getter = method;
263 | existing.setType(method.getReturnType());
264 | return;
265 | }
266 | if (comments != null) {
267 | if (existing.comments.isEmpty())
268 | existing.comments = comments;
269 | else
270 | throw new IllegalArgumentException("Inconsistent comments for property '" + key + "'");
271 | }
272 | if (method.getReturnType() == Void.TYPE || impliesSetter(method)) {
273 | if (existing.setter != null)
274 | throw new IllegalArgumentException("Found 2 setters for property '" + key + "'!");
275 | if (method.getReturnType() != Void.TYPE)
276 | throw new IllegalArgumentException("Setter for property '" + key + "' must return void!");
277 | if (method.getParameterCount() == 0)
278 | throw new IllegalArgumentException("Setter for property '" + key + "' has no parameters!");
279 | if (method.getParameterCount() > 1)
280 | throw new IllegalArgumentException("Setter for property '" + key + "' has more than 1 parameter!");
281 | existing.setType(method.getParameterTypes()[0]);
282 | existing.setter = method;
283 | } else {
284 | if (existing.getter != null)
285 | throw new IllegalArgumentException("Found 2 getters for property '" + key + "'!");
286 | if (method.getParameterCount() != 0)
287 | throw new IllegalArgumentException("Getter for property '" + key + "' cannot take parameters!");
288 | existing.setType(method.getReturnType());
289 | existing.getter = method;
290 | }
291 | }
292 |
293 | private static boolean isHandledByProxy(Annotation annotation) {
294 | return annotation.annotationType().isAnnotationPresent(HandledByProxy.class);
295 | }
296 |
297 | private static @Nullable List commentsOf(@NotNull Method method) {
298 | Comment comment = method.getAnnotation(Comment.class);
299 | if (comment != null) {
300 | String[] value = comment.value();
301 | return Arrays.stream(value)
302 | .flatMap(NEW_LINE::splitAsStream)
303 | .collect(toList());
304 | }
305 | return null;
306 | }
307 |
308 | public static @NotNull List headerOf(@NotNull Class> type) {
309 | ConfigSpec spec = type.getAnnotation(ConfigSpec.class);
310 | if (spec != null) {
311 | String[] value = spec.header();
312 | return Arrays.stream(value)
313 | .flatMap(NEW_LINE::splitAsStream)
314 | .collect(toList());
315 | }
316 | return Collections.emptyList();
317 | }
318 |
319 | @Override
320 | public String toString() {
321 | return "SpecProperty(key='" + key + "')";
322 | }
323 |
324 | public Class> type() {
325 | return type;
326 | }
327 |
328 | }
329 |
--------------------------------------------------------------------------------
/src/main/java/revxrsal/spec/CommentedConfiguration.java:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is part of Cream, licensed under the MIT License.
3 | *
4 | * Copyright (c) Revxrsal
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package revxrsal.spec;
25 |
26 | import com.google.gson.Gson;
27 | import com.google.gson.GsonBuilder;
28 | import com.google.gson.reflect.TypeToken;
29 | import lombok.SneakyThrows;
30 | import org.jetbrains.annotations.NotNull;
31 | import org.jetbrains.annotations.Nullable;
32 | import org.jetbrains.annotations.UnmodifiableView;
33 | import org.yaml.snakeyaml.DumperOptions;
34 | import org.yaml.snakeyaml.Yaml;
35 | import org.yaml.snakeyaml.events.*;
36 | import revxrsal.spec.Util.PeekingIterator;
37 |
38 | import java.io.BufferedReader;
39 | import java.io.BufferedWriter;
40 | import java.io.StringReader;
41 | import java.lang.reflect.Method;
42 | import java.lang.reflect.Type;
43 | import java.nio.file.Files;
44 | import java.nio.file.Path;
45 | import java.util.*;
46 | import java.util.regex.Pattern;
47 |
48 | import static java.nio.file.StandardOpenOption.*;
49 | import static java.util.regex.Pattern.LITERAL;
50 |
51 | /**
52 | * A configuration that supports comments. Set comments with
53 | * {@link #setComments(Map)}
54 | */
55 | public class CommentedConfiguration {
56 |
57 | private static final ThreadLocal YAML = ThreadLocal.withInitial(() -> {
58 | DumperOptions options = new DumperOptions();
59 | setProcessComments(options, false);
60 | options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
61 | return new Yaml(options);
62 | });
63 |
64 | public static final Gson GSON = new GsonBuilder()
65 | .registerTypeAdapterFactory(SpecAdapterFactory.INSTANCE)
66 | .create();
67 |
68 | /**
69 | * Pattern for matching newline characters.
70 | */
71 | public static final Pattern NEW_LINE = Pattern.compile("\n", LITERAL);
72 | private static final Type MAP_TYPE = new TypeToken