/diorite-configs-java8

Clone of https://github.com/Diorite/Diorite/tree/master/diorite-utils/config but for java 8.

Primary LanguageJavaMIT LicenseMIT

Diorite Configs

For java 8!

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.

Installation

Maven:

<dependencies>
    <dependency>
        <groupId>com.gotofinal</groupId>
        <artifactId>diorite-configs-java8</artifactId>
        <version>1.4.1</version>
    </dependency>
</dependencies>
<repositories>
    <repository>
        <name>Diorite repository</name>
        <id>diorite</id>
        <url>https://repo.diorite.org/repository/diorite/</url>
    </repository>
</repositories>

Gradle:

repositories {
    maven {
        url 'https://repo.diorite.org/repository/diorite/'
    }
}

dependencies {
    compile group: 'com.gotofinal', name: 'diorite-configs-java8', version: '1.4.1'
}

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.

Usage

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?>)

For any type

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

For numeric values

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

For collections

(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

Implementing serialization

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

Features

List of annotations

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 classes
some:
  # 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.

Validators

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) {...}

Comment handling

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.