📜 ⬆️ ⬇️

Reading Java configuration files: nProperty

image
Many developers are faced with the need to read configuration (* .ini, * .prop, * .conf, etc.) files in developed applications. In Java, there is a standard class Properties, with which you can very easily load an ini-file and read its properties. With a large amount of configuration files, reading and writing settings into objects turns into a very tedious and routine work: to create a Properties object, convert each setting to the required format and write it to the field.

The nProperty ( A n notated Property ) library is designed to simplify this process by reducing the required code to write the configuration loaders about two times.

To show how the promised reduction of the code is possible in half, two examples are given below: in the first example, the standard Properties class is used, and secondly, nProperty.
')

The article and the nProperty library itself was written by my friend and fellow in the Yorie shop for intra-team daily needs, and since he, unfortunately, does not currently have an invite for Habré, I took the liberty to publish this creation for “ Habrovsky "masses.

Content


  1. Just the main thing
  2. Reading primitive and standard types
  3. Deserialization into arrays and collections
  4. Deserialization into custom types
  5. Access Level Modifiers
  6. Initialize all class members
  7. Default values
  8. Name override
  9. Working with non-static class fields
  10. Use of methods
  11. Event handling
  12. Using streams and file descriptors
  13. Remarks
  14. License
  15. Links



Just the main thing


In both examples, the same configuration file will be used:
SOME_INT_VALUE = 2 SOME_DOUBLE_VALUE = 1.2 SOME_STRING_VALUE = foo SOME_INT_ARRAY = 1;2;3 


Example number 1. Loading configuration using the standard Properties class.
 public class Example1 { private static int SOME_INT_VALUE = 1; private static String SOME_STRING_VALUE; private static int[] SOME_INT_ARRAY; private static double SOME_DOUBLE_VALUE; public Example1() throws IOException { Properties props = new Properties(); props.load(new FileInputStream(new File("config/example.ini"))); SOME_INT_VALUE = Integer.valueOf(props.getProperty("SOME_INT_VALUE", "1")); SOME_STRING_VALUE = props.getProperty("SOME_STRING_VALUE"); SOME_DOUBLE_VALUE = Double.valueOf(props.getProperty("SOME_DOUBLE_VALUE", "1.0")); // ,           String[] parts = props.getProperty("SOME_INT_ARRAY").split(";"); SOME_INT_ARRAY = new int[parts.length]; for (int i = 0; i < parts.length; ++i) { SOME_INT_ARRAY[i] = Integer.valueOf(parts[i]); } } public static void main(String[] args) throws IOException { new Example1(); } } 


Example number 2. Downloading a configuration using nProperty.
 @Cfg public class Example2 { private static int SOME_INT_VALUE = 1; private static String SOME_STRING_VALUE; private static int[] SOME_INT_ARRAY; private static double SOME_DOUBLE_VALUE; public Example2() throws NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, InvocationTargetException { ConfigParser.parse(Example2.class, "config/example.ini"); } public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IOException, IllegalAccessException { new Example2(); } } 

Perhaps, from these beautiful examples it follows the fact that the code can be reduced even more than twice :) These examples do not cover the topic of the presence in the fields of classes of variables that are not related to configuration files, as well as a few more subtle nuances. But first things first.


Reading primitive and standard types


In the second example above, one should pay attention to the @ fg annotation. It is the cause of the reduced code. The nProperty library is based on annotations that can be applied to classes, fields, and class methods.

To read the settings from the configuration file, the type of which is primitive, it is enough to designate each field of the class with the @ fg annotation:

 public class Example3 { @Cfg private static int SOME_INT_VALUE; @Cfg private static short SOME_SHORT_VALUE; @Cfg private static long SOME_LONG_VALUE; @Cfg private static Double SOME_DOUBLE_VALUE; /* ... */ } 

The nProperty library supports a fairly rich set of standard types:

All of these listed types can be used in the example above.


Deserialization into arrays and collections


In addition to standard types, it is also possible to deserialize into arrays with one condition — the type of the array must belong to the set of standard types:

 /*     : SOME_INT_ARRAY = 1--2--3 SOME_SHORT_ARRAY = 3>2<1 SOME_BIGINTEGER_ARRAY = 1;2;3 */ public class Example5 { @Cfg(splitter = "--") private static int[] SOME_INT_ARRAY; @Cfg(splitter = "[><]") private static short[] SOME_SHORT_ARRAY; @Cfg private static BigInteger[] SOME_BIGINTEGER_ARRAY; } 

In the case of arrays, the library will take care of initializing the array of the required size.

Note the annotations for SOME_INT_ARRAY and SOME_SHORT_ARRAY. By default, nProperty uses the character ";" as a delimiter. You can easily override it by specifying the splitter property in the annotation to the field. And, as you can see, the separator can be a full-fledged regular expression.

In addition to arrays, it is possible to use collections, namely lists. Here one condition is necessary - the collection must be necessarily initialized before starting the configuration reading. This is due to the fact that instances of collection objects can be different (ArrayList, LinkedList, etc.):

 public class Example6 { @Cfg private static List<Integer> SOME_ARRAYLIST_COLLECTION = new ArrayList<>(); @Cfg private static List<Integer> SOME_LINKEDLIST_COLLECTION = new LinkedList<>(); } 

For the rest, all array deserialization properties are saved for collections.


Deserialization into custom types


As an additional function, the library can work with custom classes. The user type must have a constructor: MyClass (String), otherwise an exception will be thrown. The level of visibility of the constructor does not matter, it can be both public and private:

 public class Example8 { private static class T { private final String value; private T(String value) { this.value = value; } public String getValue() { return value; } } @Cfg private static T CUSTOM_CLASS_VALUE; } 

As you can see, the library doesn’t care that the constructor is indicated by the private modifier. As a result, the value from the configuration file will be recorded in the value field of class T.


Access Level Modifiers


It is worth noting that the nProperty library absolutely does not care what access modifiers a field, method or constructor has - the library works through the Reflections mechanism and controls these modifiers independently. Of course, interference with modifiers will not affect other parts of the application to which the library has nothing to do.


Initialize all class members


In the previous examples it is clear that with a large number of fields in the configuration, you will have to write a large number of annotations @ fg. To avoid this routine work, nProperty allows you to add annotation to the class itself, thereby marking all the fields of the class as potential fields for recording settings from the configuration file:

 @Cfg public class Example7 { /*           */ private static int SOME_INT_VALUE = 1; private static String SOME_STRING_VALUE; private static int[] SOME_INT_ARRAY; private static double SOME_DOUBLE_VALUE; private static List<Integer> SOME_ARRAYLIST_COLLECTION = new ArrayList<>(); private static List<Integer> SOME_LINKEDLIST_COLLECTION = new LinkedList<>(); @Cfg(ignore = true) private final static Logger log = Logger.getAnonymousLogger(); } 

Here you should pay attention to a member of the class log. An annotation @ fg is assigned to it with the ignore property enabled. This property means that this field will not be used by the library when reading the configuration, but will simply be skipped. This property should be used only when the annotation affects the whole class, as shown in the example above.


Default values


One of the remarkable properties of the library is that if the property is not in the configuration file, the class field will never be changed. This makes it easy to set default values ​​right in the class field declaration:

 /*      WRONG_PROPERTY */ @Cfg public class Example9 { private int WRONG_PROPERTY = 9000; private int SOME_INT_VALUE; } 

In this case, after parsing the configuration, the same value of 9000 will be stored in the WRONG_PROPERTY field.


Name override


In cases when the class field name does not match the configuration name in the configuration file, you can forcefully override it:

 public class Example10 { @Cfg("SOME_INT_VALUE") private int myIntValue; } 

Naturally, if it is possible to preserve the equivalence of names in the code and in the configuration files, then it is better to do so — this will eliminate the need to annotate each field of the class.


Working with non-static class fields


The library is able to work with both classes and their instances. This is determined by various calls to the ConfigParser.parse () method:

 @Cfg public class Example11 { private static int SOME_SHORT_VALUE; private int SOME_INT_VALUE; public static void main(String[] args) throws NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, InvocationTargetException { ConfigParser.parse(Example11.class, "config/example.ini"); //        SOME_INT_VALUE ConfigParser.parse(new Example11(), "config/example.ini"); } } 

As you can see, in the example two different calls of the same method are used. After working out the ConfigParser.parse method (Example11.class, “config / example.ini”) in SOME_INT_VALUE, it will be zero, and it is completely independent of the configuration file, because this field is not static and cannot be used without an object instance.

Immediately after the second ConfigParser.parse call (new Example11 (), "config / example.ini"), the SOME_INT_VALUE field for the created object will take the value in accordance with the contents of the configuration file.

You should carefully use this library feature, as there may be situations where the configuration will not be loaded for an “incomprehensible” reason, and in fact it turns out that the static modifier was simply not set.


Use of methods


Let's imagine that while reading a certain property from the configuration file, it is necessary to check its contents, or, for example, deserialize the contents in a special way.

There are three solutions in these situations:
  1. independently check or change the value after the library analyzes the settings file and fills in all the fields of the class
  2. create a class wrapper with a constructor as a type (as shown above)
  3. exclude class field from property list and assign to method


The most convenient and correct way is â„–3. The nProperty library allows you to work not only with fields, but also with methods:
 public class Example12 { private static List<Integer> VALUE_CHECK = new ArrayList<>(); @Cfg("SOME_INT_ARRAY") private void checkIntArray(String value) { String[] values = value.split("--"); for (String val : values) { try { /*    [0,100] */ VALUE_CHECK.add(Math.max(0, Math.min(100, Integer.parseInt(val)))); } catch (Exception ignored) {} } } } 

Here, the SOME_INT_ARRAY value from the configuration file will be passed as the first parameter to the checkIntArray (String) method. This is a very convenient mechanism for cases when standard library solutions are not suitable. In the handler method, you can do anything.

However, it is worth noting that in the case of working with methods, the library does not use a separator mechanism, that is, at the moment it is impossible to organize automatic splitting of a property into an array.

Type conversion is still supported if the type of the first parameter of the method is different from String.

As with the class fields, if the name of the method is equivalent to the name of the setting in the configuration file, then you can omit the setting of the name in the annotation.


Event handling


The nProperty library allows you to handle some events while reading a configuration. In order to implement event handling, it is necessary to implement the IPropertyListener interface and all its methods. Calling events is possible only in the case of working with full-fledged objects, instances of a class that implements the IPropertyListener interface.

Supported Events:


 @Cfg public class Example13 implements IPropertyListener { public int SOME_INT_VALUE; public int SOME_MISSED_VALUE; public int SOME_INT_ARRAY; @Override public void onStart(String path) { } @Override public void onPropertyMiss(String name) { } @Override public void onDone(String path) { } @Override public void onInvalidPropertyCast(String name, String value) { } public static void main(String[] args) throws NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, InvocationTargetException { ConfigParser.parse(new Example13(), "config/example.ini"); } } 

In the example above, all 4 events will be triggered. The onPropertyMiss event will be triggered due to the SOME_MISSED_VALUE field that is not in the configuration file. The onInvalidPropertyCast event will be triggered due to the wrong type of the SOME_INT_ARRAY field.


Using streams and file descriptors


The library is able to accept not only file names as input, it is also possible to transfer a java.io.File object, or a data stream derived from the abstract java.io.InputStream class:

 @Cfg public class Example14 { public int SOME_INT_VALUE; public int SOME_MISSED_VALUE; public int SOME_INT_ARRAY; public static void main(String[] args) throws NoSuchMethodException, InstantiationException, IllegalAccessException, IOException, InvocationTargetException { ConfigParser.parse(new Example14(), "config/example.ini"); ConfigParser.parse(new Example14(), new File("config", "example.ini")); ConfigParser.parse(new Example14(), new FileInputStream("config/example.ini"), "config/example.ini"); } } 

As you can see, in the above example, when working with a stream, the library requires that the configuration name be additionally specified, since it is impossible to get it from the low-level FileInputStream object. The name is not an important part and will be used by the library to display information (including when working with events).

Thus, data can be obtained not only from the file system, but also from any data source operating according to Java standards. The ability to work with java.io.InputStream allows the library to be successfully applied in Android operating systems:
 @Cfg public class ConfigGeneral extends PropertyListenerImpl { public static String SERVER_IP; public static int SERVER_PORT; private ConfigGeneral() { String path = "config/network/general.ini"; try { InputStream input = getApplicationContext().getResources().getAssets().open(path); ConfigParser.parse(this, input, path); } catch(Exception e) { Log.e(TAG, "Failed to Load " + path + " File.", e); } } public static void loadConfig() { new ConfigGeneral(); } } 



Remarks


In connection with the SecurityManager’s not very transparent operation, the library has a restriction on the type of configurator field to be set: the field should not have a final modifier.


License


The library is distributed under the Apache License v.2.0


Links


Source: https://habr.com/ru/post/194658/


All Articles