Special clone of diorite configs that supports java 8 (instead of 9) and use limited amount of dependencies, including support for older versions of snakeyaml.
Maven:
<dependencies>
<dependency>
<groupId>com.gotofinal</groupId>
<artifactId>diorite-configs-java8</artifactId>
<version>1.3.1</version>
</dependency>
</dependencies>
<repositories>
<repository>
<name>Diorite repository</name>
<id>diorite</id>
<url>https://repo.diorite.org/repository/diorite/</url>
</repository>
</repositories>
Jenkins: https://diorite.org/jenkins/job/diorite-configs-java8/
Also configs depends on diorite-commons-java8, snakeyaml, gson, apache commons lang3 and groovy, you must ensure that these libraries are available on classpath. (you can shade them with your project, note that spigot already contains snakeyaml, commons and gson libraries, PS: this library is NOT depending on spigot or any other minecraft related library)
Additionally some utilities and/or serializers have support for fastutil and vecmath libraries, but they are optional.
Basic idea of diorite configs is to represent configuration files as simple interfaces like:
import java.util.List;
import java.util.UUID;
import org.diorite.config.Config;
import org.diorite.config.annotations.Comment;
import org.diorite.config.annotations.Footer;
import org.diorite.config.annotations.HelperMethod;
import org.diorite.config.annotations.GroovyValidator;
import org.diorite.config.annotations.Header;
import org.diorite.config.annotations.Unmodifiable;
import org.diorite.config.annotations.Validator;
@Header("Comment on the top of the file")
@Footer("Comment on the bottom of the file")
public interface MyAppConfig extends Config
{
@Comment("This is app id, this comment will appear above field for this settings in configuration file.")
default UUID getAppId() { return UUID.randomUUID(); } // randomly generated UUID as default value
@GroovyValidator(isTrue = "x > 0", elseThrow = "MaxRequestsPerSecond can't be negative")
@GroovyValidator(isTrue = "x == 0", elseThrow = "MaxRequestsPerSecond can't be zero")
default int getMaxRequestsPerSecond() { return 5; }
void setMaxRequestsPerSecond(int newValue);
void addMaxRequestsPerSecond(int toAdd);
@Validator("maxRequestsPerSecond")
static int maxRequestsPerSecondValidator(int value) {
if (value > 100) {
return 100;
}
return value;
}
@Unmodifiable // so returned list is immutable
List<? extends UUID> getAPIIds();
boolean containsInAPIIds(UUID uuid);
boolean removeFromAPIIds(UUID uuid);
@HelperMethod
default void someCustomMethod() {
addMaxRequestsPerSecond(1);
}
// and much more
}
Then you can simply create instance of that interface and start using it!
MyAppConfig config = ConfigManager.createInstance(MyAppConfig.class);
config.bindFile(new File("someFile"));
config.load();
UUID appId = config.getAppId();
config.removeFromAPIIds(UUID.nameUUIDFromBytes("Huh".getBytes()));
config.save();
config.save(System.out); // there are methods to load/save config to/from other sources too.
Table of available methods that will be auto-implemented by diorite config system:
(for simplicity regex of property name (?<property>[A-Z0-9].*)
was replaced by <X>
and (?<property>[A-Z0-9].*?)
was replaced by <X?>
)
Name | Patterns | Examples | Return value |
---|---|---|---|
Get | get<X> |
int getMoney() |
required: define field type |
Set | set<X> |
void setMoney(int v) int setMoney(byte v) |
optional, returns previous value |
Equals | isEqualsTo<X> areEqualsTo<X> <X>isEqualsTo <X>areEqualsTo |
boolean isEqualsToMoney(int v) |
required: boolean type |
NotEquals | isNotEqualsTo<X> areNotEqualsTo<X> <X>isNotEqualsTo <X>areNotEqualsTo |
boolean moneyIsNotEqualsTo(int v) |
required: boolean type |
Name | Patterns | Examples | Return value |
---|---|---|---|
Add | (?:add<X>) (?:increment<X?>(?:By)?) |
void addMoney(int x) int incrementMoney(byte x) |
optional, returns previous value |
Subtract | (?:subtract(?:From)?<X>) (?:decrement<X?>(?:By)?) |
void decrementMoneyBy(int x) int subtractMoney(byte x) |
optional, returns previous value |
Multiple | (?:multiple|multi)<X?>(?:By)?) |
void multipleMoneyBy(double x) int multiMoney(byte x) |
optional, returns previous value |
Divide | (?:divide|div)<X?>(?:By)? |
void divMoneyBy(int x) int divideMoney(float x) |
optional, returns previous value |
Power | (?:power|pow)<X?>(?:By)? |
void powerMoney(int x) int powMoneyBy(double x) |
optional, returns previous value |
(both maps and lists)
Example properties used for examples in table:
List<? extends UUID> getIds();
Map<String, UUID> getApis();
Name | Patterns | Examples | Return value |
---|---|---|---|
GetFrom | getFrom<X> get<X?>By |
UUID getFromIds(int index) UUID getFromApisBy(String name) |
required: collection value type |
AddTo | (?:addTo|putIn)<X> |
boolean addToIds(UUID x) void addToIds(UUID... ids) UUID putInApis(String k, UUID v) void putInApis(Entry<String, UUID>...) |
optional, returns result of .add /.put operation |
RemoveFrom | removeFrom<X> |
boolean removeFromIds(UUID id) Collection<UUID> removeFromApis(String... keys) UUID removeFromApis(String key) |
optional, true/false for collection and removed value for maps |
SizeOf | (?:sizeOf)<X> <X?>(?:Size) |
int sizeOfIds() int apisSize() |
required: numeric type |
IsEmpty | (?:is)<X?>(?:Empty) |
boolen isIdsEmpty() boolean isApisEmpty() |
required: boolean type |
ContainsIn | (?:contains(?:Key?)(?:In?)|isIn)<X> (?:contains(?:In?)|isIn)<X> (?:contains|isIn)<X> |
boolen containsKeyInApis(String key) boolean isInApis(String... keys) boolean isInIds(UUID uuid) boolean containsIds(UUID... uuid) |
required: boolean type |
NotContainsIn | (?:(notContains|excludes)(?:Key?)(?:In?)|isNotIn)<X> (?:(notContains|excludes)(?:In?)|isNotIn)<X> (?:notContains|excludes|isNotIn)<X> |
boolen excludesKeyInApis(String key) boolean excludes(String... keys) boolean isNotInIds(UUID uuid) boolean notContainsIds(UUID... uuid) |
required: boolean type |
RemoveFromIf | removeFrom<X?>If remove<X?>If |
void removeApisIf(BiPredicate<UUID, String> test) boolean removeFromIdsIf(Predicate<UUID> test) |
optional: boolean type |
RemoveFromIfNot | removeFrom<X?>IfNot remove<X?>IfNot |
void removeFromApisIfNot(Predicate<Entry<UUID, String>> test) boolean removeIdsIfNot(Predicate<UUID> test) |
optional: boolean type |
Some classes can't be serialized by default (library is able to serialize anything that json/snakeyaml is able by default + deserialization of yaml is a bit enchanted to support even more types by default), then additional serializer needs to be registered.
To see examples of serializers just go to: diorite-configs-java8/org/diorite/config/serialization
- As string serialization: org/diorite/config/serialization/MetaValue
- Serialization of map wrapper: org/diorite/config/serialization/SomeProperties
- Serialization with selecting subtype based on configuration fields: org/diorite/config/serialization/AbstractEntityData + CreeperEntityData + SheepEntityData
- Registering serializers: SerializationTest#prepareSerialization
- Serializers can be also registered for existing types.
There is few annotations that can be used to control how given configuration option will be saved and/or accessed:
Name | Example | Meaning |
---|---|---|
Comment | @Comment("comment") int getX(); |
This comment will be added above given field in generated yaml# comment x: 5 This annotation can be also used inside non-config classes that are serialized to config values. |
Header | @Header({"line 1", "line 2"}}) public interface MyConfig |
Adds header to generated yaml |
Footer | @Footer({"line 1", "line 2"}}) public interface MyConfig |
Adds footer to generated yaml |
PredefinedComment | @PredefinedComment(path = {"some", "path"}, value = "comment") public interface MyConfig |
Adds comment on given path, useful for commenting fields of serialized classessome: # comment path: 5 |
CustomKey | @CustomKey("some-name") String getSomething(); |
Change name of field used inside generated yaml. some-name: value Note that this only change config key name, setter for this value will still use real name: void setSomething(String value) |
Unmodifiable | @Unmodifiable Collection<X> getX(); |
Collection returned by this getter is always unmodifiable |
BooleanFormat | @BooleanFormat(trueValues = {"o"}, falseValues = {"x"}) boolean getX(); |
Allows to use/support different text values as true/false values in config, while saving it will always use first value from list. x: o = x: true |
HexNumber | @HexNumber int getColor(); |
Value will be saved and loaded as hex value. color: 0xaabbcc |
PaddedNumber | @PaddedNumber(5) int getMoney() |
Used to save value with padding. money: 00030 |
Formatted | @Formatted("%.2f%n") double getX(); |
Uses java.util.Formatter to format value before saving (note that it might be impossible later to load it back, so it should be only used to choose number format like 0.00 ) for x = 12.544444 -> x: 12.54 |
Property | @Property("y") private int x() { return 0; } |
In java 9 it is used to define private properties, name of property is optional, if name is not present method name will be used instead. Note that this is name of property, so setter for this value will looks like: void setY(int y) CustomKey annotation can be still combined with this property In java 8 it still can be used on default methods to define properties with different name. |
PropertyType | @PropertyType(CharSequence.class) String getSomething(); |
Allows to select different serializer type for given property |
CollectionType | @CollectionType(CharSequence.class) List<? extends String> getSomething(); |
Allows to select different serializer type for given property collection type |
MapTypes | @MapTypes(keyType = CharSequence.class) Map<? extends String, Integer> getSomething(); |
Allows to select different serializer type for given property map type, note that you can choose if you want to provide type just for key, just for value or for both |
GroovyValidator | @GroovyValidator(isTrue = "x > 0", elseThrow = "y can't be negative") int getY() |
Allows to define validator using simple groovy expressions, note that you can place more than one annotation like that |
Validator | @Validator static double xValidator(double x) { return x > 100 ? 100 : 0 } |
Used to annotate validator methods, if name of method is <property>Validator then name of validator is optional, note that single validator method can be connected to more than one property @Validator({"x", "y"}) Read more in validators section. |
AsList | @AsList(keyProperty = "uuid") Map<UUID, SomeObject> getX() |
Allows to save map of values as list using given property as key when loading. x: - uuid: "uuid here" someField: value - uuid: "next uuid" someField: next value Also this annotation like MapTypes allows to select types of keys and values in map (optional) |
Mapped | @Mapped() Collection<SomeObject> getY() |
Allows to save collection as map, when using Mapped annotation it might be also required to also use ToStringMapperFunction annotation! |
ToStringMapperFunction | @ToStringMapperFunction("x.uuid") Collection<SomeObject> getY() OR @ToStringMapperFunction(property = "y") static String mapper(SomeObject x) { return x.getUuid(); } |
Allows to choose how to select key for each element of list serialized as map when using Mapped annotation. It cn be used over property to provide groovy script, or over method that should be invoked to provide that value. It can be also used on Map properties without AsList annotation to choose how key should be serialized and override default serialization code. |
ToKeyMapperFunction | @ToKeyMapperFunction("Utils.parse(x)") Map<Z, SomeObject> getY() OR @ToKeyMapperFunction(property = "y") static Z mapper(String x) { return Utils.parse(x); } |
Allows to choose how map key should be loaded, can be used to override default deserialization code |
HelperMethod | @HelperMethod default void someCustomMethod() { setX(0) } |
Used to mark additional methods that should not be implemented by diorite config system, annotation is optional in most cases |
Serializable | org/diorite/config/serialization/MetaObject | Used to mark class as serializable and mark serializer/deserializer methods in it. |
StringSerializable | org/diorite/config/serialization/MetaValue | Used to mark class as string serializable and mark string serializer/deserializer methods in it. |
DelegateSerializable | DelegateSerializable(Player.class) public class PlayerImpl implements Player |
Allow to register given class as serializable but using serializer of different type, so while registering class annotated with DelegateSerializable as StringSerializable system will look for StringSerializable annotations/methods/interface in provided class |
SerializableAs | SerializableAs(Player.class) public class PlayerImpl implements Player |
Allow to register given class as serializable but using different type, useful for separate implementation classes. |
There are 2 ways to create validator, using @GroovyValidator
as in table above, or using custom Validator
methods.
Each property can have multiple validators of each type.
By using custom methods you gain additional possibility of affecting property, as validator method can both throw some exception or change return value, like:
@Validator
default SomeType storageValidator(SomeType data)
{
if (data == null) {
return new SomeType();
}
if (data.something() > 100) {
throw new RuntimeException("Too big");
}
return data;
}
@Validator
can specify name of property that will be validated by it, or even validate more than one property: @Validator({"x", "y"})
.
If name of validator methods ends with Validator
then beginning of method name will be used as property name if annotation does not define own names.
Method that is used as validator must match one of this patterns and have @Validator annotation over it:
private T validateName(T name) {...}
default T validateName(T name) {...}
private void validateSomething(T something) {...}
default void validateSomething(T something) {...}
static void validateAge(T age) {...}
static T validateNickname(ConfigType config, T nickname) {...}
static T validateNickname(T nickname, ConfigType config) {...}
Using annotations to configure comments may look ugly in other classes than config itself, we can use the PredefinedComment
annotations to setup comments, but it might look horrible if we have a lot of fields to process.
This is why the diorite library provide multiple ways to provide own comment messages, all of them are based on special org.diorite.config.serialization.comments.DocumentComments
class.
One of the simplest thing to do, is just get an instance from a config template and add own comments:
ConfigTemplate<SomeConfig> configTemplate = ConfigManager.get().getConfigFile(SomeConfig.class);
DocumentComments comments = configTemplate.getComments();
comments.setComment("some.path", "Comment on path");
comments.setFooter("New footer!");
We can also fetch comments from some class to use them for other purposes: (like joining, described below)
DocumentComments someConfigComments = DocumentComments.parseFromAnnotations(SomeConfig.class);
But what if you have a list of elements?
listOfEntities:
- id: 4
name: Steve
- id: 43
name: Kate
special: true
Just... ignore it:
comments.setComment("people.name", "This comment will be above 'name' property of first entity in list.");
comments.setComment("people.little", "Moar comments");
And you will get:
people:
- id: 4
# This comment will be above 'name' property of first entity in list.
name: Steve
- id: 43
name: Kate
# Moar comments
little: true
(note that library does not duplicate comments in lists, and only place comment above first occurrence of given path.) If you want to use a map like this:
people:
'4':
# This comment will be above 'name' property of first entity in list.
name: Steve
'43':
name: Kate
# Moar comments
little: true
Just use a *
wildcard:
comments.setComment("people.*.name", "This comment will be above 'name' property of first entity in list.");
comments.setComment("people.*.little", "Moar comments");
But the best way to create comments are special yaml-like template files:
# Header of file, first space of comments is always skipped, if you want indent a comment just use more spaces.
# Like this.
#
#But first char don't need to be a space.
# This comment will appear over `node` path, yup, THIS one <-- this!
node: value is ignored
other:
# this comment will be ignored, as it isn't above any property.
# This comment will appear over `other.node` path
node:
# Map of people
peopleMap:
*:
# This comment will be above 'name' property of first entity in list.
name: Steve
# Moar comments
little: true
# List of people
peopleList:
# This comment will be above 'name' property of first entity in list.
name: Steve
# Moar comments
little: true
# Footer, just like header
It will just read the comments that are above all nodes and construct valid DocumentComments
for you:
DocumentComments comments = DocumentComments.parse(new File("mycomments")); // comments can be also loaded from InputStream or Reader
You can also combine multiple files or documents to one, so you can create a separate file for people comments and apply them on both peopleMap
and peopleList
:
DocumentComments comments = DocumentComments.create();
comments.setComment("some.path", "Comment on path");
comments.setFooter("New footer!");
DocumentComments peopleComments = DocumentComments.parse(new File("people-comments"));
comments.join("peopleMap.*", peopleComments);
// or use existing node
comments.join("peopleList", comments.getNode("peopleMap.*"));
// or read from some annotations
comments.join("some.node", DocumentComments.parseFromAnnotations(SomeConfig.class));
End.