a small Java library for loading
.jenvconfiguration files because apparently Java needed another way to deal with configuration (yes, i made one) ... and i'm totally not competing with.env(if i were, i'd be lowkey DEAD)
JEnv is a small Java library for loading and accessing configuration files written in the JENV format!
i made this mainly to practice working with:
- Java generics
MapPathand file I/O- functional interfaces
- sealed interfaces
- records
- custom exceptions
- parsing
- reusable library design
- Maven
- unit testing
- designing a small public API
- separating loading, configuration access, value handling, and error handling into their own components
- actually making a library that another Java program can use instead of just making another program that reads input and prints something, mhm!!!
- headaches (unwanted)
the main idea is pretty simple because:
.jenv file (fancy '.env')
↓
JEnvLoader (reads and parses the file)
↓
Map<String, String> (key-value pairs! for example: PORT=8080: PORT is the key, 8080 is the value)
↓
JEnv (represents the loaded configuration, woohoooooo!!!)
↓
JEnvValue<T> (here's a present for you!! OPEN IT.)
JEnvLoader reads and parses a JENV file. . .
JEnv provides the main configuration API. . .
JEnvValue<T> represents an individual configuration value and provides ways to safely access it or provide fallbacks. . .
there are three names here and I SWEAR they are not the same thing:
- JEnv — the name of the Java library/project
.jenv— the file extension- JENV — the configuration file format
so, for example:
development.jenv
is a .jenv file containing configuration written in the JENV format, which can be loaded using the JEnv library.
very important distinction because i absolutely would have confused myself later otherwise. how nice! (rhymes)
- loads
.jenvconfiguration files - supports
.jenvfile paths - simple
KEY=VALUEconfiguration syntax - ignores blank lines
- retrieves raw values as
String - converts values through user-provided parsers
JEnvValue<T>for handling present and missing valuesOption<T>for internal optional-value representation- fallback values through
or() - lazy fallback values through
orElse() - terminal access through
unwrap() - fallback access through
unwrapOr() - checks for keys with
contains() - custom JEnv exception hierarchy
- parse errors include line and column information
- reusable Java library API
- Maven-based build (yet again)
- JUnit test suite (kinda like
Python'sunittest, huh..) - does not require an application entry point because this is a library and not a program with a
main()method yes?
JENV currently uses a deliberately simple key-value format!.
a basic file looks like:
HOST=localhost
PORT=8080
DEBUG=true
each non-blank line contains:
KEY=VALUE
the first = separates the key from the value.
values can contain additional = characters because the parser only treats the first = as the separator:
URL=https://example.com?a=b
which becomes:
key: URL
value: https://example.com?a=b
blank lines are ignored:
HOST=localhost
PORT=8080
the main entry point for loading a configuration is:
JEnv env = JEnv.load(
Path.of("development.jenv")
);JEnv.load() creates a JEnvLoader, loads the specified file, and uses the resulting values to construct a JEnv instance.
the loader itself can also be used directly when the parsed map is what you want:
JEnvLoader loader = new JEnvLoader();
Map<String, String> values =
loader.load(Path.of("development.jenv"));this keeps file loading and configuration access separate instead of making JEnv responsible for parsing files itself..
given:
HOST=localhost
PORT=8080
DEBUG=true
you can retrieve a raw value with:
String host = env
.get("HOST")
.unwrap();or check whether a key exists:
if (env.contains("HOST")) {
// ...
}missing values are represented by an empty JEnvValue:
JEnvValue<String> value =
env.get("MISSING");which can then be handled without immediately throwing an exception.
JENV files contain strings, but configuration values often represent other types.
JEnv therefore supports a parser function:
int port = env
.get("PORT", Integer::parseInt)
.unwrap();and:
boolean debug = env
.get("DEBUG", Boolean::parseBoolean)
.unwrap();the parser is responsible for converting the raw string into the desired type.
epic, no?
conceptually:
"8080"
↓
Integer::parseInt
↓
8080
this means JEnv doesn't need a giant collection of methods like:
getInt()
getBoolean()
getDouble()
getLong()
getFloat()
...
because the caller can provide the conversion they need. (and.. other than conversion, too!)
JEnvValue<T> is the main wrapper returned by JEnv.get().
instead of immediately returning null for a missing key, JEnv represents the result explicitly:
JEnvValue<T>
├── present
└── empty
this allows operations to be chained.
for example:
String host = env
.get("HOST")
.or("localhost")
.unwrap();if HOST exists, its existing value is used.
if it doesn't exist, "localhost" is used instead.
(i did this because i have a burning love for Rust)
orElse() accepts a Supplier<T>:
String host = env
.get("HOST")
.orElse(() -> "localhost")
.unwrap();this is useful when producing the fallback value requires some computation.
unwrap() retrieves the contained value:
String host = env
.get("HOST")
.unwrap();do take note that, um, this will unwrap a bomb (not all presents contain joy and glee). okay, actually, the bomb is an exception.. it DOES technically bomb the program by ending it.. so........ do something about it!!
ANYWAYS. while unwrapOr() provides a fallback at the point where the value is finally retrieved:
String host = env
.get("HOST")
.unwrapOr("localhost");the difference is intentional:
or() / orElse()
↓
JEnvValue<T>
↓
unwrap() / unwrapOr()
↓
T
the first group continues working with JEnvValue.
the second group terminates the chain and gives you the actual value.
JEnv also contains a small Option<T> abstraction for representing whether a value exists. (indeed, i am an Option<Person>. you may unwrap() me!)
it has two variants:
Option<T>
├── Some<T>
└── None<T>
for example:
Option<String> present =
Option.some("hello");
Option<String> missing =
Option.none();Some cannot contain null, while None represents the absence of a value... none.. SELF-EXPLANATORY, ain't it MATE??? ok no i'm not British goodbye
back in track! Option is primarily an internal implementation detail of JEnv's value handling rather than the main public API users need to interact with.
JEnv has its own exception hierarchy:
RuntimeException
└── JEnvException
└── JEnvParseException
JEnvException is the base exception for JEnv-specific errors
JEnvParseException is used when a JENV file contains invalid syntax
for example, given that:
HOST=localhost
INVALID_LINE_HAHA_UWU_NOTICE_ME_SENPAI
PORT=8080
the loader can report where the problem occurred:
Error at 2:13: Line is missing the '=' character.
the exception also exposes the location:
catch (JEnvParseException e) {
int line = e.getLine();
int column = e.getColumn();
/*
> just imagine that the code below this comment prints
> the line, column, and error message.. yay!
*/
}parse exceptions are not swallowed by the loader. they propagate to the caller so the application using JEnv can decide how to handle them. this is so giving ?!! (Rust reference... REFERENCE??? &&&&&&&&&&&&&&&&&&a)
a complete example might look like this.
development.jenv:
HOST=localhost
PORT=8080
DEBUG=true
Java:
import java.io.IOException;
import java.nio.file.Path;
import com.jay.jenv.JEnv;
public class Main {
public static void main(String[] args) throws IOException { // *throws trantrum*, teehee!~ JUST KIDDING OMG
JEnv env = JEnv.load(
Path.of("development.jenv")
);
String host = env
.get("HOST")
.unwrap();
int port = env
.get("PORT", Integer::parseInt)
.unwrapOr(8080);
boolean debug = env
.get("DEBUG", Boolean::parseBoolean)
.unwrapOr(false);
System.out.println(host);
System.out.println(port);
System.out.println(debug);
}
}which gives:
localhost
8080
true
ooo first time doing this and, uh, i had to get these line symbols everywhere
jenv/
├── src/
│ ├── main/
│ │ └── java/
│ │ └── com/
│ │ └── jay/
│ │ └── jenv/
│ │ ├── JEnv.java
│ │ ├── JEnvValue.java
│ │ │
│ │ ├── option/
│ │ │ └── Option.java
│ │ │
│ │ ├── loader/
│ │ │ └── JEnvLoader.java
│ │ │
│ │ └── exception/
│ │ ├── JEnvException.java
│ │ └── JEnvParseException.java
│ │
│ └── test/
│ └── java/
│ └── com/
│ └── jay/
│ └── jenv/
│ ├── JEnvTest.java
│ ├── JEnvValueTest.java
│ │
│ ├── option/
│ │ └── OptionTest.java
│ │
│ └── loader/
│ └── JEnvLoaderTest.java
│
├── .gitignore
├── LICENSE
├── README.md
└── pom.xml
the project is intentionally split into small components instead of putting loading, parsing, value handling, and configuration access into one enormous class. tada!!
- Java 26
- Maven 3.9 or newer or whatever.. just, um, use a compatible Maven version?!?
- a Java compiler compatible with the configured Java release. compatible compatible this that these those bla bla bla
clone the repository:
git clone https://github.com/jayywashere/jenv
cd jenvrun the test suite:
mvn testclean the project:
mvn cleanbuild the library:
mvn clean packagethe compiled JAR will be placed in:
target/
you can also install the library into your local Maven repository:
mvn installthis is useful when testing JEnv from another local Maven project before publishing it publicly. wwwwwwwwww
JEnv uses JUnit for unit testing.!?
the test suite covers the main pieces of the library:
JEnv
├── value retrieval
├── typed value parsing
├── key existence
└── loading
JEnvValue
├── present values
├── empty values
├── fallbacks
└── unwrapping
Option
├── Some
├── None
└── fallback behavior
JEnvLoader
├── valid files
├── blank lines
├── malformed lines
├── invalid extensions
└── parse error locations
run all tests with:
mvn testthis started as my final Java project for my little five-language challenge thing.
i wanted the Java project to be something that felt different from my usual small programs, so instead of making another CLI with:
read input
do something
print result
NOT THIS AGAIN.. how about:
read input
do something crazy
print result
oh... well, whatever... so, um:
i decided to make an actual reusable library.
i also wanted to practice Java features that are much easier to ignore when making tiny programs:
- generics
- functional interfaces
- sealed interfaces
- records
- custom exceptions
- file I/O
- Maven
- unit testing
- API design
- package structure
the JENV format itself is intentionally simple. i didn't want to spend 400 years inventing a configuration language with 700 features when the interesting part for me was designing the Java library around it.
there are already plenty of configuration solutions for Java.
i mostly wanted to make my own small one, understand every part of it, and have something reusable at the end.
and now apparently i have a tiny configuration format, a loader, an option abstraction, an exception hierarchy, a value wrapper, tests, and a Maven project. wowowow
uhhh.
okay.
vye i mean bye
See LICENSE.