├── settings.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── test │ └── java │ │ └── revxrsal │ │ └── spec │ │ └── test │ │ ├── Example.java │ │ └── ServerConfig.java └── main │ └── java │ └── revxrsal │ └── spec │ ├── ArrayCommentStyle.java │ ├── annotation │ ├── Save.java │ ├── Reload.java │ ├── Reset.java │ ├── Order.java │ ├── HandledByProxy.java │ ├── Memoize.java │ ├── ConfigSpec.java │ ├── Key.java │ ├── IgnoreMethod.java │ ├── Comment.java │ └── AsMap.java │ ├── MHLookup.java │ ├── SpecProxy.java │ ├── SpecReference.java │ ├── SpecClass.java │ ├── SpecAdapterFactory.java │ ├── Util.java │ ├── MapProxy.java │ ├── Specs.java │ ├── SpecProperty.java │ └── CommentedConfiguration.java ├── .github └── workflows │ ├── gradle.yml │ └── publish.yml ├── .gitignore ├── LICENSE ├── gradlew.bat ├── README.md └── gradlew /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "spec" 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Revxrsal/spec/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Apr 25 22:46:36 EET 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /src/test/java/revxrsal/spec/test/Example.java: -------------------------------------------------------------------------------- 1 | package revxrsal.spec.test; 2 | 3 | import revxrsal.spec.Specs; 4 | 5 | import java.io.File; 6 | 7 | public class Example { 8 | 9 | public static void main(String[] args) { 10 | ServerConfig config = Specs.fromFile(ServerConfig.class, new File("server.yml")); 11 | System.out.println(config); 12 | config.save(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/revxrsal/spec/ArrayCommentStyle.java: -------------------------------------------------------------------------------- 1 | package revxrsal.spec; 2 | 3 | /** 4 | * The array comment styles in a {@link CommentedConfiguration} 5 | */ 6 | public enum ArrayCommentStyle { 7 | 8 | /** 9 | * Comments the first element only 10 | */ 11 | COMMENT_FIRST_ELEMENT, 12 | 13 | /** 14 | * Comments all elements 15 | */ 16 | COMMENT_ALL_ELEMENTS, 17 | 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Java CI with Gradle 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v2 16 | 17 | - name: Set up JDK 17 18 | uses: actions/setup-java@v2 19 | with: 20 | java-version: '17' 21 | distribution: 'microsoft' 22 | 23 | - name: Build JAR 24 | run: | 25 | chmod +x ./gradlew 26 | ./gradlew build 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | *.iws 13 | *.iml 14 | *.ipr 15 | out/ 16 | !**/src/main/**/out/ 17 | !**/src/test/**/out/ 18 | 19 | ### Eclipse ### 20 | .apt_generated 21 | .classpath 22 | .factorypath 23 | .project 24 | .settings 25 | .springBeans 26 | .sts4-cache 27 | bin/ 28 | !**/src/main/**/bin/ 29 | !**/src/test/**/bin/ 30 | 31 | ### NetBeans ### 32 | /nbproject/private/ 33 | /nbbuild/ 34 | /dist/ 35 | /nbdist/ 36 | /.nb-gradle/ 37 | 38 | ### VS Code ### 39 | .vscode/ 40 | 41 | ### Mac OS ### 42 | .DS_Store -------------------------------------------------------------------------------- /src/main/java/revxrsal/spec/annotation/Save.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 | * Marks a function as being the reload function for a {@link ConfigSpec}. 10 | *

11 | * Note that only top-level values can be saved! 12 | *

13 | * Example: 14 | *

{@code @ConfigSpec
15 |  * public interface GameSettings {
16 |  *
17 |  *     @Comment("The cooldown message. Use %cooldown% as a placeholder.")
18 |  *     default String cooldownMessage() {
19 |  *         return "Starting in %cooldown%s";
20 |  *     }
21 |  *
22 |  *     @Save
23 |  *     void save();
24 |  * }}
25 | */ 26 | @HandledByProxy 27 | @Target(ElementType.METHOD) 28 | @Retention(RetentionPolicy.RUNTIME) 29 | public @interface Save { 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/revxrsal/spec/annotation/Reload.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 | * Marks a function as being the reload function for a {@link ConfigSpec}. 10 | *

11 | * Note that only top-level values can be reloaded! 12 | *

13 | * Example: 14 | *

{@code @ConfigSpec
15 |  * public interface GameSettings {
16 |  *
17 |  *     @Comment("The cooldown message. Use %cooldown% as a placeholder.")
18 |  *     default String cooldownMessage() {
19 |  *         return "Starting in %cooldown%s";
20 |  *     }
21 |  *
22 |  *     @Reload
23 |  *     void reload();
24 |  * }}
25 | */ 26 | @HandledByProxy 27 | @Target(ElementType.METHOD) 28 | @Retention(RetentionPolicy.RUNTIME) 29 | public @interface Reload { 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/revxrsal/spec/annotation/Reset.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 | * Marks a function as being the reset function for a {@link ConfigSpec}. 10 | *

11 | * This will restore the config spec to its default state. All values will 12 | * be set to their default values. 13 | *

14 | * Example: 15 | *

{@code @ConfigSpec
16 |  * public interface GameSettings {
17 |  *
18 |  *     @Comment("The cooldown message. Use %cooldown% as a placeholder.")
19 |  *     default String cooldownMessage() {
20 |  *         return "Starting in %cooldown%s";
21 |  *     }
22 |  *
23 |  *     @Reset
24 |  *     void reset();
25 |  * }}
26 | */ 27 | @HandledByProxy 28 | @Target(ElementType.METHOD) 29 | @Retention(RetentionPolicy.RUNTIME) 30 | public @interface Reset { 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/revxrsal/spec/annotation/Order.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 | * Sets the index of the spec field according to other fields. This helps 10 | * ensure values come out in a specific order, as neither Gson nor Java guarantee 11 | * the field order. 12 | *

13 | * Lower values come first. 14 | *

15 | * Example: 16 | *

{@code @ConfigSpec
17 |  * public interface GameSettings {
18 |  *
19 |  *     @Order(0)
20 |  *     default int cooldown() {
21 |  *         return 20;
22 |  *     }
23 |  *
24 |  *     @Order(1)
25 |  *     String cooldownMessage();
26 |  * }}
27 | */ 28 | @Target(ElementType.METHOD) 29 | @Retention(RetentionPolicy.RUNTIME) 30 | public @interface Order { 31 | 32 | /** 33 | * The index value 34 | * 35 | * @return The index value 36 | */ 37 | int value(); 38 | 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Revxrsal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | release: 5 | types: [ created ] 6 | workflow_dispatch: 7 | 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | environment: Maven Central 13 | permissions: 14 | contents: read 15 | packages: write 16 | steps: 17 | - name: Checkout Project 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup Java 21 | uses: actions/setup-java@v4 22 | with: 23 | distribution: corretto 24 | java-version: 17 25 | 26 | - name: Setup Gradle 27 | uses: gradle/actions/setup-gradle@v3 28 | 29 | - name: Gradle Build 30 | run: | 31 | chmod +x ./gradlew 32 | ./gradlew build 33 | 34 | - name: Gradle Publish 35 | run: ./gradlew publishAndReleaseToMavenCentral 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_USERNAME }} 39 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_PASSWORD }} 40 | ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.MAVEN_SIGNING_KEY_ID }} 41 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.MAVEN_SIGNING_KEY }} 42 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.MAVEN_SIGNING_PASSWORD }} 43 | -------------------------------------------------------------------------------- /src/main/java/revxrsal/spec/annotation/HandledByProxy.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.ApiStatus; 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 | * Marks an annotation as handled by the proxy, and not a property. Used 35 | * on {@link Reload}, {@link Save}, {@link Reset} and {@link AsMap}. 36 | *

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 | *

21 | * Example: 22 | *

{@code @ConfigSpec
23 |  * public interface SearchArea {
24 |  *
25 |  *     default double radius() {
26 |  *         return 5;
27 |  *     }
28 |  *
29 |  *     void setRadius(double radius);
30 |  *
31 |  *     @Memoize
32 |  *     default double radiusCubed() {
33 |  *         System.out.println("Computing r^3");
34 |  *         return radius() * radius() * radius();
35 |  *     }
36 |  *
37 |  *     @Memoize
38 |  *     default double radiusSquared() {
39 |  *         System.out.println("Computing r^2");
40 |  *         return radius() * radius();
41 |  *     }
42 |  * }}
43 | * 44 | *
{@code
45 |  * SearchArea area = Specs.createDefault(SearchArea.class);
46 |  * System.out.println(area.radiusCubed());
47 |  * System.out.println(area.radiusCubed());
48 |  * area.setRadius(10);
49 |  * System.out.println(area.radiusCubed());
50 |  * System.out.println(area.radiusCubed());
51 |  * }
52 | * Will print: 53 | *
54 |  * Computing r^3
55 |  * 125.0
56 |  * 125.0
57 |  * Computing r^3
58 |  * 1000.0
59 |  * 1000.0
60 |  * 
61 | */ 62 | @HandledByProxy 63 | @Target(ElementType.METHOD) 64 | @Retention(RetentionPolicy.RUNTIME) 65 | public @interface Memoize { 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/revxrsal/spec/annotation/ConfigSpec.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.*; 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 | [![Discord](https://discord.com/api/guilds/939962855476846614/widget.png)](https://discord.gg/pEGGF785zp) 4 | [![Maven Central](https://img.shields.io/maven-metadata/v/https/repo1.maven.org/maven2/io/github/revxrsal/spec/maven-metadata.xml.svg?label=maven%20central&colorB=brightgreen)](https://search.maven.org/artifact/io.github.revxrsal/spec) 5 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | [![Build](https://github.com/Revxrsal/spec/actions/workflows/gradle.yml/badge.svg)](https://github.com/Revxrsal/spec/actions/workflows/gradle.yml) 7 | [![CodeFactor](https://www.codefactor.io/repository/github/revxrsal/spec/badge)](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: ![](https://img.shields.io/maven-metadata/v/https/repo1.maven.org/maven2/io/github/revxrsal/spec/maven-metadata.xml.svg?label=maven%20central\&colorB=brightgreen) 73 | 74 | #### Gradle 75 | 76 | ```groovy 77 | dependencies { 78 | implementation("io.github.revxrsal:spec:[VERSION]") 79 | } 80 | ``` 81 | 82 | Latest version: ![](https://img.shields.io/maven-metadata/v/https/repo1.maven.org/maven2/io/github/revxrsal/spec/maven-metadata.xml.svg?label=maven%20central\&colorB=brightgreen) 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 iterator; 132 | private @Nullable E peekedElement; 133 | private boolean hasPeeked; 134 | 135 | PeekingIterator(@NotNull Iterator 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>() { 73 | }.getType(); 74 | 75 | /** 76 | * YAML processor instance for reading and writing YAML data. 77 | */ 78 | protected final Yaml yaml; 79 | 80 | /** 81 | * A map storing comments associated with specific configuration keys. 82 | */ 83 | protected final Map configComments = new HashMap<>(); 84 | 85 | /** 86 | * A map storing comments associated with specific configuration keys. 87 | */ 88 | protected List headers = Collections.emptyList(); 89 | 90 | /** 91 | * Json instance for serializing and deserializing JSON data. 92 | */ 93 | protected final Gson gson; 94 | 95 | /** 96 | * Path to the configuration file. 97 | */ 98 | protected final Path file; 99 | 100 | /** 101 | * The JSON representation of the configuration data. 102 | */ 103 | protected Map data = new LinkedHashMap<>(); 104 | 105 | /** 106 | * The array commenting style 107 | */ 108 | protected final ArrayCommentStyle arrayCommentStyle; 109 | 110 | public CommentedConfiguration(Path file, Gson gson, ArrayCommentStyle arrayCommentStyle, Yaml yaml) { 111 | this.file = file; 112 | this.gson = gson; 113 | this.arrayCommentStyle = arrayCommentStyle; 114 | this.yaml = yaml; 115 | } 116 | 117 | public CommentedConfiguration(Path file, Gson gson, ArrayCommentStyle arrayCommentStyle) { 118 | this(file, gson, arrayCommentStyle, YAML.get()); 119 | } 120 | 121 | /** 122 | * Loads the content of this configuration 123 | */ 124 | @SneakyThrows 125 | public void load() { 126 | if (!Files.exists(file)) { 127 | data = new LinkedHashMap<>(); 128 | return; 129 | } 130 | try (BufferedReader reader = Files.newBufferedReader(file)) { 131 | data = (Map) yaml.load(reader); 132 | if (data == null) 133 | data = new LinkedHashMap<>(); 134 | } 135 | } 136 | 137 | /** 138 | * Sets the comment of the given path. 139 | * 140 | * @param path The comment path. Subkeys are delimited by '.', and array entries have 0 141 | * as their parent. 142 | * @param comment The comment 143 | */ 144 | public void setComment(@NotNull String path, @NotNull String comment) { 145 | this.configComments.put(path, comment); 146 | } 147 | 148 | /** 149 | * Sets the comments of this configuration file. 150 | * 151 | * @param comments The comments to set. Supports multiple lines (use \n as a spacer). 152 | */ 153 | public void setComments(@NotNull Map comments) { 154 | this.configComments.clear(); 155 | this.configComments.putAll(comments); 156 | } 157 | 158 | /** 159 | * Saves this configuration file with comments set with {@link #setComments(Map)}. 160 | */ 161 | @SneakyThrows 162 | public void save() { 163 | if (configComments.isEmpty()) { 164 | try (BufferedWriter writer = Files.newBufferedWriter(file, CREATE, TRUNCATE_EXISTING, WRITE)) { 165 | yaml.dump(data, writer); 166 | } 167 | return; 168 | } 169 | String simpleDump = yaml.dump(data); 170 | String[] split = NEW_LINE.split(simpleDump); 171 | List lines = new ArrayList<>(split.length); 172 | Collections.addAll(lines, split); 173 | StringReader reader = new StringReader(simpleDump); 174 | Iterable events = yaml.parse(reader); 175 | handleEvents(events.iterator(), lines); // terribly inefficient way but I can't care less lol 176 | if (!lines.isEmpty()) { 177 | String first = lines.get(0); 178 | if (Character.isWhitespace(first.charAt(0))) { 179 | lines.set(0, first.substring(1)); 180 | } 181 | } 182 | for (int i = 0; i < headers.size(); i++) { 183 | String l = headers.get(i); 184 | if (l.startsWith("#")) 185 | lines.add(i, "#" + l); 186 | else 187 | lines.add(i, "# " + l); 188 | } 189 | if (!headers.isEmpty()) { 190 | lines.add(headers.size(), ""); 191 | } 192 | if (file.getParent() != null) 193 | Files.createDirectories(file.getParent()); 194 | Files.write(file, lines, CREATE, TRUNCATE_EXISTING, WRITE); 195 | } 196 | 197 | /** 198 | * Create a config from a file 199 | * 200 | * @param file The file to load the config from. 201 | * @param json The JSON instance to deserialize with 202 | * @param arrayCommentStyle The array commenting style. See {@link ArrayCommentStyle}. 203 | * @return A new instance of CommentedConfiguration 204 | */ 205 | public static @NotNull CommentedConfiguration from( 206 | @NotNull Path file, 207 | @NotNull Gson json, 208 | @NotNull ArrayCommentStyle arrayCommentStyle 209 | ) { 210 | //Creating a blank instance of the config. 211 | return new CommentedConfiguration(file, json, arrayCommentStyle); 212 | } 213 | 214 | /** 215 | * Create a config from a file 216 | * 217 | * @param file The file to load the config from. 218 | * @param arrayCommentStyle The array commenting style. See {@link ArrayCommentStyle}. 219 | * @return A new instance of CommentedConfiguration 220 | */ 221 | public static @NotNull CommentedConfiguration from( 222 | @NotNull Path file, 223 | @NotNull ArrayCommentStyle arrayCommentStyle 224 | ) { 225 | //Creating a blank instance of the config. 226 | return new CommentedConfiguration(file, GSON, arrayCommentStyle); 227 | } 228 | 229 | /** 230 | * Create a config from a file 231 | * 232 | * @param file The file to load the config from. 233 | * @param gson The JSON instance to deserialize with 234 | * @return A new instance of CommentedConfiguration 235 | */ 236 | public static @NotNull CommentedConfiguration from( 237 | @NotNull Path file, 238 | @NotNull Gson gson 239 | ) { 240 | //Creating a blank instance of the config. 241 | return new CommentedConfiguration(file, gson, ArrayCommentStyle.COMMENT_FIRST_ELEMENT); 242 | } 243 | 244 | /** 245 | * Create a config from a file 246 | * 247 | * @param file The file to load the config from. 248 | * @return A new instance of CommentedConfiguration 249 | */ 250 | public static @NotNull CommentedConfiguration from(@NotNull Path file) { 251 | //Creating a blank instance of the config. 252 | return from(file, GSON); 253 | } 254 | 255 | /** 256 | * Retrieves the value for a key and deserializes it to the specified type. 257 | * 258 | * @param key The key to retrieve the value for. 259 | * @param type The type to deserialize the value into. 260 | * @param The type of the returned value. 261 | * @return The deserialized value. 262 | */ 263 | public T get(@NotNull String key, @NotNull Type type) { 264 | return fromValue(gson, data.get(key), Object.class, type); 265 | } 266 | 267 | 268 | /** 269 | * Deserializes the entire configuration data to the specified type. 270 | * 271 | * @param type The type to deserialize the data into. 272 | * @param The type of the returned value. 273 | * @return The deserialized data. 274 | */ 275 | public T getAs(@NotNull Type type) { 276 | return fromValue(gson, data, MAP_TYPE, type); 277 | } 278 | 279 | /** 280 | * Retrieves the value for a key and deserializes it to the specified class. 281 | * 282 | * @param key The key to retrieve the value for. 283 | * @param type The class to deserialize the value into. 284 | * @param The type of the returned value. 285 | * @return The deserialized value. 286 | */ 287 | public T get(@NotNull String key, @NotNull Class type) { 288 | return get(key, (Type) type); 289 | } 290 | 291 | /** 292 | * Sets a value for a key using JSON serialization. 293 | * 294 | * @param key The key to set the value for. 295 | * @param v The value to set. 296 | */ 297 | public void set(@NotNull String key, @Nullable Object v) { 298 | if (v == null) 299 | data.remove(key); 300 | else 301 | data.put(key, toJsonValue(gson, v, v.getClass())); 302 | } 303 | 304 | /** 305 | * Sets a value for a key using JSON serialization with a specific type. 306 | * 307 | * @param key The key to set the value for. 308 | * @param v The value to set. 309 | * @param type The type used for serialization. 310 | */ 311 | public void set(@NotNull String key, @NotNull Object v, @NotNull Type type) { 312 | data.put(key, toJsonValue(gson, v, type)); 313 | } 314 | 315 | private static Object toJsonValue(Gson gson, @NotNull Object o, @NotNull Type type) { 316 | String toJson = gson.toJson(o, type); 317 | return gson.fromJson(toJson, Object.class); 318 | } 319 | 320 | private static T fromValue(Gson gson, Object o, @NotNull Type valueType, @NotNull Type javaType) { 321 | String toJson = gson.toJson(o, valueType); 322 | return gson.fromJson(toJson, javaType); 323 | } 324 | 325 | /** 326 | * Checks if the configuration contains a value for the given path. 327 | * 328 | * @param path The path to check. 329 | * @return {@code true} if the path exists, {@code false} otherwise. 330 | */ 331 | public boolean contains(@NotNull String path) { 332 | return data.containsKey(path); 333 | } 334 | 335 | public void setHeaders(@NotNull List headers) { 336 | this.headers = headers; 337 | } 338 | 339 | /** 340 | * Replaces the configuration data with the given JSON object. 341 | * 342 | * @param data The new JSON object to set. 343 | */ 344 | public void setTo(@NotNull Object data, Type type) { 345 | Object value = toJsonValue(gson, data, type); 346 | if (!(value instanceof Map)) { 347 | throw new IllegalArgumentException("Expected data to be a map-like structure, found " + value); 348 | } 349 | //noinspection unchecked 350 | this.data = new LinkedHashMap<>((Map) value); 351 | } 352 | 353 | /** 354 | * Replaces the configuration data with the given JSON object. 355 | * 356 | * @param data The new JSON object to set. 357 | */ 358 | public void setTo(@NotNull Object data) { 359 | setTo(data, data.getClass()); 360 | } 361 | 362 | /** 363 | * Retrieves the entire configuration data as a JSON object. 364 | * 365 | * @return The configuration data. 366 | */ 367 | public @UnmodifiableView Map getData() { 368 | return Collections.unmodifiableMap(data); 369 | } 370 | 371 | protected void handleEvents(Iterator eventsI, List lines) { 372 | PeekingIterator events = PeekingIterator.from(eventsI); 373 | LinkedList path = new LinkedList<>(); 374 | Set commentsAdded = new HashSet<>(); 375 | boolean expectKey = true; 376 | boolean lastWasScalar = false; 377 | int offset = 0; 378 | while (events.hasNext()) { 379 | Event event = events.next(); 380 | if (event instanceof DocumentStartEvent) { 381 | expectKey = true; 382 | } 383 | if (event instanceof MappingStartEvent) { 384 | expectKey = true; 385 | } else if (event instanceof MappingEndEvent) { 386 | path.pollLast(); 387 | expectKey = true; 388 | if (events.hasNext()) { 389 | Event next = events.peek(); 390 | if (next instanceof ScalarEvent) { 391 | path.pollLast(); 392 | } 393 | } 394 | } else if (event instanceof ScalarEvent) { 395 | if (expectKey) { 396 | expectKey = false; 397 | if (lastWasScalar) 398 | path.removeLast(); 399 | path.add(((ScalarEvent) event).getValue()); 400 | } else { 401 | expectKey = true; 402 | } 403 | } 404 | if (event instanceof SequenceStartEvent) { 405 | path.add(SpecClass.ARRAY_INDEX); 406 | } else if (event instanceof SequenceEndEvent) { 407 | path.pollLast(); 408 | expectKey = true; 409 | if (events.hasNext()) { 410 | Event next = events.peek(); 411 | if (next instanceof ScalarEvent) { 412 | path.pollLast(); 413 | } 414 | } 415 | } 416 | 417 | lastWasScalar = event instanceof ScalarEvent; 418 | String commentPath = String.join(".", path); 419 | String comment = configComments.get(commentPath); 420 | if (comment != null && (commentsAdded.add(commentPath) || arrayCommentStyle == ArrayCommentStyle.COMMENT_ALL_ELEMENTS)) { 421 | lines.add(event.getStartMark().getLine() + (offset++), comment); 422 | } 423 | } 424 | } 425 | 426 | /** 427 | * Reflective access to the `setProcessComments` method in {@link DumperOptions}. 428 | */ 429 | private static @Nullable Method SET_PROCESS_COMMENTS; 430 | 431 | static { 432 | try { 433 | // Attempt to retrieve the private `setProcessComments` method. 434 | SET_PROCESS_COMMENTS = DumperOptions.class.getDeclaredMethod("setProcessComments", boolean.class); 435 | SET_PROCESS_COMMENTS.setAccessible(true); 436 | } catch (NoSuchMethodException ignored) { 437 | // Ignored as the method may not exist in older versions. 438 | } 439 | } 440 | 441 | /** 442 | * Sets the `processComments` flag on the given {@link DumperOptions} instance. 443 | * 444 | * @param options The {@link DumperOptions} instance. 445 | * @param process The value to set for `processComments`. 446 | */ 447 | @SneakyThrows 448 | protected static void setProcessComments(@NotNull DumperOptions options, boolean process) { 449 | if (SET_PROCESS_COMMENTS != null) 450 | SET_PROCESS_COMMENTS.invoke(options, process); 451 | } 452 | } 453 | --------------------------------------------------------------------------------