Context
Context is a zero-dependency Java library that provides a combinator framework for building extractors, that can read values from a context, and injectors, that can write values into a context.
Install / Use
/learn @typemeta/ContextREADME
<img src="https://img.shields.io/maven-central/v/org.typemeta/context.svg"/>
Context is a zero-dependency Java library that provides a combinator framework for building extractors, that can read values from a context, and injectors, that can write values into a context.
A context is anything that acts as either a source of values (for extractors)
or as a target for values (for injectors).
Examples of contexts include Java Properties objects, JDBC ResultSet objects (for database extractors),
and JDBC PreparedStatement objects (for database injectors).
The library provides implementations of extractors and injectors for the above context types, but can can also support any type of context.
Getting Started
Requirements
Requires Java 1.8 or higher.
Resources
- Release builds are available on the Releases page.
- Maven artifacts are available on the Sonatype Nexus repository
- Javadocs are for the latest build are on javadocs.io page.
Maven
Add this dependency to your project pom.xml:
<dependency>
<groupId>org.typemeta</groupId>
<artifactId>context-core</artifactId>
<version>${context-core.version}</version>
</dependency>
(and define context-core.version accordingly)
Example
Consider a simple class consisting of several member fields:
static class Config {
final LocalDate endDate;
final OptionalInt numThreads;
final String env;
// Usual constructor, member retrieval methods, and toString.
}
along with a Config instance and a Properties instance:
final Config config = new Config(
LocalDate.of(2021, 04, 19),
OptionalInt.of(24),
"DEV"
);
final Properties props = new Properties();
If we want to write a Config object to the properties,
we would have to write each field individually:
props.setProperty("endDate", before.endDate().toString());
before.numThreads().ifPresent(n -> props.setProperty("numThreads", Integer.toString(n)));
props.setProperty("env", before.env());
Likewise, to read the config back out we have to read the individual fields
and then construct the Config object:
final String numThreads = props.getProperty("numThreads");
final Config after = new Config(
LocalDate.parse(props.getProperty("endDate")),
numThreads == null ? OptionalInt.empty() : OptionalInt.of(Integer.parseInt(numThreads)),
props.getProperty("env")
);
Using context, we can define an injector, which will inject or write a Config object into a Properties object:
final Injector<Properties, Config> setConfigProp =
Injectors.combine(
PropertiesInjectors.LOCALDATE.bind("endDate").premap(Config::endDate),
PropertiesInjectors.OPT_INTEGER.bind("numThreads").premap(Config::numThreads),
PropertiesInjectors.STRING.bind("env").premap(Config::env)
);
and an extractor, which will extract a Config object from a Properties object:
final Extractor<Properties, Config> getConfigProp =
Extractors.combine(
PropertiesExtractors.LOCALDATE.bind("endDate"),
PropertiesExtractors.OPT_INTEGER.bind("numThreads"),
PropertiesExtractors.STRING.bind("env"),
Config::new
);
and use them like this:
// Write the config data to the props.
setConfigProp.inject(props, config);
// Read the config back out.
final Config config2 = getConfigProp.extract(props);
User Guide
Combinators are an approach to organising libraries by providing a set of primitive constructs, along with a set of functions that can combine existing constructs to form new ones. Context provides two sets of combinators - extractors and injectors.
Extractors
An Extractor extracts a value from a context.
The primary interface is Extractor, which has one abstract method:
@FunctionalInterface
public interface Extractor<CTX, T> {
T extract(CTX ctx);
// ...
}
I.e. an extractor is a function that takes a context and returns a value extracted from the context.
Contexts can be any type that supports the retrieval of values.
The simplest context type we can imagine is the Java Optional type,
which either holds a value or doesn't.
So using Optional as an example context type, we can define an extractor to extract a String value from an Optional<String> context:
Extractor<Optional<String>, String> optGet = Optional::get;
and use it to extract a value from a context:
final Optional<String> optStr = Optional.of("test");
final String s = optGet.extract(optStr);
assert(s.equals("test"));
We can convert this extractor into one that extracts a value of a different type by mapping a function over it:
final Extractor<Optional<String>, Integer> optLen = optGet.map(String::length);
final int len = optLen.extract(optStr);
assert(len == 4);
ExtractorByName
Extractors can be built for more interesting types of context,
such as the Java Properties class.
However, unlike Optional,
in order to be able to extract a value from a Properties object,
a property key is required.
Therefore we need a slightly different type of extractor,
one that adds an extra string parameter to the extract method:
@FunctionalInterface
public interface ExtractorByName<CTX, T> {
T extract(CTX ctx, String name);
// ...
}
We can construct an instance of this type of extractor as before:
final ExtractorByName<Properties, String> getProp = Properties::getProperty;
and can use it by calling the extract method with a Properties object and a key name:
final String javaVer = getPropVal.extract(System.getProperties(), "java.version");
Alternatively, since we typically know the property name in advance,
we can bind this ExtractorByName to a name,
which then gives us a standard Extractor:
final Extractor<Properties, String> getJavaVer = getPropVal.bind("java.version");
final String javaVer = getJavaVer.extract(System.getProperties());
System.out.println(javaVer);
ExtractorByIndex
An ExtractorByIndex is similar to ExtractorByName,
but where the extract method expects an integer index instead of a name.
@FunctionalInterface
public interface ExtractorByIndex<CTX, T> {
T extract(CTX ctx, int index);
// ...
}
As before, an ExtractorByIndex can be bound to an integer value, to create a standard extractor.
Checked Extractors
At first glance, the JDBC ResultSet class seems like a suitable candidate for converting into an extractor,
using a method reference to infer the extractor value.
However, the ResultSet get methods (e.g. ResultSet.getBoolean) all throw a SQLException in their signature.
This prevents us from creating an extractor directly:
// Fails to compile....
final ExtractorByName<ResultSet, Boolean> BOOLEAN = ResultSet::getBoolean;
To address this, each extractor interface contains an inner interface named Checked, e.g. Extractor.Checked.
Each Checked interface is similar to its outer interface, with one difference - the extract method throws an exception.
The exact type of exception is specified as a type argument to the interface:
@FunctionalInterface
interface Checked<CTX, T, EX extends Exception> {
T extract(CTX ctx) throws EX;
// ...
}
These interfaces can then be used to construct extractors from methods that throw an exception:
final ExtractorByName.Checked<ResultSet, Boolean, SQLException> BOOLEAN = ResultSet::getBoolean;
We can convert a Checked extractor instance to an unchecked one by calling Checked.unchecked:
final ExtractorByName<ResultSet, Boolean> BOOLEAN2 = BOOLEAN.unchecked();
This gives us an extractor that will catch the checked exception and rethrow as a RuntimeException.
Specialisations
The generic type parameter T in the extractor interfaces specifies the type of the extracted value.
Currently Java generic types do not support primitive types (byte, int, etc) directly,
which means their boxed equivalents (Byte, Integer, ...) must be used.
This introduces the possibility of null values, as well as a slight performance overhead.
If the value being extracted can never be null then there exists specialised equivalents of the extractor interfaces,
which can be used instead:
| Base Type | Double Specialisation | Integer Specialisation | Long Specialisation |
|---|---|---|---|
| Extractor | DoubleExtractor | IntegerExtractor | LongExtractor |
| ExtractorByName | DoubleExtractorByName | IntegerExtractorByName | LongExtractorByName |
| ExtractorByIndex | DoubleExtractorByIndex | IntegerExtractorByIndex | LongExtractorByIndex |
Each specialised class provides an alternative extract method that supports extracting the primitive type:
public interface DoubleExtractor<CTX> extends Extractor<CTX, Double> {
double extractDouble(CTX ctx);
@Override
default Double extract(CTX ctx) {
return extractDouble(ctx);
}
// ...
}
Constructors
There are various ways to construct an extractor. The first and most common is to construct one via a lambda or method reference:
final Extractor<Optional<String>, String> optGet = Optional::get;
Each extractor type also has a static of constructor method (e.g. Extractor.of)
that can be used where a lambda or method reference can't be used directly:
// Fails to compile.
Related Skills
node-connect
343.3kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
92.1kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
343.3kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
343.3kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
