A small Java library to read and write INI files.
- Zero dependencies.
- Configurable to be usable with most variants of the format.
- Global sections.
- Nested sections.
- Line continuations
- Configurable delimiters.
- Configurable whitespace usage.
- Multiple modes for handling of duplicate properties and sections.
- Multiple modes for handling of multi-value keys
- Multiple modes for handling of escape characters
- Configurable case sensitivity.
- Configurable order preservation.
- JPMS compliant.
- Requires JDK 11 or above (JDK 17 for tests).
- Optional Preferences implementation.
- String interpolation with configurable variable pattern.
- Optional reflection based serialization and deserialization of objects.
- Tests (see above badge for current coverage)
Available on Maven Central, so just add the following dependency to your project's pom.xml
.
<dependency>
<groupId>com.sshtools</groupId>
<artifactId>jini-lib</artifactId>
<version>0.4.0-SNAPSHOT</version>
</dependency>
See badge above for version available on Maven Central. Snapshot versions are in the Sonatype OSS Snapshot Repository.
<repository>
<id>oss-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<snapshots />
<releases>
<enabled>false</enabled>
</releases>
</repository>
If you are using JPMS, add com.sshtools.jini
to your module-info.java
.
Using Apache Maven is recommended.
- Clone this module
- Change directory to where you cloned to
- Run
mvn package
- Jar Artifacts will be in the
target
directory.
The general pattern for reading an INI document is ..
- Create a configured
INIReader
viaINIReader.Builder
. - Get an
INI
instance usingreader.read(...)
. - Query the
INI
instance for sections, properties etc.
And for writing an INI document ..
- Either use an
INI
document you have obtained from anINIReader
, create a default document (order preserved, case insensitive keys) usingINI.create()
, or useINI.Builder()
to configure behaviour. - Create a configured
INIWriter
viaINIWriter.Builder
. - Write the instance to some target using
INIWriter.write(..)
.
var ini = INI.create();
ini.put("Name", "Alice");
ini.put("Age", 34);
ini.put("Registered", false);
var sec = ini.create("Address");
sec.put("Street", "15 Stone Lane");
sec.put("Area", "");
sec.put("City", "Arbington");
sec.put("County", "Inishire");
sec.put("PostCode", "ABC 123");
var wrt = new INIWriter.Builder().build();
try(var out = Files.newBufferedWriter(Paths.get("data.ini"))) {
wrt.write(ini, out);
}
var ini = INI.fromFile(Paths.get("data.ini"));
System.out.format("Name: %s%n". ini.get("Name"));
System.out.format("Age: %d%n". ini.getInt("Age"));
if(ini.getBoolean("Registered"))
System.out.println("Is registered%n");
ini.sectionOr("Address").ifPresent(s -> {
System.out.println("Address");
System.out.format(" Street: %s%n". s.get("Street"));
System.out.format(" Area: %s%n". s.get("Area"));
System.out.format(" City: %s%n". s.get("City"));
System.out.format(" County: %s%n". s.get("County"));
System.out.format(" PostCode: %s%n". s.get("PostCode"));
System.out.format(" Tel: %s%n". s.get("PostCode", "N/A"));
});
An optional java.util.prefs.Preferences implementation is available, which will replace the default when added to the CLASSPATH.
Instead of storing preferences in the registry, or XML files, or whatever your platform might use by default, all preferences would be stored in .ini
files in appropriate locations.
Just add the following dependency to your project's pom.xml
.
<dependency>
<groupId>com.sshtools</groupId>
<artifactId>jini-prefs</artifactId>
<version>0.4.0-SNAPSHOT</version>
</dependency>
The .ini
file backend will not be initialised until the first time you access a Preferences
node. Files will not be written until the first .put()
or .flush()
.
var prefs = Preferences.userRoot();
prefs.put("akey", "Some Value");
It is possible to configure INI based preferences if you do so before the first access to
a Preferences
node.
var bldr = new INIStoreBuilder().
withScope(Scope.USER).
withoutAutoFlush().
withName("jini.test").
withPath(Files.createTempDirectory("jinitestdir"));
var store = bldr.build();
INIPreferences.configure(store);
var prefs = Preferences.userRoot();
prefs.put("akey", "Some Value");
System.out.println(prefs.get("akey", "No value!"));
// ...
You can also use this builder make use of the Preferences
API without using the static methods in Preferences
such as systemRoot()
. You can create as many roots as you like (to different file paths), and make use of them in any manner you like.
var bldr = new INIStoreBuilder().
withScope(Scope.USER).
withoutAutoFlush().
withName("jini.test").
withPath(Files.createTempDirectory("jinitestdir"));
try (var store = bldr.build()) {
var prefs = store.root();
prefs.put("akey", "Some Value");
System.out.println(prefs.get("akey", "No value!"));
}
Note that a store
is scoped, and should be closed when finished with.
The optional jini-serialization
module adds support for writing an object graph as human-readable INI files, as well as reading such INI files and recreating said object graph.
Just add the following dependency to your project's pom.xml
.
<dependency>
<groupId>com.sshtools</groupId>
<artifactId>jini-serialization</artifactId>
<version>0.4.0-SNAPSHOT</version>
</dependency>
- Collections and Maps must provide an
itemType
for their values for de-serialization to work. - Maps currently only support
String
keys, and values can only be primitive types. - The entire object graph is serialized (subject to include / exclude rules). There is no support for object references yet.
Serialization and de-serialization works using Java's reflection feature. It will look for accessible fields that are not otherwise configured to be excluded, inspect their value and convert it an entry in a INI section with a key and a string value. If the value is a complex object, it may create further INI sections and inspect the object for it's values, and so on.
Sometimes fields may be not be immediately accessible, for example if they are private
. By default, Jini will try to make such fields accessible to reflection.
Modern Java has further restriction on accessing such fields, and it reflection may not be possible. In these cases, Jini will also look for accessor methods that can be reflected and provide the value. By default, it expects to use the JavaBean pattern, i.e. "getter" and "setter" methods.
The entry point to serializing an object can be via a (reusable) INISerializer
which itself cannot be constructed directly, but instead is created by INISerializer.Builder
. Once an INISerializer
is obtained, you then call srlzr.serialize(myObject)
which will return an INI
object that you mean then write as normal (e.g. using an INIWriter
).
Alternatively, you can use one of several convenience methods.
INISerializer.toINI(Object object)
creates anINI
from anObject
.INISerializer.write(Object object, String path)
andINISerializer.writer(Object object, Path path)
writes an object to a file.INISerializer.write(Object object, Writer writer)
writes the contents of theINI
to aWriter
.
If your object contains only primitives, String
and other primitive objects, ByteBuffer
, any other Object
that follows follows the same rules, or arrays of such objects or primitives, then no additional code will be to allow serialization to happen.
class Address {
String line1 = "99 Some Road";
String city = "Nodnol";
}
class Person {
String name = "Mr Crumbly";
int age = 99;
Address address = new Address();
String[] telephones = new String[] { "123 456789", "987 654321" };
}
INISerialize.write(new Person(), "crumbly.ini");
If your object contains any other Object
that does not fit this description, or a Collection
or Map
of any type, then you many need to add additional meta-data (such as itemType
to overcome type-erasure) to allow Jini to serialize the object.
This can be done by either adding annotations to the target object, or using the programmatic callback API provided by INISerializer.Builder
. This customisation also allows fields or methods to be excluded, have different key
s used in the INI file and other behavioural changes.
The entry point to de-serializing an object can be via a (reusable) INIDeserializer
which itself cannot be constructed directly, but instead is created by INIDeserializer.Builder
. Once an INIDeserializer
is obtained, you then call desrlzr.deserialize(ini, MyObject.class)
which will return an instance of type MyObject
constructed from the data in an INI
document that you obtained as normal (e.g. using an INIReader
).
Alternatively, you can use one of several convenience methods.
INIDeserializer.fromINI(INI ini, MyObject.class)
creates aMyObject
from anINI
.INIDeserializer.read(String path, MyObject.class)
andINISerializer.read(Path path, MyObject.class)
reads an object from a file.INISerializer.read(Reader reader, MyObject.class)
reader INI contents from aReader
and construct aMyObject
from it.
class Address {
String line1;
String city;
}
class Person {
String name;
int age;
Address address;
String[] telephones;
}
var person = INIDeserializer.read("crumbly.ini", Person.class);
The same rules apply as in serialization, i.e. when the type of an object cannot be derived any other way it must be provided programmatically or via an annotation. The same annotations used in serialization are also used for de-serialization.
De-serialization imposes some additional requirements and restrictions too.
- All types that have no special handling, must have empty public constructors.
- For
Collection
andMap
types, if you want it to be of a particular type of collection or map, you should make sure it is initialized in construction (i.e. an initialized field, or created in a default constructor). In this case it must be a modifiable. If the field isnull
when deserializing, and data for the collection is present, then an unmodifiable variant of the collections will be automatically created and filled.
- New
jini-serialization
module for object serialization and deserialization. SeeINISerializer
andINIDeserializer
. duplicateSectionAction
default is nowDuplicateAction.APPEND
. This means multiple sections with the same name by default will now all be available.- Multi-line string support for keys and values. Wrap strings in supported quote character, e.g
''' ..... '''
in a manner similar to Java.
- Fixes for reloading.
- Removal of value events not fired by
INISet
. - Removed schema types
FLOAT
,LOCATION
andCOLOR
. These are nowDiscriminator
enumeration instances. There are two discriminator implementations,TextDiscriminator
to be used forType.TEXT
andNumberDiscriminator
to be used forType.NUMBER
. The list of discriminators is now likely to grow instead of theType
.
- More work on
INISchema
, can now wrap anINI
to provide a proxied instance that guarantees correctness. - Created
jini-config
module that can be used to provide a monitored INI based configuration system for applications.
- Added string interpolation features.
- Renamed some getters to shorten them, and also remove an ambiguity with
getAllOr()
. - Separated values separator wasn't being ignored in quotes.
- Added
EscapeMode
forINIWriter
andINIReader
that allows altering behaviour of escaping. java.util.prefs.Preferences
implementation
- Added
Section.readOnly()
andDocument.readOnly()
to create a read-only facades. - Added
INI.empty()
that returns a static empty and read-only document. - Events are fired from all parent
Section
orINI
on value or child section change. - Added
obtainSetion()
that gets or creates sections given a path.
- Added
onValueUpdate()
andonSectionUpdate()
to listen for changes to document.
- Added
keys()
method. - Made
containsSection(String)
now support testing for nested sections by changing the signature tocontainsSection(String...)
.
Uses LinkedCaseInsensitiveMap from Spring Utilities.