Developer Documentation v1.0.0
Everything you need to know about embedding Nuts Application Framework in your application.
1 Nuts Application Framework
NAF: The Internals of Nuts, Exposed
Nuts is distributed as a single JAR with zero dependencies. To achieve that, it had to re-implement the basics: a portable file system (NPath), a structured console (NOut/NErr/NMsg), a CLI parser that also does completion (NCmdLine), a config model that preserves comments (NElement/TSON), an expression engine (NExpr), and an XDG-compliant workspace layout.
NAF (Nuts Application Framework) is not a separate framework. It is those internals, exposed.
We didn't build a framework to build a package manager. We built a package manager that needed to be zero-dep, portable, and embeddable, and the framework is the trace it left behind - the pheromone.
This means:
- Every API in NAF is used in production by Nuts itself to install, run, and update itself.
- Every API is zero-dep, OS-portable (Linux, macOS, Windows, tested), and designed for embedding.
- You don't have to adopt the whole stack. Use only NPath or only NCmdLine - they are decoupled.
NAF is already powering 10+ production apps (including state-level deployments) and 60+ OSS tools built on net.thevpc.nuts.toolbox. If you are building CLI tools, DevOps automation, launchers, or plugin-based Java apps and are tired of gluing Picocli + Jackson + Commons-IO + HttpClient, NAF gives you one coherent, already-dogfooded runtime.
nuts as a Framework (or Nuts Application Framework, or even simpler : NAF):
- Adds support for Application Lifecycle (Hooks for install, update, uninstall)
- Adds support for auto update
- Adds support for isolated input/output (via session in/out)
- Adds support for Desktop Integration
- Adds Shortcuts, Menus
- Adds Aliases
- Adds support for Base Directory API
- API to manage per application directories (log, cache, config,...)
- Adds support for Base Commandline API
- standardized commandline options
- inherit common options (--table, --json, ...)
Dual Role by Design
At its core, Nuts plays two essential roles:
- As a package manager, Nuts handles runtime dependency resolution, dynamic execution of artifacts, and integration with Maven-compatible repositories.
- As a framework, Nuts offers a rich set of APIs to handle I/O, configuration, text formatting, command execution, file systems, compressed archives, and more — helping developers write cleaner, more powerful, and portable applications.
Build, Embed, Automate
Nuts is designed to work in multiple modes of operation:
- Run Java programs and tools directly from repositories without downloading or installing them manually.
- Integrate it as a library to give your own application full access to the Nuts runtime capabilities.
- Use it as a DevOps engine to build repeatable scripts, installers, or deployment workflows.
- Use Nuts as a command-line tool to install, run, or deploy applications.
- Build your own ecosystem of versioned components, launchers, and tools powered by Nuts' modular runtime.
In short, Nuts is a full-stack developer toolbox — part package manager, part framework, part scripting engine — designed to make modern Java development more dynamic, composable, and automation-friendly.
Whether you are:
- Building CLI tools that need versioned plugins and extensible command handling,
- Developing DevOps utilities with integrated workspace/session management,
- Creating modular applications that dynamically load artifacts at runtime,
- Writing cross-environment installers, launchers, or monitoring tools,
- Or designing educational platforms, scripting DSLs, or JVM-based OS abstractions,
Nuts delivers the foundational infrastructure so you can focus on features, not plumbing. Key Capabilities
The NAF framework offers a wide set of features: ✅ Package & Dependency Management
- Compatible with Maven repositories and standards
- Supports local, remote, and custom repositories
- Enables runtime dependency resolution and dynamic artifact loading
✅ Configurable Workspaces & Sessions
- Isolated and shared workspace models with full lifecycle control
- Sessions encapsulate runtime configuration, user preferences, I/O handling, logging, and output styles
- Built-in support for dry-run, trace, confirmation, GUI/headless modes
✅ Structured I/O & Logging
- Supports semantic console output (NTF format) with colors, styles, and structured messages
- Unified API for stdout/stderr, input, logging, formatting, and piping
- Handles both human-friendly and machine-readable outputs (e.g., JSON, XML, Props, Tree)
✅ Filesystem & Networking Utilities
- Provides advanced file abstraction via NPath
- Built-in support for streaming, compressing, uncompressing, digesting, and manipulating file trees
- Unified access to HTTP, classpath, resources, and virtual filesystems
✅ Extensibility & Integration
- Supports modular extension points: listeners, commands, install hooks, and repositories
- Embedded scripting with support for Java source snippets and other languages
- Can be integrated into Spring, JavaFX, Swing, or any plain Java project
✅ Developer & DevOps Friendly
- Features like NExec, Nsh with ssh support make it ideal for automation
- Cross-platform support: Linux, macOS, Windows
- Zero external runtime dependencies — deploy as a bare JAR, with automatic resolution on first use
Why Use NAF?
Unlike traditional libraries or shell utilities, NAF brings together the flexibility of a modern scripting environment, the structure of a dependency-aware runtime, and the developer convenience of a polished CLI framework — all in one embeddable package. You can think of it as your:
- Maven-like package manager, but runtime aware
- Command-line framework, but fully pluggable
- Launcher platform, but without installation or configuration hurdles
- Shell scripting toolkit, but type-safe and Java-native
With NAF, the boundary between development and runtime fades away, letting you write, deploy, and evolve tools without sacrificing portability, maintainability, or developer experience.
Lightweight, Modular, and Composable
- Modular architecture built around workspaces, sessions, and execution contexts.
- Supports multi-repository, multi-version, and multi-runtime setups.
- Helps maintain separation between application logic and environment/runtime logic — a key feature for scripting, testing, and dynamic execution.
Not Just Tools — Ecosystems
NAF encourages composability: it enables you to define reusable, versioned components that can evolve independently and work across environments. It brings a new level of reusability to the Java ecosystem, bridging the gap between packaged applications, scripts, and shared libraries. Whether you're building a command-line tool, a plugin system, a runtime launcher, or a portable enterprise toolkit — Nuts gives you a structured, scalable, and developer-friendly platform to do it all.
1.1 Hello World
NAF powers the Nuts package manager. To learn about using Nuts as a tool, see the Nuts documentation.To make use of NAF you need add the dependency net.thevpc.nuts#nuts:1.0.0 and provide a hint to maven to point to the right repository https://maven.thevpc.net
Configure your pom.xml
<dependencies>
<dependency><groupId>net.thevpc.nuts</groupId><artifactId>nuts</artifactId><version>1.0.0</version></dependency>
</dependencies>
<repositories>
<repository><id>thevpc</id><url>https://maven.thevpc.net</url></repository>
</repositories>
Bootstrap your Workspace
import net.thevpc.nuts.*;
public class HelloWorld {
public static void main(String[] args){
Nuts.require();
}
}
Use NAF components, anywhere in your app
import net.thevpc.nuts.*;
public class HelloWorld {
public static void main(String[] args){
Nuts.require(); // <-- this command should be called only once per app
NOut.println(NMsg.ofC("Hello %s","World"));
runMethod();
}
public static void runMethod(){
NOut.println(NMsg.ofV("Hello $v",NMaps.of("v","World")));
}
}
A note on Nuts.require() and Workspaces
Nuts.require() in the example above is a convenience for demos and single-app mains. It creates an in-memory singleton workspace bound to the current JVM.
In production, Nuts never relies on a global singleton. A workspace is a filesystem-isolated environment (config, apps, cache, log...) selected via --workspace=path or programmatically:
NWorkspace wsA = Nuts.openWorkspace("-w=/opt/ws-a");
NWorkspace wsB = Nuts.openWorkspace("-w=/opt/ws-b");
wsA.runWith(() -> { // everything inside uses wsA: NOut, NPath, repos, etc. NOut.println("running in A"); });
Yes, you can create and switch between multiple NWorkspace instances in the same JVM. Isolation is by filesystem root, not by classloader, so it works reliably for multi-tenant services and tests.
2 Command Line Processing
NCmdLine is a flexible and OS-portable command line parser that supports short and long options, boolean flags, valued options, non-options, comments, and argument files. It is the recommended way to handle command line input in NAF applications.
NCmdLine supports command lines in the following form :
my-app -o=y --name='some name' -ex --extra value arg1 arg2
where the command here supports short and long options (short ones are -o, -e and -x, where -e and -x are combined as -ex), and of course non options or regular arguments (here arg1 and arg2). Note also that value could be interpreted as a value for --extra (or not; depending on how you configure your parser, for this option).
2.1 Creating NCmdLine instance
Command line can either be created manually or parsed.
Manual creation
You can create a command by providing the arguments:
NCmdLine c1= NCmdLine.ofArgs("ls","-l");
Parsing from string
You can also create a commandline by parsing a string.
NAF supports multiple commandline dialects (bash/linux, bat/Windows,...)
NCmdLine c1= NCmdLine.of("ls -l", NShellFamily.BASH);
When you do not specify the NShellFamily, runtime OS default is considered.
NCmdLine c1= NCmdLine.parse("ls -l");
Portable Parsing
You would want to be portable across all operating systems, you can use ofDefault method.
NCmdLine c1= NCmdLine.ofDefault("ls -l");
2.2 Command Line Elements
Options vs Non-options
In NAF, command line arguments are categorized into three main types:
- Options : Arguments that start with - or + and may carry a value.
- Non-Options : Arguments that do not start with - or +. Typically filenames, commands, or positional arguments.
- Comments : Arguments that are ignored, starting with -// or +//.
Short vs Long Options
- Short options: single prefix - or + followed by a single character, e.g., -o or +x.
- Long options: double prefix -- or ++ followed by a word, e.g., --output or ++enable-feature.
Short options can be combined:
-ex # equivalent to -e -x
Exception: certain options like -version or +version are treated as single options and not expanded.
Options With Values
- Options can carry a value (string, boolean, number).
- Values can be attached with = or provided as the next argument:
--name=Alice # attached value
+name Alice # next-argument value
Boolean Options
Boolean options do not always require a value:
--install # equivalent to true
+install=true
--install=false # or --!install / --~install
! and ~ are treated as negation. This is useful in shells where ! has special meaning.
Boolean options in NAF can be true or false without explicitly writing true or false. The parser recognizes multiple synonyms for convenience:
True Values
true, enable, enabled, yes, always, y, on, ok, t, o
False Values
false, disable, disabled, no, none, never, n, off, ko, f
Non-Options
Non-options are all other arguments — files, commands, or positional values:
my-app -o --name Alice file1.txt file2.txt
Here, file1.txt and file2.txt are non-options.
Ignored Arguments
Arguments starting with -// or +// are ignored and can be used for comments or metadata in the command line.
2.3 Customizing CmdLine parsing
Configuring NCmdLine
commandName(String)
This method help defining the name of the command supporting this command line. This is helpful when generating errors/exception so that the message is relevant for instance, you would call setCommandName("ls"), so that all errors are in the form of unexpected argument --how
expandSimpleOptions(true|false)
This method can change the default behavior of NCmdLine (defaults to true). When true, options in the form -ex are expanded to -e -x.
registerSpecialSimpleOption(argName)
This method limits setExpandSimpleOptions application so that for some options that start with - (simple options), they are not expanded. A useful example is '-version'. You wouldn't want it to be interpreted as '-v -e -r -s -i -o -n', would you?
expandArgumentsFile(true|false)
This method can change the default behavior of NCmdLine (defaults to true). When false, options in the form @path/to/arg/file are interpreted as non options. When true (which is the default), the parser will load arguments from the given file/location.
2.4 Processing the commandline
Using CommandLine, The recommended way...
NCmdLine has a versatile parsing API. The Matcher is the recommended entry point: it lets you declare, in a single fluent chain, how each option or positional argument should be recognized (when*), how a match should be interpreted (as*), and how the overall pass should behave once every token has been considered (require/requireAll/anyMatch/noMatch).
NCmdLine cmdLine = NApplication.of().cmdLine(); // or from somewhere else
NRef<Boolean> boolOption = NRef.of(false);
NRef<String> stringOption = NRef.ofNull();
List<String> others = new ArrayList<>();
cmdLine.matcher()
.when("-o", "--option").asFlag(v -> boolOption.set(v.booleanValue()))
.when("-n", "--name").asEntry(v -> stringOption.set(v.stringValue()))
.whenNonOption().asArg(v -> others.add(v.image()))
.withDefaults()
.requireAll();
//do the good stuff here
NOut.println(NMsg.ofC("boolOption=%s stringOption=%s others=%s", boolOption, stringOption, others));
The when* family — declaring what to match
| Method | Matches | Notes |
|---|---|---|
| One or more named options or positional keywords. | The common case. Supports multi-patterns and multi-word sequences, e.g., |
| any token that looks like an option (-x, --xxx) | no name filtering |
| any token that does not look like an option | typically used to collect positional arguments |
| the single peeked token, filtered by your own predicate | general-purpose replacement for whenOption/whenNonOption when their fixed shapes aren't enough; safe on an empty cmdline |
| arbitrary, multi-token lookahead | the full escape hatch — inspect several upcoming tokens before deciding; you are responsible for checking |
The as* family — interpreting a match
| Method | Consumer receives | Value rule |
|---|---|---|
| matched NArg | boolean toggle; never consumes a following token; honors negation ( |
| matched NArg | like asFlag, but the consumer only fires when the flag resolves to true |
| matched NArg | value via |
| matched NArg | value only via |
| matched NArg | value via |
| matched NArg | no value contract — hands back whatever matched, as-is (typically paired with |
| the whole NCmdLine | full manual control — use for --help-style unconditional termination, or conditional termination based on your own runtime check; you decide whether/how much to consume (e.g. |
Supplying a raw processor: with(...) and withDefaults()
with(NCmdLineProcessor processor) registers a processor directly, bypassing the declarative when/as vocabulary entirely. It's useful for two different situations:
1. Falling back to session-level default handling for anything the declarative rules above didn't match:
cmdLine.matcher()
.when("-o", "--option").asFlag(v -> boolOption.set(v.booleanValue()))
.withDefaults() // delegates unmatched tokens to NSession defaults
.requireAll();
2. Mutually-exclusive top-level command dispatch, where each processor decides for itself whether it applies (returns true) or declines (returns false), and the first one that applies wins:
boolean handled = cmdLine.matcher()
.when(cl -> doVersion(cl))
.when(cl -> doInstall(cl))
.when(cl -> doUninstall(cl))
.anyMatch();
if (!handled) {
NOut.println(NMsg.ofPlain("unrecognized command"));
}
// ...
private boolean doInstall(NCmdLine cl) {
if (!cl.next("install").isPresent()) {
return false; // not our command — let the next processor try
}
String pkg = cl.next().map(NArg::image).orElse("");
NOut.println(NMsg.ofC("installing %s", pkg));
return true;
}
Finishing the pass: require, requireAll, anyMatch, noMatch
| Method | Returns | Behavior |
|---|---|---|
| void | tries to match one token against every registered rule; throws if nothing matched |
| void | equivalent to |
| boolean | tries every registered rule once against the current token; true if one matched, never throws |
| boolean | |
Use require/requireAll when an unrecognized token should be a hard error (the common case for a leaf command). Use anyMatch/noMatch when you want to decide yourself what happens on failure — e.g. printing help instead of throwing.
A more complete example
NCmdLine cmdLine = NApplication.of().cmdLine();
Opts o = new Opts();
cmdLine.matcher()
.when("--help").asRaw(cl -> {
showHelp();
cl.skipAll(); // everything after --help is discarded
})
.when("-f", "--full").asFlag(v -> o.full = v.booleanValue())
.when("-e", "--example").asEntry(v -> o.example = v.stringValue())
.when("--color").asAttachedEntry(v -> o.color = v.stringValue())
.when("-J", "--java-options").asRequiredEntry(v -> o.javaOptions = v.stringValue())
.whenArg(a -> !a.isOption() && a.isNonOption()
&& (a.image().startsWith("./") || a.image().startsWith("../")))
.asArg(v -> o.paths.add(v.image()))
.whenNonOption().asArg(v -> o.positionals.add(v.image()))
.withDefaults()
.requireAll();
//do the good stuff here
NOut.println(NMsg.ofC("options=%s", o));
Using CommandLine, The low-level way...
The matcher is built entirely on top of NCmdLine's pull API — next(...),peek(), skip() — and you can always drop down to that API directly for full manual control. This is also the form autocomplete support is easiest to reason about explicitly, since each next(...) call can carry its own display label and value-completion hint right at the call site, instead of those being attached separately through a matcher chain.
NCmdLine cmd = NApplication.of().cmdLine();
boolean boolOption = false;
String stringOption = null;
List<String> others = new ArrayList<>();
while (cmd.hasNext()) {
NOptional<NArg> option = cmd.next(NArgType.FLAG, "toggle option", "-o", "--option");
if (option.isPresent()) {
boolOption = option.get().booleanValue();
continue;
}
NOptional<NArg> entry = cmd.next(NArgType.ENTRY, "name",
NArgValueComplete.ofFlags(NArgCompleteFlag.NONE),
"-n", "--name");
if (entry.isPresent()) {
stringOption = entry.get().stringValue();
continue;
}
NOptional<NArg> file = cmd.next(NArgType.ENTRY, "file path",
NArgValueComplete.ofFlags(NArgCompleteFlag.FILENAMES),
"--file");
if (file.isPresent()) {
others.add(file.get().stringValue());
continue;
}
NOptional<NArg> nonOption = cmd.nextNonOption();
if (nonOption.isPresent()) {
others.add(nonOption.get().image());
continue;
}
if (cmd.isCompleteMode()) {
// still under autocomplete: nothing else matched this word,
// skip it silently instead of failing the whole completion pass
cmd.skip();
continue;
}
cmd.throwUnexpectedArgument();
}
// test if application is running in exec mode
// (and not in autoComplete mode)
if (cmd.isExecMode()) {
//do the good stuff here
NOut.println(NMsg.ofC("boolOption=%s stringOption=%s others=%s", boolOption, stringOption, others));
}else{
cmd.printCompleteResult();
return;
}
Each branch follows the same shape: try one next(...) call, and if it's present, consume it and continue the loop; if every branch declines, the final fallback either silently skip()s (when completing — an incomplete word shouldn't abort the whole completion pass) or throwUnexpectedArgument()
(when actually executing — an unrecognized token really is an error).
This is exactly the same decision tree the matcher expresses more declaratively via when(...)/as*(...) — the difference is purely how the per-option display label and completion hint are supplied: inline as arguments to next(...) here, versus attached through the fluent chain there. Both forms feed the same underlying autocomplete machinery, which is covered next.
The Unified Model: Parsing is Completion
One of the most powerful features of NCmdLine is that you do not write separate logic for auto-completion. The same matcher() chain that executes your command also generates shell completions.
Why do we "process each time"?
When a user presses
3 Structured Messaging
NMsg is the Nuts Application Framework (NAF) message and text formatting system. It allows you to create dynamic, flexible, and visually rich messages in your applications. You can use it for console output, logs, and any scenario requiring formatted text.
NMsg is fully integrated with NOut and NErr for displaying rich and meaningful CLI output.
3.1 Placeholder Formats
NMsg supports multiple placeholder formats for dynamic message generation:
C-style (ofC) – like printf in C (
%s,%d, etc.)Java / SLF4J style (ofJ) –
{}or{0},{1}Variable substitution (ofV) – named placeholders using
$nameor${name}Moustache substitution (ofM) – named placeholders using
Sql substitution (ofS) – positional and named placeholders using
?or:nameCustom substitution (ofCustom) – user defined format by implementing NMsgCustomFormatter
Examples
// C-style formatting
NMsg.ofC("Hello %s, you have %d new notifications", "Alice", 5);
// Java formatting
NMsg.ofJ("Downloading {0} from {1}", "report.pdf", "server-01");
// SLF4J-style formatting
NMsg.ofJ("Downloading {} from {}", "report.pdf", "server-01");
// Variable substitution from map
NMsg.ofV("User $user on ${app}", Map.of("user", "Alice", "app", "NAF"));
// Variable substitution from function
NMsg.ofV("Threshold=$th, Date=$date", name -> switch (name) {
case "th" -> 0.85;
case "date" -> LocalDate.now();
default -> null;
});
// SQL-style positional formatting
// Beware you are responsible for escaping the strings
NMsg.ofS("SELECT * FROM users WHERE status = ? AND age >= ?", "\"ACTIVE\"", 21);
// SQL substitution from function
// Beware you are responsible for escaping the strings
NMsg.ofV("Select a from Table where a.column=:value", name -> switch (name) {
case "value" -> 0.85;
default -> null;
});
// Variable substitution from function (with mustache)
NMsg.ofM("Threshold=, Date=", name -> switch (name) {
case "th" -> 0.85;
case "date" -> LocalDate.now();
default -> null;
});
// Custom registered formatter
NMsg.ofCustom("upper", "hello world");
Notes:
- Avoid mixing styles in a single message.
${}syntax is safer for complex strings (e.g.,$val123textvs${val}123text).{ { } }syntax is safer when '$' has specific meanings in your context.
C-style Formatting (ofC)
Use ofC to create messages using standard String.format()-style syntax:
NOut.println(NMsg.ofC("Hello %s", "world"));
Placeholders like %s, %d, etc., behave as expected. Useful for simple messages with positional arguments.
Java MessageFormat (ofJ)
Use ofJ for Java-style formatting with {0}, {1} placeholders:
NOut.println(NMsg.ofJ("Hello {0}", "world"));
NOut.println(NMsg.ofJ("Hello {}", "world")); // SLF4J-style
Both formats are supported, and will be filled using the provided arguments in order (but should not be mixed).
{}placeholders are matched sequentially, like in SLF4J.{0},{1}, etc. allow for specific argument reordering or reuse.
Variable-based Formatting (ofV)
Use ofV to format messages using named variables:
NOut.println(NMsg.ofV("Hello $v", NMaps.of("v", "world")));
NOut.println(NMsg.ofV("Hello ${v}", NMaps.of("v", "world")));
Both $v and ${v} syntaxes are supported.
Variables are replaced by name using the $ prefix. This is useful for dynamically named arguments or template-based rendering, particularly when formatting messages from dynamic key-value maps (e.g., for templates or localization).
- $v is simple and concise.
${v} is safer when followed by alphanumeric characters (e.g.,
$val123textvs${val}123text).
Missing variables are left as-is or replaced with a placeholder, depending on context or configuration.
Variable-based Moustache Formatting (ofM)
Use ofM to format messages using named variables with Mustache-style placeholders:
NOut.println(NMsg.ofV("Hello ", NMaps.of("v", "world")));
Variables are replaced by name using Mustache-style. This is useful for dynamically named arguments or template-based rendering, particularly when formatting messages from dynamic key-value maps (e.g., for templates or localization). This isolates variables completely from string payloads that might natively use the $ character. Missing variables are left as-is or replaced with a placeholder, depending on context or configuration.
SQL-style Formatting (ofS)
Use ofS for SQL-style queries using ? positional placeholders:
// SQL-style positional formatting
// Beware: you are responsible for escaping the strings
NMsg.ofS("SELECT * FROM users WHERE status = ? AND age >= ?", "\"ACTIVE\"", 21);
// SQL variable substitution from function
// Beware: you are responsible for escaping the strings
NMsg.ofV("Select a from Table where a.column=:value", name -> switch (name) {
case "value" -> 0.85;
default -> null;
});
Because NMsg builds an abstract AST (NText) rather than interacting directly with a JDBC driver, parameter values are inserted literally into the node tree. Callers must handle any required SQL escaping or quoting manually.
Custom Formatting (ofCustom)
You can extend NMsg by registering a custom implementation of NMsgCustomFormatter.
Custom formatters can be registered dynamically at runtime via NExtensions or auto-discovered using standard Java SPI (META-INF/services/net.thevpc.nuts.spi.NComponent) paired with @NScore for priority ordering (when needed).
// Register custom formatter dynamically
NExtensions.of().registerInstance(NMsgCustomFormatter.class, new NMsgCustomFormatter() {
@Override
public String id() {
return "upper";
}
@Override
public NText format(NMsg msg) {
String m = (String) msg.message();
return NText.ofPlain(m.toUpperCase());
}
@Override
public List<String> extractParams(String message) {
return Collections.emptyList();
}
});
// Execute custom formatter by ID
NMsg msg = NMsg.ofCustom("upper", "hello");
3.2 Styling Messages
In NAF, messages are not just plain text — they can be styled and formatted to convey meaning, emphasize content, or improve readability. The NMsg API allows you to combine colors, text modes, and semantic tokens to create rich, dynamic messages that adapt to different contexts (CLI, GUI terminals, logs, etc.).
Why style messages?
- Highlight important information: e.g., warnings, errors, success messages.
- Improve readability: visually distinguish values, keys, or code snippets.
- Semantic clarity: convey the role of a message part (like a keyword, boolean, or comment) rather than just its content.
- Consistency: pre-defined color schemes and semantic tokens help maintain a unified look across your application.
Styling Categories
Default Styling
NAF automatically applies default styles to many common data types, so messages are expressive without requiring explicit styling:
- Boolean values (true / false) are styled using NTextStyle.bool().
- Numbers
- Dates / Times / Temporals
- Enums
- etc.
// Boolean value without explicit styling
NOut.println(NMsg.of("Value=%s", true));
// Equivalent to explicitly styling the boolean
NOut.println(NMsg.of("Value=%s", NMsg.ofStyledBool("true")));
Color Index / Theme
NAF provides predefined colors, e.g., primary1, secondary5, error, warn, which map to your application’s theme. These ensure consistent appearance without manually specifying RGB values.
// Primary / Secondary themed colors
NMsg.ofStyledPrimary1("text");
NMsg.ofStyledSecondary5("text");
// Arbitrary foreground color
NMsg.ofStyledForegroundColor("text", Color.RED);
// Modes: bold, blink, striked
NMsg.ofStyledBold("text");
NMsg.ofStyledBlink("text", Color.RED);
NMsg.ofStyledStriked("text");
Foreground / Background Colors
You can specify arbitrary colors using Java Color objects. Foreground colors affect text color; background colors can be combined to create highlighted blocks or banners.
// Arbitrary foreground color
NMsg.ofStyledForegroundColor("text", Color.RED);
Text Modes
Modes like bold, italic, blink, strikethrough add emphasis and can be combined with colors for richer visual cues.
// Modes: bold, blink, striked
NMsg.ofStyledBold("text");
NMsg.ofStyledBlink("text", Color.RED);
NMsg.ofStyledStriked("text");
Semantic Tokens
These are high-level categories representing the meaning of text:
- comments → for secondary or muted content
- warn → for warnings
- error → for errors or alerts
- keyword, string, boolean → for syntax-like highlighting
NMsg.ofStyledComments("comment");
NMsg.ofStyledWarn("warning");
NMsg.ofStyledString("string");
NMsg.ofStyledKeyword("keyword");
NMsg.ofStyledBoolean("boolean");
NMsg.ofStyledError("error");
3.3 Nested Messages
You can nest messages to create complex, styled outputs:
// Custom styling example
NMsg.ofC("Task %s completed with status %s",
"Upload",
NText.ofStyled("OK", NTextStyle.primary1())
);
Nested messages combine formatting, styling, and placeholders dynamically.
// Nested messages example
NMsg.ofV("User $user completed ${task}",
NMaps.of(
"user", "Alice",
"task", NMsg.ofV("task %s in %s", "Upload",
NText.ofStyled("123ms", NTextStyle.secondary1()))
)
);
3.4 Text Rendering Formats
NMsg NMsg also provides multiple text rendering formats to make messages visually expressive. You can use plain messages that print as-is, styled messages to highlight errors, warnings, or other emphasis, code blocks for monospaced text with optional language hints, and NTF (Nuts Text Format) for lightweight markup supporting bold, italic, colors, and other rich formatting. These formats, combined with placeholders, allow NMsg to produce rich, dynamic, and visually informative output in any context.
NTF allows quick and readable text formatting directly in message strings.
NMsg.ofNtf("##bold## ##:/:italic## java public class{} ");
you can use Code blocks to render source code with syntax coloring
NMsg.ofPlain("This is a plain message");
NMsg.ofCode("java", "System.out.println(\"Hello NAF\");");
you can also force plain text rendering
NMsg.ofPlain("This is a plain message");
4 Structured Elements
With NElement, Nuts lets you build, parse, and format structured data effortlessly. From plain objects toJSON, XML, orTSON, you can read and write files, parse into Java objects, or print with optional NTF color formatting — all in a runtime-friendly way.
4.1 NElement API Documentation
1. Introduction
NElement is the foundational agnostic object model for structured data in the Nuts Application Framework (NAF). It is designed to act as a universal pivot format for transforming data between JSON, XML, YAML, TSON, and other structured representations.
While it supports multiple formats, it is primarily grounded in TSON (Typed JSON), which is inherently a superset of JSON, YAML, and XML. Because of this rich foundation, NElement offers two massive advantages:
- Universal Pivot: Parse from one format, manipulate in a unified model, and serialize to another.
- True Roundtrip Fidelity: The TSON parser is a true roundtrip parser. It can read a configuration file, allow you to update specific values programmatically, and write it back without dropping comments, punctuation, spaces, or original formatting.
Core Design Principles
Immutable Construction: Strongly prefers Builder patterns (NObjectElementBuilder, NArrayElementBuilder) to construct elements safely and predictably.
Roundtrip Awareness: Captures and preserves NElementComment, NElementLine, and NNewLineMode metadata during parsing.
Fail-Never-Again Navigation: Uses is*() checks and NOptional returning as*() methods to prevent ClassCastException. Errors and warnings are attached directly to the tree as NElementDiagnostic rather than halting execution.
2. The Roundtrip Capability (Preserving Formatting)
One of the most powerful features of NElement is its ability to update configuration files while acting as a "good citizen"—leaving the developer's comments, spacing, and layout completely intact.
The secret behind NElement's ability to preserve formatting lies in its Affix System. Every element can hold NBoundAffix objects anchored to specific NAffixAnchor positions (e.g., START, PRE_1, POST_1, SEP_1, END).
Affixes include:
NElementSpace / NElementNewLine: Preserves exact whitespace and line breaks.
NElementSeparator: Preserves commas, semicolons, etc.
NElementComment: Preserves block (/ /) and line (//) comments.
NElementAnnotation: Preserves custom metadata (e.g., @deprecated).
Example: Updating a Config File Without Losing Comments
Imagine a config.tson (or .json) file:
{
// Database connection settings
host: "localhost", // Default to local
port: 8080,
/* Feature flags */
features: {
darkMode: true
}
}
Step 1: Parse with Roundtrip Preservation
// The TSON reader captures comments, whitespace, and structure into the NElement tree
NElement config = NElementReader.ofTson().read(NPath.of("config.tson"));
Step 2: Safely Navigate and Modify
// Safely find and update the port, attaching a diagnostic if something is wrong
NElement updatedConfig = config.transformOptional(new NElementTransform() {
@Override
public List<NElement> preTransform(NElementTransformContext context) {
NElement current = context.element();
// Look for the 'port' field
if (current.isNamedPair("port")) {
// Safely update the value to 9090
return Collections.singletonList(
NElement.ofPair("port", NElement.ofInt(9090))
);
}
// Return unmodified element for everything else (preserving comments/structure)
return Collections.singletonList(current);
}
}).orElse(config);
Step 3: Write Back with Fidelity
// The writer reconstructs the file, keeping the comments and original layout
String updatedTson = NElementWriter.ofTson()
.formatter(NElementFormatter.ofPretty()) // Respects original spacing where possible
.formatPlain(updatedConfig);
// Output will still contain "// Database connection settings" and "/* Feature flags */"
NOut.println(updatedTson);
Note: The underlying tree retains NElementComment and NElementLine nodes attached to their respective parents, ensuring the roundtrip is lossless regarding human-readable metadata.
3. Expression Handling & Reshaping (NFlatExprElement)
NElement does not wire operator precedence rules by default. When parsing expressions, it initially creates an NFlatExprElement (a flat list of operands and operators). It is up to the developer to choose how to resolve precedence.
This is done via the NExprElementReshaper, which transforms a flat expression into a structured NOperatorElement tree.
NFlatExprElement flatExpr = /* parsed flat expression: "a + b * c" */;
// Choose a reshaping strategy
NElement structuredTree = flatExpr.reshape(NExprElementReshaperType.JAVA);
// Applies standard Java precedence (* before +)
// Available Reshaper Types:
// - DEFAULT: Standard fallback precedence.
// - JAVA: Standard Java/C-like precedence.
// - LEFT_ASSOCIATIVE: Evaluates strictly left-to-right.
// - LOGICAL: Prioritizes logical operators (AND, OR).
// - EMPTY: Returns the flat structure as-is.
Note: The API supports a massive NOperatorSymbol enum, including standard math (+, -, , /), logical (&&, ||), arrows (->, =>), and advanced mathematical Unicode symbols (∫, ∑, ∈, ⊆), complete with lexeme aliases.
4. NElement as a Pivot Format
Because NElement abstracts the underlying syntax, you can use it to translate between formats. While TSON/JSON roundtrip is fully supported today, the model is designed to accommodate XML and YAML as they are integrated.
// 1. Parse from JSON
String jsonInput = "{\"name\": \"app\", \"version\": 1}";
NElement pivot = NElementReader.ofJson().read(jsonInput);
// 2. Manipulate using the agnostic NElement API
NElement enhanced = NElement.ofObjectBuilder()
.addAll(pivot.asObject().get().entries()) // Copy existing
.set("environment", NElement.ofString("production"))
.build();
// 3. Serialize to TSON (or future XML/YAML writers)
String tsonOutput = NElementWriter.ofTson().formatPlain(enhanced);
4. Creating Elements (The Builder Pattern)
To maintain immutability and avoid the pitfalls of mutable state, always prefer Builder static factory methods.
// Primitives
NElement str = NElement.ofString("Hello World");
NElement num = NElement.ofInt(42, NNumberLayout.DECIMAL, "ms"); // With layout/suffix
NElement bool = NElement.ofTrue();
// Complex Structures (Builder Pattern)
NElement complexStructure = NElement.ofArrayBuilder()
.add(
NElement.ofObjectBuilder()
.set("name", NElement.ofString("service-a"))
.set("active", NElement.ofTrue())
.set("endpoints", NElement.ofArrayBuilder()
.add(NElement.ofString("http://localhost:8080"))
.add(NElement.ofString("http://localhost:8081"))
.build())
.build()
)
.build(); // Returns an immutable NElement
5. Safe Inspection and Extraction
Avoid ClassCastException entirely by using the is*() and as*() method pairs. The as*() methods return an NOptional, enabling functional, fail-safe data extraction.
NElement elem = /* ... parsed element ... */;
// 1. Type Checking
if (elem.isNamedObject("database")) {
// ...
}
// 2. Safe Casting and Extraction (Functional Style)
NOptional<String> hostOpt = elem.asObject()
.flatMap(obj -> obj.get("host")) // Get the 'host' pair
.flatMap(NElement::asStringValue); // Extract the string value
if (hostOpt.isPresent()) {
NOut.println(NMsg.ofC("Connecting to: %s",hostOpt.get()));
} else {
// Fail-never-again: handle the absence gracefully
}
// 3. Direct Typed Extraction
NOptional<LocalDate> date = elem.asLocalDateValue();
NOptional<Boolean> flag = elem.asBooleanValue();
6. Serialization & Object "Destruction"
When converting arbitrary Java objects into NElement, the framework follows a strict, predictable fallback chain known as "Destruction":
Explicit Serializer: If an NElementSerializer is registered for the class/interface in the NElementMapperStore, it is used.
NToElement Interface: If the object implements NToElement, its toElement() method is called.
- Recursive Destruction: For any other object, the framework uses reflection to navigate its fields/getters, recursively building an NObjectElement or NArrayElement.
Undestructable Types: The recursion stops when it hits a "simple" or "atomic" type (e.g., String, Number, Boolean, Instant, Path).
NCustomElement Fallback: If a type is explicitly marked as undestructable (or cannot be destructed), it is wrapped in an NCustomElement.
The Power of NCustomElement
You can attach any Java object directly into the NElement tree as an NCustomElement. This allows for a powerful mixture of structured, serializable data and rich, domain-specific Java objects that should not be flattened.
// Prevent a specific rich type from being destructed into a plain String/Map
NElements elements = NElements.of();
elements.mapperStore()
.removeAllSimpleTypesFilters()
.addSimpleTypesFilter(c -> MyRichDomainObject.class.isAssignableFrom(c));
Map<String, Object> data = Map.of("id", 1, "payload", new MyRichDomainObject());
NElement tree = elements.toElement(data);
// 'payload' is now safely stored as an NCustomElement, preserving its identity.
7. Deserialization & Contextual Mapping
Deserializers are highly flexible and can be registered in the NElementMapperStore based on multiple contextual dimensions. The framework will automatically select the most specific match:
NElementMapperStore ms = parser.mapperStore();
// 1. By Java Type
ms.setDeserializer(MyConfig.class, myCustomDeserializer);
// 2. By Element Type (e.g., force all OBJECTs to deserialize a certain way)
ms.setDeserializer(NElementType.OBJECT, MyConfig.class, myObjectDeserializer);
// 3. By Named Element (e.g., only when the key/name is "database")
ms.setDeserializer(NElementType.OBJECT, "database", NNameSelectorStrategy.CASE_INSENSITIVE, MyConfig.class, myDbDeserializer);
You can register custom deserializers for specific Java classes to handle complex parsing logic, such as accumulating repeated fields into an array or applying lenient parsing rules.
NElementMapperStore ms = parser.mapperStore();
ms.setDeserializer(NElementType.OBJECT, MyConfig.class,
ms.deserializerBuilderOf(MyConfig.class)
.configureLenient()
.booleanDefaultTrue()
.onUnsupportedChild(context -> {
// Custom logic to handle unexpected child elements gracefully
MyConfig instance = context.instance();
// ... accumulate or transform ...
return true; // indicate handled, preventing a parse failure
}).build()
);
8. Diagnostics and Error Handling
Aligning with robust error-handling design, NElement supports attaching diagnostics directly to the tree. This allows the system to distinguish between recoverable warnings and non-recoverable errors (like an NErrorElement) without throwing exceptions that tear down the parsing process.
// Check for non-recoverable structural errors
if (elem.isErrorTree()) {
List<NElementDiagnostic> fatalIssues = elem.treeDiagnostics();
log.error("Element tree contains fatal errors: {}", fatalIssues);
// Handle gracefully, perhaps by falling back to defaults
} else {
// Process normally, but still check for warnings
List<NElementDiagnostic> warnings = elem.diagnostics();
if (!warnings.isEmpty()) {
log.warn("Configuration has warnings: {}", warnings);
}
}
9. Core Element Type Hierarchy
All elements implement NElement. They are categorized into specialized interfaces for type-safe operations:
| Interface | Description |
|---|---|
NPrimitiveElement | Atomic values (String, Int, Boolean, Instant, etc.). Provides value(). |
NObjectElement | Key-value container. Extends NNamedElement, NListContainerElement, and NParametrizedContainerElement. |
NArrayElement | Ordered list of elements. Supports named arrays and parameterized arrays (e.g., func(arg1, arg2)[item1, item2]). |
NFragmentElement | A generic, flexible container. Provides extensive path-based querying (getByPath, getIntValueByPath, etc.). |
NListElement | Represents ordered/unordered lists with depth, markers, and marker variants (ideal for Markdown-like structures). |
NTupleElement | Positional parameter container, optionally named. |
NFlatExprElement | A flat sequence of operands and operators, awaiting reshaping via NExprElementReshaper. |
NOperatorElement | A structured expression node with NOperatorPosition, operands, and NOperatorSymbols. |
NCustomElement | Wraps an arbitrary Java Object that should not be destructed. |
NBinaryStreamElement / NCharStreamElement | Represents lazy or large data payloads via NInputStreamProvider / NReaderProvider. |
NEmptyElement | Represents an explicitly empty state. |
10. Formatting Styles (NElementFormatterStyle)
The formatter defines a layered hierarchy of rules, moving from raw data preservation to total structural reconstruction:
| Style | Intervention | Use Case |
|---|---|---|
VERBATIM | Low. Writes exactly what is stored in affixes. Only injects whitespace for Fatal collisions (where tokens would merge). | Maintaining Git history or manual formatting in config files. |
STABLE | Medium-Low. Fixes fatal collisions + injects spacing for Unpretty collisions (e.g., between quotes and identifiers). | Standard programmatic serialization and logging. |
SIMPLE | Medium. Builds on STABLE, injects missing structural separators (commas), and strips Root Garbage (parent separators). | CLI output, displaying individual property values, UI labels. |
COMPACT | High. Strips all optional whitespace and non-essential separators. Reapplies only absolute minimum Fatal disambiguation. | Network transmission or high-density data storage. |
PRETTY | Total. Ignores existing affixes. Performs structural reconstruction with consistent indentation, column alignment, and complexity-based line wrapping. | Generating documentation, example files, auto-formatting. |
CUSTOM | Variable. Reserved for user-defined formatting logic via NElementFormatterAction. | Specialized domain-specific rendering. |
11. Tree Traversal & Safe Navigation
Avoid ClassCastException by using the is*() and as*() method pairs, which return NOptional.
NElement elem = /* ... */;
// Safe, functional extraction
NOptional<String> host = elem.asObject()
.flatMap(obj -> obj.get("network"))
.flatMap(obj -> obj.get("host"))
.flatMap(NElement::asStringValue);
// Path-based querying (via NFragmentElement / NListContainerElement)
NOptional<Integer> port = elem.getIntValueByPath("network", "port");
12. Custom Traversal with NElementVisitor
elem.traverse(new NElementVisitor() {
@Override
public NTreeVisitResult enter(NElement element) {
if (element.isErrorTree()) {
return NTreeVisitResult.TERMINATE; // Stop traversal on non-recoverable error
}
return NTreeVisitResult.CONTINUE;
}
@Override
public NTreeVisitResult visitAnnotation(NElementAnnotation annotation) {
// Annotations are not NElements, handled separately
return NTreeVisitResult.CONTINUE;
}
@Override
public void exit(NElement element) {
// Post-order processing
}
});
13. Readers & Writers (Multi-Format Support)
NElementReader and NElementWriter provide a unified API for multiple content types, with optional NTF (Nuts Text Format) support for enriched terminal output.
// Reading
NElement fromJson = NElementReader.ofJson().read("{\"a\": 1}");
NElement fromYaml = NElementReader.ofYaml().read(Path.of("config.yml"));
NElement fromTson = NElementReader.ofTson().read("a: 1 # with comment");
// Writing
String compactJson = NElementWriter.ofPlainJson().compact(true).formatPlain(elem);
String prettyYaml = NElementWriter.ofYaml().formatter(NElementFormatter.ofPretty()).formatPlain(elem);
// NTF (Rich Terminal) Output
NElementWriter.ofNtfTson().format(iterable, NPrintStream.of(System.out));
14. Best Practices Summary
- Treat NElement as the Pivot: Use it as the central, format-agnostic representation when translating between JSON, TSON, XML, or YAML.
- Leverage Roundtrip Parsing: Rely on NElementReader.ofTson() and NElementFormatter.ofVerbatim() to preserve NAffix metadata (comments, spacing) when patching configuration files.
- Reshape Expressions Explicitly: Remember that NFlatExprElement has no inherent precedence. Always call .reshape(NExprElementReshaperType.JAVA) (or another strategy) before evaluating.
- Prefer Builders: Always use NElement.ofObjectBuilder(), NElement.ofArrayBuilder(), etc., terminating with .build().
- Use NOptional: Rely on as*Value() and as*() methods for fail-safe navigation.
- Mix Structured and Custom Data: Don't be afraid to use NCustomElement to embed rich Java objects directly into the tree, preventing destructive reflection mapping.
- Attach, Don't Throw: Use NElementDiagnostic and isErrorTree() to handle parsing anomalies gracefully without tearing down the application.
5 Expressions & Templates
5.1 Expressions
NExpr — Expression & Template Engine (Nuts Ecosystem)
NExpr is a lightweight, embeddable expression and templating engine, part of the thevpc/Nuts ecosystem. It parses expression strings into an AST (NExprNode), evaluates them against a pluggable NExprContext, and powers a templating language (NExprTemplate) built on the same parser.
Typical uses: dynamic filtering/config expressions, safe user-facing scripting for CLI tools or config files, string interpolation/templating, and lightweight "scripting glue" inside larger Nuts-based applications (e.g. NAF, NARU).
This is the entry point into a small doc set:
| Doc | Covers |
|---|---|
NExpr.md (this file) | Core concepts, quick start, context building, variables, functions/constructs, the AST, if/else, literal mapping, design notes. |
| expr-operators | Operator kinds, declaring/removing operators, the full precedence & associativity table, NExprCommonOp, the complete built-in operator table. |
| expr-evaluation | The evaluation pipeline, NExprCallContext/NExprCallHandler/NExprNodeValue, custom resolvers, the built-in function table, worked examples. |
| expr-templating | NExprTemplate: Moustache/JSP-style directives, |
| expr-tokenizer | NStreamTokenizer, and how/why it differs from |
1. Core Concepts
| Concept | Type | Role |
|---|---|---|
| Context | NExprContext | Read-only evaluation environment: holds resolvable vars, functions, constructs, operators. Can |
| Mutable Context | | A context you can populate/mutate at runtime: declare/undeclare vars, functions, constructs, operators; set variable values. |
| Context Builder | NExprContextBuilder | Fluent builder used to assemble a context (built-ins, math/physics constants, custom operators, resolvers) before calling |
| Node | NExprNode (and subtypes) | The parsed AST. Has a |
| Var | NExprVar | A named, gettable/settable value (variable or constant) bound into a context. |
| Function | NExprFunction | A named callable, used for both functions ( |
| Operator | NExprOperator | A named callable with an NExprOpType (PREFIX / POSTFIX / INFIX), a precedence, and an associativity. See expr-operators. |
| Template | NExprTemplate | A text-templating layer built on the same expression engine. See expr-templating. |
Object graph at a glance
NExprContextBuilder.of()
.declareBuiltins()
.declareVar(...) / .declareOperator(...) / .declareVars(resolver) / ...
.build() -> NExprContext (read-only usage)
.buildMutable() -> NExprMutableContext (can declare/undeclare/set at runtime)
NExprContext
.parse(String) -> NOptional<NExprNode>
.evalFunction/.evalOperator/... -> NOptional<Object>
.ofTemplate() -> NExprTemplate
2. Quick Start
2.1 Parse and evaluate a plain expression
NExprContext expr = NExprContextBuilder.of()
.declareBuiltins()
.build();
NExprNode n = expr.parse("1+2*3").get(); // NOptional<NExprNode>
NOut.println(n); // "1 + 2 * 3"
2.2 Build a mutable context, declare variables, evaluate with side effects
NExprMutableContext expr = NExprContextBuilder.of()
.declareBuiltins()
.buildMutable();
expr.declareVar("a");
NExprNode n = expr.parse("a=1").get();
Object result = n.eval(expr).get(); // assignment expression; evaluates and stores into "a"
2.3 String interpolation with $'...'
NExprMutableContext expr = NExprContextBuilder.of()
.declareBuiltins()
.buildMutable();
expr.declareVar("v");
expr.setVarValue("v", "me");
NExprNode n = expr.parse("$'something for $v'").get();
String out = (String) n.eval(expr).get(); // "something for me"
2.4 Templating (Moustache style)
Map<String, Object> vars = new HashMap<>();
vars.put("world", "Earth");
vars.put("yellow", true);
vars.put("blue", true);
NExprTemplate tpl = NExprContextBuilder.of()
.declareBuiltins()
.declareVars(NExprVarResolver.ofMap(vars))
.build()
.ofTemplate()
.withJspStyle();
String out = tpl.processString(
"hello <%:if yellow %> <%world%> <%:else if blue %> my <%:else%> World <%:end%>"
);
// -> "hello Earth "
See expr-templating.md for the full directive set, and expr-evaluation for a formula/constraint-evaluation example closer to numeric/engineering use.
3. Building a Context: NExprContextBuilder
NExprContextBuilder.of() starts from an empty context builder (internally derived from NExprContext.of().childContext()), so builders are always created as a child of a base/root context — this supports layered/nested context composition.
Key builder operations:
| Method | Purpose |
|---|---|
| Registers the engine's default operators/functions/constructs (arithmetic, comparison, logical, indexing |
| Adds pi/PI/ |
| Adds a set of SI physics constants (C, |
| Adds standard |
| Register a single variable, or a resolver that lazily/dynamically resolves variables by name. |
| Register a function or a function resolver. |
| Register a "construct" — syntactically function-like, semantically distinct (see §5 below). |
| Register a custom operator — see [ |
| Register a generic, composite resolver — see below. |
| Symmetric removal methods for every |
| Get/set the mapper responsible for turning raw literal tokens into typed values (numbers, strings, booleans, etc). |
| If enabled, referencing an undeclared variable name auto-declares it instead of failing (useful for loosely-typed scripting contexts). |
| Produces an immutable NExprContext. |
| Produces a NExprMutableContext you can keep mutating after construction. |
3.1 Generic resolvers
Beyond the per-kind resolvers (NExprFunctionResolver, NExprVarResolver — see §5 and §4), the builder also accepts two more general resolver shapes:
public interface NExprResolver {
default NOptional<NExprFunction> getFunction(String fctName, NExprNodeValue[] args, NExprContext context) {
return NOptional.ofEmpty(() -> NMsg.ofC("function not found %s", fctName));
}
default NOptional<NExprFunction> getConstruct(String constructName, NExprNodeValue[] args, NExprContext context) {
return NOptional.ofEmpty(() -> NMsg.ofC("construct not found %s", constructName));
}
default NOptional<NExprOperator> getOperator(String opName, NExprOpType type, NExprNodeValue[] args, NExprContext context) {
return NOptional.ofEmpty(() -> NMsg.ofC("operator not found %s of type %s", opName, type));
}
default NOptional<NExprVar> getVar(String varName, NExprContext context) {
return NOptional.ofEmpty(() -> NMsg.ofC("var not found %s", varName));
}
}
@FunctionalInterface
public interface NExprOperatorResolver {
NOptional<NExprOperator> getOperator(String opName, NExprOpType type, NExprNodeValue[] args, NExprContext context);
}
NExprResolver is a composite resolver — a single object that can, at your option, back functions, constructs, operators, and variables all at once (every method has a sensible "not found" default via
NOptional.ofEmpty(...), so you only override the kinds you actually want to resolve). Registered viaNExprContextBuilder.declareResolver(NExprResolver). Useful when one backing source (e.g. a scripting bridge, a bean/reflection adapter, an embedding host object) legitimately supplies more than one kind of symbol.NExprOperatorResolver is the operator-specific counterpart to NExprFunctionResolver/NExprVarResolver — a single-method resolver for dynamically supplying NExprOperators by
(name, type), registered viaNExprContextBuilder.declareOperators(NExprOperatorResolver).
Note the consistent NOptional.ofEmpty(() -> NMsg.ofC(...)) idiom for "not found" — a lazily-built diagnostic message rather than an eager string, so resolvers that are asked about hundreds of candidate names during lookup chains don't pay message-formatting cost unless the failure message is actually inspected.
4. Variables — NExprVar
interface NExprVar extends NExprVarReader, NExprVarWriter {
String name();
Object get(NExprContext context);
void set(Object value, NExprContext context);
}
Factory methods on NExprVar:
| Factory | Semantics |
|---|---|
| Plain read/write variable, initially null. |
| Plain read/write variable with an initial value. |
| Fully custom get/set behavior (e.g. backed by an external object/bean). |
| Computed once, lazily, then treated as constant. |
| Fixed constant value. |
| Read-only, computed via reader each time (not cached). |
On a NExprMutableContext:
declareVar(String name)— declare with no value yet.declareVar(NExprVar var)— declare a fully custom variable.declareConstant(String name, Object value)— declare an immutable constant.setVarValue(String varName, Object value)— assign a value to an already-declared variable.getOrDeclareVar(String name, Supplier<Object> initialValue)— get-or-create in one call.undeclareVar(NExprVar)/removeVar(NExprVar)/removeVar(String)— remove a variable.
Bulk/dynamic variable resolution uses NExprVarResolver:
| Factory | Semantics |
|---|---|
| Resolver whose values are treated as lazily-computed constants. |
| Resolver whose values are read-only/live. |
| Backed by a plain Map — used in TemplateTest for injecting template variables. |
| Read-only map-backed resolver. |
5. Functions & Constructs — NExprFunction
interface NExprFunction {
static NExprFunction of(String fctName, NExprCallHandler handler);
String name();
Object eval(NExprCallContext callContext);
}
NExpr distinguishes functions from constructs even though both are represented by NExprFunction:
Functions (getFunction / declareFunction / evalFunction) — standard callable-by-name expressions, e.g.
printChunk(0)(see test8/test9).Constructs (getConstruct / declareConstruct / evalConstruct) — a syntactically similar but semantically separate namespace, typically used for constructor-like or keyword-like call forms (kept distinct from functions so the same name can mean different things in each namespace, and so language-level forms like object construction don't collide with user functions).
Both are resolved either by direct declaration or via a resolver:
NExprFunctionResolver.getFunction(fctName, args, context)— a functional interface, so custom resolution logic (e.g. reflection-based dispatch, bean method lookup) can be plugged in without declaring every function individually.
NExprCallContextType tags how a call is being evaluated: FUNCTION, CONSTRUCT, or OPERATOR (with alias parsing for strings like "FCT", "NEW"/"CONSTRUCTOR", "OP").
For how to actually implement a function/construct handler (NExprCallHandler, NExprCallContext, NExprNodeValue) and for two worked examples (a reflection-based function resolver and a physics-formula evaluator), see expr-evaluation. The full built-in function table (string, join, format*, isBlank, …) also lives there.
6. The AST — NExprNode
Every successful context.parse(expression) returns NOptional<NExprNode>.
public interface NExprNode {
static NExprWordNode ofWord(String name);
static NExprLiteralNode ofLiteral(Object name);
NOptional<Object> eval(NExprContext context);
NExprNodeType nodeType();
List<NExprNode> children();
String name();
}
6.1 Node types — NExprNodeType (complete enum)
public enum NExprNodeType implements NEnum {
FUNCTION,
OPERATOR,
WORD,
LITERAL,
INTERPOLATED_STR,
IF,
}
| Value | Meaning | Example |
|---|---|---|
WORD | A bare identifier/word token | a (test3); also the node checked in the built-in |
OPERATOR | Any operator application (prefix/postfix/infix, including grouping | |
LITERAL | A literal value token (number, quoted string, boolean, null) | the 1 in |
FUNCTION | A function or construct call node, e.g. | |
INTERPOLATED_STR | A | |
IF | An | |
6.2 Node subtypes seen in the tests
NExprWordNode — a bare word/identifier node (e.g. the c operand in
a*b+c, test11). Constructible directly viaNExprNode.ofWord(String).NExprLiteralNode — a literal value node (numbers, quoted strings, booleans — e.g. the 1 in
a.b>1, test12). Constructible directly viaNExprNode.ofLiteral(Object).NExprInterpolatedStringNode — produced by
ofDollarInterpolatedString(...)/ofMoustacheInterpolatedString(...), and by parsing$'...'syntax directly (test10).NExprNodeValue extends NExprNode— the argument-wrapper type passed into every NExprCallHandler; see expr-evaluation.
6.3 Common node API in practice
For an OPERATOR node,
name()is the operator's symbol/name ("+",".","(","[", ...) andchildren()holds its operands in order.toString()renders the expression back out in a normalized, spaced infix form (e.g.1+2*3→"1 + 2 * 3").eval(context)generally needs a mutable context for anything stateful (assignment,++/--, declaring vars on the fly).
6.4 Statements & sequencing
Multiple expressions/statements can be chained with ;, and consecutive/trailing separators are tolerated:
expr.parse("printChunk(0);;;;\n"); // test8 — trailing empty statements OK
expr.parse("printChunk(0);;printChunk(0);;printChunk(0)\n"); // test9 — chained calls
expr.parse("if (a) 'hello' else {'hella'};x=3"); // test7 — block body `{ ... }`, followed by another statement
{ ... } acts as a block/grouping for statement bodies (as an if/else branch, per test7). ; itself is a genuine left-associative infix operator (not just a parser-level separator) — see the built-ins table in expr-operators for exactly how it chains evaluation.
7. if / else Expressions
NExprNode n = expr.parse("if (a) 'hello' else 'hella' end").get();
// n.nodeType() == NExprNodeType.IF
Supported forms (from the test suite and the TemplateTest doc-comment):
if cond thenExpr else elseExpr end— single-line, both branches are expressions.Multi-branch
else ifchains are supported in the template layer (<%:else if ...%>) — see expr-templating.
8. Literal Mapping — NExprLiteralMapper
Every context (and builder) exposes a literalMapper(). This component is responsible for converting raw literal tokens encountered during parsing (numbers, quoted strings, booleans, etc.) into actual typed Java values used at evaluation time. You can supply a custom mapper via NExprContextBuilder.literalMapper(NExprLiteralMapper mapper) — useful if you want, e.g., all integer literals to become BigDecimal, or custom date/duration literal formats.
Under the hood, literal tokenization (before mapping) is done by NStreamTokenizer — see expr-tokenizer for how it decides a token is TT_INT/TT_LONG/TT_BIG_INT/TT_FLOAT/TT_DOUBLE/TT_BIG_DECIMAL in the first place.
9. API Reference Summary
NExprContext (read-only)
| Method | Returns | Purpose |
|---|---|---|
| | Look up a function by name/args. |
| | Look up a construct by name/args. |
| | Look up an operator by name+kind. |
| | All currently registered operators. |
| | Look up a variable. |
| | Shortcut for current value of a variable. |
| NExprContextBuilder | Start a new builder layered on top of this context. |
| | Directly invoke by name without pre-parsing an expression. |
| | Directly invoke a specific operator kind. |
| | Parse a full expression string into an AST. |
| NExprNodeValue | Wrap a raw value or an existing node as a call argument. |
| see expr-operators | Type-based operator-overload resolution. |
| NExprInterpolatedStringNode | Build interpolated string nodes programmatically. |
| NExprTemplate | Obtain the templating façade over this context. |
| NExprLiteralMapper | Current literal-to-value mapper. |
NExprMutableContext (adds to the above)
declareFunction, declareConstruct, declareVar, declareConstant, declareOperator, setVarValue, getOrDeclareVar, and the symmetric undeclare* / remove* family (by instance or by name).
NExprContextBuilder
See §3.
10. Design Notes & Practical Patterns
Layered contexts:
childContext()and the fact thatNExprContextBuilder.of()itself starts from a child of a root context suggest NExpr is meant to be composed in layers — e.g. a shared "base" context with math/physics constants, further specialized per use case (a CLI expression filter vs. a template renderer) without re-declaring everything.Restricted DSLs via operator pruning: you can start from
declareBuiltins()and subtract down to exactly the operator surface you want to expose to untrusted or simplified input — see [NExpr-Operators.md §removing/restricting](./NExpr-Operators.md). This is a good pattern for CLI query languages (e.g. filter expressions in nuts commands) where you don't want full scripting power.Functions vs. constructs as separate namespaces: if you're integrating NExpr into a larger framework (e.g. NARU), keep in mind function and construct calls are resolved independently — a name can be safely reused across both without collision, but that also means declaring a function does not make it callable as a construct.
NExprResolver for multi-kind backing sources:
if one object naturally backs several symbol kinds at once (e.g. an embedding host exposing both variables and methods), prefer a single NExprResolver over separately wiring a NExprVarResolver and a NExprFunctionResolver that both delegate to the same backing object.NExprCommonOp decouples semantics from spelling:
if you expose custom operator names/aliases to end users but still want generic numeric/string logic to "know" which one is "plus", implement against NExprCommonOp + findCommonInfixOp rather than hardcoding operator name strings — see expr-operators.Operators that mutate state (
, by calling=,+=,++, --, …) deliberately control when their operands get evaluated.eval(context)on specific NExprNodeValue args themselves rather than letting the engine eager-evaluate all arguments up front — see expr-evaluation.
11. Remaining Gaps
1.
NOptional's failure-path contract in full detail
— e.g. whether parse errors carry position/line info. NOptional is documented separately in the Nuts ecosystem; this doc set only covers the small set of accessor methods (.get(),.orNull(),.isPresent(), …) as they're actually used in NExpr call sites.2.
TERNARY_CMP precedence tier
— confirmed reserved for an upcoming ternary? :operator, not yet wired up.3.
NExprInterpolatedStringNode's full API
beyond construction — only its role (interpolated-string AST node) is evidenced, not its complete method set.
Happy to fold in answers to any of the above and expand the relevant doc.
5.2 Expression Operators
NExpr — Operators
Part of the NExpr doc set. Covers NExprOperator, operator kinds, declaring and removing operators, precedence & associativity, NExprCommonOp, and the complete built-in operator table.
interface NExprOperator {
static NExprOperator of(String name, NExprOpType operatorType, int operatorPrecedence,
NOperatorAssociativity associativity, NExprCallHandler handler);
NOperatorAssociativity operatorAssociativity();
String name();
NExprOpType operatorType();
int operatorPrecedence();
Object eval(NExprCallContext callContext);
}
1. Operator kinds — NExprOpType
| Kind | Meaning | Example |
|---|---|---|
PREFIX | Unary, precedes its operand | -x, |
POSTFIX | Unary, follows its operand | |
INFIX | Binary, between two operands | |
An operator is uniquely identified by the pair (name, type) — the same symbol can exist as both prefix and infix (e.g. - as unary negation vs. binary subtraction, or ++/-- as both prefix and postfix) since they're registered/looked-up separately (getOperator(opName, type, ...), removeOperator(name, type)).
2. Declaring custom operators
NExprContextBuilder.of()
.declareBuiltins()
.declareOperator("~", NExprOpType.PREFIX, /* precedence */ 1300,
NOperatorAssociativity.RIGHT,
callContext -> { /* custom logic */ return null; })
.build();
Shorthand overloads exist (declareOperator(name, handler), declareOperator(name, type, handler)) for cases where you don't need to pin an explicit precedence/associativity.
You can also supply a whole NExprOperatorResolver instead of declaring operators one at a time — see [NExpr.md §3.1 Generic resolvers](./NExpr.md).
3. Removing / restricting the operator set
A useful pattern from the test suite (_retain helper in ExprTest) shows how to whittle a built-in context down to only a chosen subset of operators, by iterating NExprMutableContext.operators() and calling removeOperator(operator) for anything not matching a desired (type, name):
NExprMutableContext ctx = ...;
for (NExprOperator op : ctx.operators()) {
boolean keep = op.operatorType() == NExprOpType.INFIX && op.name().equals("+");
if (!keep) {
ctx.removeOperator(op);
}
}
// Now ctx.parse("1+2+3") works, but "1+2*3" would fail (no "*" operator left).
This is useful for building restricted DSLs — e.g. exposing only arithmetic to end users, or only comparison operators for a filter language.
4. Precedence & associativity
NOperatorAssociativity — exactly two values:
public enum NOperatorAssociativity implements NEnum {
LEFT,
RIGHT;
}
NExprOpPrecedence — the canonical numeric precedence constants (higher number = binds tighter):
public final class NExprOpPrecedence {
public static final int STATEMENT_SEPARATOR = 50;
public static final int ASSIGN = 100;
public static final int TERNARY_CMP = 200;
public static final int OR = 300;
public static final int AND = 400;
public static final int PIPE = 500; // |
public static final int COMPLEMENT = 600;
public static final int AMP = 700; // &
public static final int EQ = 800;
public static final int NEQ = EQ;
public static final int CMP = 900; // < <= > >=
public static final int LT = CMP;
public static final int LTE = CMP;
public static final int GT = CMP;
public static final int GTE = CMP;
public static final int SHIFT = 1000; // << >>
public static int PLUS = 1100;
public static int MINUS = PLUS;
public static int MUL = 1200;
public static int DIV = MUL;
public static int MOD = MUL;
public static final int POW = 1250;
public static final int COALESCE = MUL + 10; // ?? (1210)
public static final int NOT = 1300;
public static final int UNARY_PRE = 1300;
public static final int UNARY_POST = 1400;
public static final int PARS = 1600;
public static final int BRACKETS = PARS;
public static final int BRACES = PARS;
public static final int DOT = PARS;
public static final int STATEMENT_SEPARATOR = 50;
}
Tier ladder, high → low (binds tighter → binds looser):
| Constant | Value | Operator(s) |
|---|---|---|
PARS / BRACKETS / BRACES / DOT | 1600 | |
UNARY_POST | 1400 | postfix |
NOT / UNARY_PRE | 1300 | prefix - (negation), prefix |
POW | 1250 | |
COALESCE | 1210 | |
MUL / DIV / MOD | 1200 | (NExpr's built-in |
PLUS / MINUS | 1100 | |
SHIFT | 1000 | |
CMP (LT/LTE/GT/GTE) | 900 | |
EQ / NEQ | 800 | reserved tier for equality — |
AMP | 700 | bitwise |
COMPLEMENT | 600 | reserved (likely a future bitwise-complement operator) |
PIPE | 500 | bitwise |
AND | 400 | logical |
OR | 300 | logical |
TERNARY_CMP | 200 | reserved for an upcoming ternary |
ASSIGN | 100 | |
STATEMENT_SEPARATOR | 50 | |
Caveat — the declareBuiltins() excerpt vs. the full engine:
declareBuiltins() available for this doc registers & and | as aliases of the logical AND/OR operators (precedence tiers AND/OR), and registers ==/!=/% at the coarser CMP tier rather than at EQ/MOD. Separately, PIPE (bitwise |), AMP (bitwise &), SHIFT (<</>>), and COALESCE (??) are confirmed to be wired to their own distinct operators elsewhere in the full engine — i.e. NExpr distinguishes a logical &/| from a bitwise &/| (much like the difference is only visible through which tier the parser actually binds to, not through the symbol alone). Only TERNARY_CMP (200) is confirmed genuinely unused so far, reserved for a future ternary operator. COMPLEMENT (600) has no confirmed operator yet.Associativity, as registered in the shown declareBuiltins() excerpt:
| Category | Associativity |
|---|---|
Comparisons, | LEFT |
Assignment | RIGHT |
Prefix -, prefix | RIGHT |
Prefix | LEFT (as literally registered — asymmetric vs. prefix -/ |
Postfix | LEFT |
Parentheses ( ... ) are themselves represented as an NExprOperator/NExprNode named "(" whose single child is the grouped sub-expression (see test4, test5, test14 in ExprTest) — they are not stripped away during parsing. Their eval handler in declareBuiltins() deliberately throws ("unable to evaluate"); the actual grouping/indexing/block semantics are resolved by the parser itself, not by evaluating the (/[/{ node directly. The same is true for [ and {. If you ever see IllegalArgumentException("unable to evaluate"), it usually means something tried to eval() one of these structural marker nodes directly instead of walking its children.
5. Common operator identity — NExprCommonOp
NExprCommonOp is a portable, symbol-based enum used to identify "well-known" operator semantics independent of exactly how they were declared/named in a given context — useful when writing generic operator implementations that need to defer to a context's notion of, say, "the current +":
PLUS("+"), MINUS("-"), MUL("*"), DIV("/"), REM("%"), XOR("^"), POW("**"),
OR_BITS("|"), AND_BITS("&"), OR("||"), AND("&&"),
EQ("=="), NOT("!"), LT("<"), GT(">"), LTE("<="), GTE(">="), NE("!="),
DOT("."), ASSIGN("="), LIKE("=~"), EQ_REGEX("==~")
Each constant carries an image() (its canonical textual symbol) and supports lenient parse(String) (case/whitespace-insensitive, matching name, id, image, or declared aliases).
Contexts expose a way to resolve a type-specific implementation of a common operator without hardcoding the operator's declared name:
NOptional<NFunction2<A, B, ?>> findCommonInfixOp(NExprCommonOp op, Class<? extends A> firstType, Class<? extends B> secondType);
NOptional<NFunction<A, ?>> findCommonPrefixOp(NExprCommonOp op, Class<? extends A> argType);
NOptional<NFunction<A, ?>> findCommonPostfixOp(NExprCommonOp op, Class<? extends A> argType);
This is effectively an operator-overloading lookup: "give me the function that implements PLUS for (Integer, String)", etc. Note NExprCommonOp already distinguishes OR_BITS/AND_BITS (|/&) from OR/AND (||/&&) — consistent with §4's confirmation that logical and bitwise &/| are genuinely separate operators in the engine, not just precedence-tier aliases of each other.
6. Complete built-in operator table (declareBuiltins())
Constants also declared here (not operators, but part of the same call):
| Name | Value |
|---|---|
true | true |
false | false |
null | null |
Operators:
| Symbol(s) | Type | Precedence | Assoc. | Notes |
|---|---|---|---|---|
| INFIX | AND | LEFT | logical AND (see caveat in §4 re: distinct bitwise |
| INFIX | OR | LEFT | logical OR (see caveat re: distinct bitwise |
| INFIX | CMP | LEFT | comparisons |
| INFIX | CMP | LEFT | inequality, all aliasing NE |
| INFIX | PLUS | LEFT | addition, subtraction |
| INFIX | MUL | LEFT | multiplication, division |
| INFIX | CMP | LEFT | remainder — registered at comparison precedence, not MUL/MOD (see §4 caveat) |
| INFIX | OR | LEFT | XOR |
| INFIX | POW | LEFT | exponentiation |
| INFIX | DOT (PARS tier) | LEFT | member access — handler evaluates the left side then delegates to an internal |
- (unary) | PREFIX | NOT | RIGHT | unary negation |
| PREFIX | NOT | RIGHT | logical NOT |
| INFIX | ASSIGN | RIGHT | assignment; requires the LHS node to be |
| INFIX | ASSIGN | RIGHT | compound assignment; reads old value, evaluates RHS, delegates to |
| PREFIX | NOT | LEFT | pre-increment; adds |
| POSTFIX | NOT | LEFT | post-increment; same as above but returns the old value |
-- | PREFIX | NOT | LEFT | pre-decrement (mirrors |
-- | POSTFIX | NOT | LEFT | post-decrement, returns old value |
| INFIX | STATEMENT_SEPARATOR | LEFT | statement chaining; evaluates every operand in order and returns the last one's value — this is precisely what makes |
| POSTFIX | PARS | LEFT | grouping marker; |
| POSTFIX | BRACKETS (PARS tier) | LEFT | indexing marker; |
| POSTFIX | BRACES (PARS tier) | LEFT | block marker; |
For the full built-in function table (string, join, format*, isBlank, …) and math/physics constants and functions, see [NExpr-Evaluation.md](./NExpr-Evaluation.md).
7. Practical patterns
Operators are identified by
This lets(name, type), not just name.+mean different things as prefix vs. infix, and lets++/-- exist as both prefix and postfix simultaneously. Always pass the intended NExprOpType when doing manual getOperator/removeOperator lookups.Restrict, don't rebuild, for DSLs: start from
declareBuiltins()and prune down (§3) rather than hand-assembling an operator set from scratch — you get consistent precedence/associativity behavior for free for whatever subset you keep.Watch the
because%precedence gotcha:%shares CMP precedence rather than MUL, an expression likea % b > 0parses differently than a C-family programmer might expect at first glance — worth a comment in any expression that mixes%with comparisons.
5.3 Expression evaluation
NExpr — Evaluation
Part of the NExpr doc set. Covers how a parsed NExprNode actually gets executed: NExprCallContext, NExprCallHandler, NExprNodeValue, custom function/operator resolvers, the complete built-in function table, and two worked examples.
1. The evaluation pipeline
Parsing (context.parse(expr)) only produces an NExprNode; nothing runs until you call eval(...).
String expression
│ context.parse(expression)
▼
NOptional<NExprNode>
│ .get()
▼
NExprNode
│ node.eval(context)
▼
NOptional<Object>
│ .get()
▼
Object (the actual runtime result: Boolean, Number, String, custom object, …)
In NExpr call sites specifically, you'll mostly see:
| Call | Used for |
|---|---|
| Unwrap, throwing if absent/errored — the default when failure should propagate loudly (e.g. |
| Unwrap to null on absence/error — for optional/best-effort values (e.g. inside firstNonNull, join). |
| Combined with |
| Presence-only checks, e.g. the isNumber/isBoolean built-ins. |
2. NExprCallContext / NExprCallHandler
Every function, construct, and operator call is driven by these two interfaces:
public interface NExprCallContext {
String name();
List<NExprNodeValue> args();
NOptional<NExprNodeValue> arg(int index);
NExprContext context();
NExprCallContextType contextType(); // FUNCTION, CONSTRUCT, or OPERATOR
NExprOpType operatorType();
int operatorPrecedence();
NOperatorAssociativity operatorAssociativity();
}
@FunctionalInterface
public interface NExprCallHandler {
Object eval(NExprCallContext callContext);
}
NExprCallHandler is the single method you implement for every custom function, construct, or operator. NExprCallContext gives you everything about the current call site: its resolved name(), its (unevaluated!) args(), the context() to evaluate against, which of the three call kinds this is, and — for operators — the operator's own type/precedence/associativity (rarely needed inside a handler, but available for handlers that want to behave differently depending on how they were invoked).
3. NExprNodeValue
public interface NExprNodeValue extends NExprNode {
NExprNode node();
NOptional<Object> value();
}
NExprNodeValue
extends NExprNode
— every call argument is itself a full AST node, not a pre-computed value. It adds:node()— the underlying wrapped NExprNode (useful if you need to inspect structure, e.g. checkingnodeType() == WORDthe way the built-in=operator does — since NExprNodeValue inheritsnodeType()/children()/name()/eval()directly from NExprNode,node()mainly matters when you specifically want the unwrapped node rather than going through the value-wrapper's own inherited AST methods).value()— resolve straight to a value (NOptional<Object>), the "just give me the result" shortcut most functions use instead of calling.eval(context)themselves.
Because it's a full NExprNode, an NExprNodeValue also has its own eval(NExprContext context) (inherited), which is what lets an operator evaluate an argument against a different context than the one it was originally bound to, or defer evaluation entirely.
It's what context.bindLiteral(Object) and context.bindNode(NExprNode) produce, and what you pass back into context.evalInfixOperator/evalPrefixOperator/evalPostfixOperator(...) when a handler needs to delegate to another operator — e.g. how += computes its result:
Object newValue = context.evalInfixOperator(
"+",
context.bindLiteral(oldValue),
context.bindLiteral(partValue)
).get();
Two evaluation styles, and why both exist
Every built-in handler follows one of two patterns:
Value-only functions call
.value()on each argument and don't care whether/how it was computed — e.g. string, boolean, join,format*, isBlank.Structural/stateful operators call
.eval(context)themselves, deliberately, and often inspect the unevaluated node first — e.g.=checksargs.get(0).nodeType() == WORDbefore evaluating anything, so it can validate the left-hand side is an assignable variable name and resolve it viacontext.getVar(varName), rather than receiving an already-computed value it could no longer trace back to a variable. The same pattern shows up in++/-- and all compound assignments (+=etc.): read the old value first, compute the new one, then write it back — an order that's only possible because the operator controls evaluation timing itself instead of the engine eager-evaluating all arguments up front.
4. Custom function/operator resolution
Beyond declaring functions one at a time with declareFunction(NExprFunction.of(name, handler)), you can register a whole resolver that computes callables on demand:
@FunctionalInterface
public interface NExprFunctionResolver {
NOptional<NExprFunction> getFunction(String fctName, NExprNodeValue[] args, NExprContext context);
}
@FunctionalInterface
public interface NExprOperatorResolver {
NOptional<NExprOperator> getOperator(String opName, NExprOpType type, NExprNodeValue[] args, NExprContext context);
}
registered via NExprContextBuilder.declareFunctions(...) / .declareOperators(...) respectively (or .declareConstructs(...) for the construct namespace). There's also a composite NExprResolver that can back functions, constructs, operators, and variables from one object — see [NExpr.md §3.1](./NExpr.md).
4.1 Worked example — reflection-based function dispatch
The pattern used in production to expose an arbitrary Java object's methods as callable expression functions, gating a feature flag with a user-supplied onlyIf expression:
NExprMutableContext d = NExprContextBuilder.of()
.declareBuiltins()
.declareFunctions((fctName, args, context) -> {
Method m = TypeHelper.getDeclaredMethodRuntime(
targetClass, fctName,
Arrays.stream(args).map(x -> x == null ? null : x.getClass()).toArray(Class[]::new)
);
if (m == null) {
throw new IllegalArgumentException("not found method " + fctName + Arrays.asList(args));
}
m.setAccessible(true);
return NOptional.of(NExprFunction.of(fctName, callContext -> {
List<NExprNodeValue> callArgs = callContext.args();
try {
return m.invoke(targetInstance,
callArgs.stream().map(x -> x.value().ifErrorThrow().orNull()).toArray());
} catch (InvocationTargetException e) {
throw new RuntimeException(NException.getErrorMessage(e.getCause()), e.getCause());
} catch (IllegalAccessException e) {
throw new RuntimeException(NException.getErrorMessage(e), e);
}
}));
})
.buildMutable();
NExprNode parsed = d.parse(onlyIfExpression.trim()).get();
boolean enabled = (boolean) parsed.eval(d).get();
This is the "is this feature/command enabled" boolean-gating shape: a user-authored expression string is evaluated once per check, and any function call in it transparently dispatches to a real Java method via reflection.
4.2 Worked example — evaluating an antenna design constraint
A different shape of the same idea — not reflection dispatch, but a small, user-editable formula/constraint evaluated against physical constants and runtime design variables, closer to the kind of MoM/antenna tooling in Hadruwaves:
NExprMutableContext ctx = NExprContextBuilder.of()
.declareBuiltins()
.declareMathFunctions()
.declarePhysicsConstants()
.buildMutable();
ctx.declareVar("f"); // operating frequency (Hz)
ctx.declareVar("epsilonR"); // substrate relative permittivity
ctx.declareVar("W"); // patch width (m)
ctx.setVarValue("f", 2.4e9);
ctx.setVarValue("epsilonR", 4.4);
ctx.setVarValue("W", 0.0286);
// A constraint a user could type into a config field:
// "is the free-space wavelength at f at least 5x the patch width?"
NExprNode constraint = ctx.parse("(C / f) >= 5 * W").get();
boolean ok = (boolean) constraint.eval(ctx).get();
// A derived quantity using a physics constant directly:
NExprNode lambdaExpr = ctx.parse("C / f").get();
double lambda = ((Number) lambdaExpr.eval(ctx).get()).doubleValue();
The general recipe: build a context once with
declareBuiltins()
declareMathFunctions() + declarePhysicsConstants(), expose a handful of declareVars for the caller's actual inputs, then repeatedly parse-and-eval user- or config-supplied formula strings against it. Useful for design-rule checks, validation rules, or computed defaults — anywhere you want end users to author small formulas without embedding a full scripting language.
5. Complete built-in function table (declareBuiltins())
| Name | Aliases | Behavior |
|---|---|---|
| — | Converts x to String via |
| — | Converts x to Boolean |
| — | Converts x to Double |
| — | Converts x to Long |
| — | Converts x to Integer |
| — | Converts x to Float |
| — | true if x parses as a number |
| — | true if x parses as a boolean |
| — | true if x is blank per |
| — | Returns the first non-null argument |
| — | Returns the first non-blank argument |
| formatC | |
| — | |
| — | |
| — | Joins an Iterable with sep; falls back to |
Math functions (declareMathFunctions())
All single- or double-argument wrappers around java.lang.Math, with arguments coerced via NLiteral.of(...).asDouble():
sin, cos, tan, sinh, cosh, tanh, asin, acos, atan2(y, x), toRadians, toDegrees, exp, log, log10, sqrt, cbrt, abs, signum, ulp, ceil, floor, rint, round, pow(x, y), max(a, b), min(a, b).
Math constants (declareMathConstants())
| Name | Value |
|---|---|
pi, PI, | |
E | |
Physics constants (declarePhysicsConstants())
| Name | Value | Meaning |
|---|---|---|
C | 299792458.0 | speed of light (m/s) |
| 8.8541878128×10⁻¹² | vacuum permittivity (F/m) |
| 1.25663706212×10⁻⁶ | vacuum permeability (H/m) |
| 376.730313668 | free-space impedance (Ω), = μ0·C |
e | 1.602176634×10⁻¹⁹ | elementary charge (C) |
h | 6.62607015×10⁻³⁴ | Planck constant (J·s) |
| 1.054571817×10⁻³⁴ | reduced Planck constant (J·s) |
kB | 1.380649×10⁻²³ | Boltzmann constant (J/K) |
NA | 6.02214076×10²³ | Avogadro constant (mol⁻¹) |
me | 9.1093837015×10⁻³¹ | electron mass (kg) |
mp | 1.67262192369×10⁻²⁷ | proton mass (kg) |
G | 6.67430×10⁻¹¹ | gravitational constant (m³·kg⁻¹·s⁻²) |
g | 9.80665 | standard gravity (m/s²) |
R | 8.314462618 | ideal gas constant (J/mol/K) |
| 5.670374419×10⁻⁸ | Stefan–Boltzmann constant (W/m²/K⁴) |
Note the constants use their proper physics symbols (ε0, μ0, η0, ħ, σ, π) rather than ASCII transliterations — if you want ASCII-friendly aliases (epsilon0, mu0, eta0, hbar, sigma) for keyboard-friendly formula entry, declare them yourself as additional NExprVar.ofConst(...) bindings alongside declarePhysicsConstants().
5.4 Expression Templating
NExpr — Templating
Part of the NExpr doc set. Covers NExprTemplate: its built-in styles, the Moustache directive set, $-string interpolation, and how the templating layer is meant to be embedded inside other host languages/formats.
1. What NExprTemplate is
NExprTemplate is a text-templating layer built directly on top of the expression engine — every { {...}} (or equivalent) block is just an NExpr expression/statement, evaluated against the same kind of NExprContext used everywhere else in NExpr. That means anything you can parse()/eval() as a plain expression — function calls, operators, if/else, variable access — is available inside a template block, with no separate templating-specific expression grammar to learn.
Obtained from a context via context.ofTemplate(), then configured with a syntax style, and driven with processString(String):
NExprTemplate tpl = context.ofTemplate().withMoustacheStyle();
String out = tpl.processString(someTemplateText);
2. Built-in styles
NExpr ships more than one delimiter convention, so the same underlying directive language can be dropped into different kinds of host documents without looking out of place:
Moustache style —
{ { }}delimiters, in the spirit of Mustache/Handlebars-family templating. This is the style demonstrated in TemplateTest and in the quick-start example below.JSP-style — a delimiter convention in the spirit of JSP/EL-style templating (
<% %>/${ }-flavored), for embedding into contexts where that's the more natural or expected marker syntax (e.g. HTML-adjacent templates, or teams already used to JSP-family tag delimiters).Custom delimiters — because the directive language is decoupled from the delimiter syntax, NExpr's templating is extensible with your own prefix/suffix markers, letting you embed the same
{ {:if}}/{ {:for}}/interpolation semantics inside essentially any host language or file format (config files, other DSLs, code-generation templates, …) simply by choosing marker characters that don't collide with that host format's own syntax — without having to reimplement the underlying if/for/expression evaluation logic each time.
The exact method names/signatures for selecting the JSP-style preset and for supplying fully custom prefix/suffix delimiters weren't part of the sources reviewed for this doc set — only .withMoustacheStyle() was directly evidenced. If you can share that part of the NExprTemplate API, this section can be filled in with exact usage.
3. Moustache-style directives
| Syntax | Meaning |
|---|---|
| Execute a statement, emit nothing |
| Evaluate an expression and emit its result |
| Loop over an iterable expression, binding varName each iteration |
| Same, also binding a loop index |
| Conditional block, with any number of |
Since the expression inside every block is a normal NExpr expression, anything declared on the underlying context — custom functions, operators, physics/math constants, whatever your app registers — is usable directly inside { { }}.
3.1 Worked example
Map<String, Object> vars = new HashMap<>();
vars.put("world", "Earth");
vars.put("yellow", true);
vars.put("blue", true);
vars.put("my", true);
String out = NExprContextBuilder.of()
.declareBuiltins()
.declareVars(NExprVarResolver.ofMap(vars))
.build()
.ofTemplate().withJspStyle()
.processString("hello <%:if yellow %> <%world%> <%:else if blue %> my <%:else%> World <%:end%>");
// yellow=true -> "hello Earth "
// yellow=false, blue=true -> "hello my "
Map<String, Object> vars = new HashMap<>();
vars.put("world", "Earth");
vars.put("yellow", true);
vars.put("blue", true);
vars.put("my", true);
String out = NExprContextBuilder.of()
.declareBuiltins()
.declareVars(NExprVarResolver.ofMap(vars))
.build()
.ofTemplate().withMoustacheStyle()
.processString("hello { {:if yellow }} { {world}} { {:else if blue }} my { {:else}} World { {:end}}");
// yellow=true -> "hello Earth "
// yellow=false, blue=true -> "hello my "
4. $-style string interpolation
Independent of the Moustache templating layer, the expression parser itself understands dollar-interpolated string literals inline in any expression — not just inside a template:
expr.parse("$'something for $v'"); // parses to an NExprInterpolatedStringNode
which evaluates to a string with $v substituted by the current value of variable v. The context also exposes this programmatically, without going through parse(...):
NExprInterpolatedStringNode ofDollarInterpolatedString(String a);
NExprInterpolatedStringNode ofMoustacheInterpolatedString(String a);
Note there are two distinct interpolation flavors available at the node level — a dollar-prefixed quoted-string flavor ($'...', $"...", $... , driven by the tokenizer's TT_ISTR_SQ/TT_ISTR_DQ/TT_ISTR_AQ token types — see [NExpr-Tokenizer.md](./NExpr-Tokenizer.md)) and a Moustache-flavored one (ofMoustacheInterpolatedString), which is presumably what backs { {expression}} substitution inside a template body itself.
5. Design takeaway: templating as a thin skin over the expression engine
Because directive semantics (if/for/expression-eval) live in the shared expression engine and only the delimiter syntax varies by style, NExpr's templating isn't really a separate feature from the rest of NExpr — it's the same context, the same operators/functions/constants, the same parser, wrapped with a marker-recognition pass. Practically, this means:
Any restriction you apply to a context for security/DSL reasons (see [
NExpr-Operators.md §3 removing/restricting](./NExpr-Operators.md)) applies equally whether that context is used for plainparse()/eval()or forofTemplate()— there's no separate "template sandbox" to configure.Custom functions/operators/constants declared once are usable both as plain expressions and inside template blocks, with no re-registration needed.
- Choosing (or defining) a delimiter style is purely about not colliding with the host document's own syntax — it doesn't change what the templating layer is capable of expressing.
5.5 Expression Tokenization
NExpr — Low-Level Tokenization (NStreamTokenizer)
Part of the NExpr doc set. Covers NStreamTokenizer, the lexer backing NExpr's parser, and why it exists as a separate class instead of reusing java.io.StreamTokenizer.
NStreamTokenizer is usable standalone for custom lexing needs, independent of the rest of NExpr.
NStreamTokenizer st = new NStreamTokenizer(new StringReader("8.0.0"));
st.xmlComments(true);
st.parseNumbers(false);
st.wordChars('0', '9');
st.wordChars('.', '.');
st.wordChars('-', '-');
int tokenType;
while ((tokenType = st.nextToken()) != NToken.TT_EOF) {
NOut.println(st.image);
}
1. Why not java.io.StreamTokenizer?
NStreamTokenizer is deliberately modeled on java.io.StreamTokenizer's API shape (nextToken(), ttype, sval, pushBack(), wordChars(...), whitespaceChars(...), quoteChar(...), ordinaryChar(...), commentChar(...), lineno() — all present, doc-comment style and all) but reimplements it from scratch to lift several hard limitations of the JDK class that matter for parsing an expression language rather than a generic config/data format:
Limitation in | How NStreamTokenizer differs |
|---|---|
Numeric tokens are always coerced into a single | nval is a Number, and the tokenizer chooses the narrowest precise Java numeric type for each literal: Integer → Long → BigInteger for integral literals (TT_INT/TT_LONG/TT_BIG_INT), and Float → Double → BigDecimal for fractional literals (TT_FLOAT/TT_DOUBLE/TT_BIG_DECIMAL), falling through to the next-wider type only if parsing at the narrower type fails. This matters for an expression language where literal precision should be preserved through evaluation (e.g. large integer IDs, or exact decimal literals for financial/scientific values) rather than silently rounded through double. |
No notion of an "operator" token at all — every non-word, non-number, non-quote, non-comment character is returned one at a time as its own single-character ttype. | NStreamTokenizer has a dedicated |
Only two configurable comment styles (slashSlashComments, slashStarComments), always silently discarded — no XML comments, no shell-style | Dedicated handling for four comment families — C |
Whitespace is always silently skipped between tokens (only newlines can be made "significant" via eolIsSignificant). | returnSpaces lets all whitespace runs be returned as their own TT_SPACE token carrying the exact whitespace text via image — letting a consumer reconstruct original formatting/spacing exactly, which test17 in ExprTest relies on directly ( |
| One fixed pair of quote characters can be configured, and there's no interpolated-string concept — a quoted string is always just a plain string. | Three quote characters are pre-registered by default ( |
No concept of a range/ellipsis token — | NStreamTokenizer's number reader explicitly looks ahead for a second |
| The token-type/flag surface is fixed — you can't register new "kinds" of parsable content without subclassing. | |
| Extended/accented Latin characters (128–255) aren't word-constituent by default. | The private no-arg constructor pre-registers |
2. Key configuration methods
| Method | Effect |
|---|---|
| Mark a character range (or single char) as word-constituent. |
| Mark a range as whitespace-only (clears any other attribute). |
| Strip all special meaning from a range/character — returned as single-char tokens. |
| Mark a character as starting a to-end-of-line comment. |
| Mark a character as a string delimiter. |
| Toggle numeric-literal recognition for 0-9, |
| Toggle whether line breaks are returned as TT_EOL tokens vs. treated as plain whitespace. |
| Toggle C-style |
| Toggle XML |
| Convenience preset: |
| Convenience preset: |
| Force word tokens to lowercase in sval (raw case preserved in image). |
| Generic enable/disable and query for extensible token kinds (comments, interpolated strings, …), keyed by NToken constants. |
| Un-consume the current token so the next |
| Peek whether another character is available without consuming a token. |
3. Fields populated after nextToken()
| Field | Meaning |
|---|---|
ttype | The token type just read — either one of the |
image | The exact raw source text of the token — including original whitespace/case/escaping as written, unlike sval which may be normalized (e.g. lowercased, or with escape sequences already resolved). |
sval | The token's string value — the word text for TT_WORD, or the decoded body for quoted-string tokens (escape sequences like |
nval | The token's numeric value as a Number, for any of the TT_INT/TT_LONG/TT_BIG_INT/TT_FLOAT/TT_DOUBLE/TT_BIG_DECIMAL types. |
4. Notable tokenizing behaviors (verified by ExprTest)
With
parseNumbers(true)and returnSpaces on, whitespace is preserved as its own token image (test17:"1 .. 3"→ tokens["1", " ", "..", " ", "3"]), letting a consumer reconstruct spacing exactly.".."is recognized as a distinct range-like operator token, separate from decimal points, even directly adjacent to digits with no surrounding spaces (test18:"1..3"→["1", "..", "3"]).Signed numbers are tokenized as a single signed literal, and
..still splits correctly around them (test19:"-1..-3"→["-1", "..", "-3"]).Multi-character operator runs like
"<<"are recognized as one TT_OP token, not two single-character tokens (testTokenize2).Word-char ranges are reconfigurable per instance (
wordChars('0','9')+wordChars('.', '.')+wordChars('-', '-')withparseNumbers(false)), which is how a version-string-like token such as"8.0.0"can be tokenized as one plain word rather than being (mis)parsed as a number.
NToken.TT_EOF is the end-of-stream sentinel returned by nextToken(); NToken also defines the other constants referenced throughout this doc (TT_WORD, TT_INT, TT_LONG, TT_BIG_INT, TT_FLOAT, TT_DOUBLE, TT_BIG_DECIMAL, TT_OP, TT_EOL, TT_NOTHING, TT_COMMENTS, TT_COMMENT_LINE_C, TT_COMMENT_MULTILINE_C, TT_COMMENT_MULTILINE_XML, TT_COMMENT_LINE_SH, TT_ISTR_SQ, TT_ISTR_DQ, TT_ISTR_AQ).
6 Text Rendering & Models
At the heart of Nuts' text rendering capabilities lies NTF (Nuts Text Format) — a structured, styled, and composable text model. NTF defines how text content, styles, and layout are represented, enabling you to build complex output (with colors, emphasis, alignment, structure, and more) in a way that is device-agnostic and output-agnostic.
The NText API represents these styled text blocks, and NTextStyle describes how they appear (e.g., bold, italic, success, error). Because every renderer produces NText, different outputs — from simple styled strings to complex ASCII tables or banners — can be nested, combined, and transformed consistently.
Once generated, NTF text can be rendered into multiple output formats:
- ✅ ANSI sequences – On POSIX-compatible terminals, NText is rendered as styled ANSI text. On Windows, Nuts uses Jansi to ensure the same result.
- 🌐 HTML – NText can also be rendered as HTML, enabling use in web pages or documentation. Tools like NSite (a static site generator built on Nuts/NAF) rely on this feature to transform NText blocks into HTML markup.
- 📦 Other targets – Because NTF is an abstract representation, it can be serialized to other formats or even stored as structured data for later rendering.
Building on this foundation, NTextArt offers a versatile rendering framework that converts structured data, text, and even images into expressive visual representations. It provides specialized renderers for different visualization needs:
- Text Renderers – Convert text into stylized ASCII art (e.g., figlets, banners).
- Structured Renderers – Render structured data such as tables and trees.
- Image Renderers – Transform raster images into ASCII-based art.
Because all renderers output NText (based on NTF), their results can be further styled, composed with other messages, serialized, or rendered to different backends — all while remaining portable across platforms.
6.1 Nuts Text Format
Nuts Text Format (NTF)
The Nuts Text Format (NTF) is a lightweight, expressive markup language designed specifically to enhance command-line interface (CLI) output with rich, portable, and visually appealing formatting. It provides a powerful yet simple syntax that lets developers create colorful, structured, and semantically meaningful text that works seamlessly across different terminal environments and beyond.
The Need for NTF
Traditional terminal output often relies on plain text or embedded ANSI escape codes to achieve colors and styles. This approach has several drawbacks:
Lack of readability: Raw escape sequences are hard to read and maintain within source code.
Poor portability: Different terminals support different levels of ANSI or control codes, leading to inconsistent rendering.
Limited structure: Plain text and raw ANSI codes do not express document structure well (like lists, tables, sections).
On the other hand, existing markup languages each have their own limitations for CLI contexts:
HTML is rich and flexible but too verbose and requires an HTML viewer, unsuitable for terminals.
Markdown is easy to write and readable but lacks support for dynamic styling and rich terminal features.
Man pages (troff/groff) provide basic terminal help formatting but are complex to author and limited in styling options.
What Makes NTF Unique?
NTF is crafted to fill this gap by being a terminal-first markup format that is both human-readable and machine-processable, with several key advantages:
Readable markup: NTF syntax uses simple inline markers for colors, font styles, lists, tables, and sections, which are easy to write and understand.
Rich formatting: Supports foreground and background colors, bold/italic/underline, code blocks, bullet and numbered lists, tables, links, and other structured elements.
Portability: NTF content is independent of the terminal's escape code specifics. Instead, it is parsed and translated to the appropriate ANSI sequences or other target formats at runtime.
Multi-target rendering: Beyond ANSI terminals, NTF can be converted to Markdown (for documentation or developer notes) and HTML (for web-based manuals), ensuring a unified authoring experience.
Context-awareness: NTF rendering adapts automatically to the capabilities of the target output device or session configuration, allowing graceful degradation when color or style is not supported.
Easy toggling: Users can enable or disable colored output through standard Nuts options or programmatically, without affecting the underlying markup.
NTF in Nuts Ecosystem
Within the Nuts toolbox, NTF plays a central role in delivering a consistent, high-quality user experience:
Command-line help system: All command help, options, examples, and warnings in Nuts are authored in NTF. This allows help to be:
Colorful and well-structured on capable terminals.
Plain-text friendly when color is disabled.
Automatically exportable to Markdown or HTML for documentation portals.
Output formatting: When printing messages, lists, objects, or errors, Nuts can utilize NTF to add semantic structure and emphasis, improving clarity and user comprehension.
Unified authoring: Developers write output messages once in NTF and can be confident it will render correctly across all supported environments without manual adjustments.
How NTF compares to other formats:
NTF is specifically designed for developer-friendly, portable, terminal-first output formatting. It bridges the gap between simple text styling (like ANSI escape codes) and more advanced document-oriented formats (like Markdown or AsciiDoctor), making it uniquely suited for CLI applications.
The table below highlights how NTF compares to other common formats across key capabilities:
| Feature | NTF | ANSI Escape Codes | Markdown | AsciiDoctor | HTML |
|---|---|---|---|---|---|
Colored output | ✅ Named colors, indexed and hex support | ✅ Manual, code-based | ❌ (Extensions needed) | ✅ With roles/styles | ✅ CSS/inline styles |
Styled text | ✅ Bold, italic, underline, strikethrough | ✅ Limited (manual control) | ✅ Bold, italic | ✅ Bold, italic, underline | ✅ Full style control |
Semantic color tags (e.g. error, warning) | ✅ Built-in mappings ( | ❌ None | ❌ None | ⚠️ Manual via roles | ✅ Possible via class |
Nested/Combined styles | ✅ Fully supported (e.g. | ❌ Complex / fragile | ❌ Not supported | ✅ Supported | ✅ Fully supported |
Structured sections (titles, subtitles) | ✅ NTF supports semantic headers ( | ❌ None | ✅ Basic headings | ✅ Full document structure | ✅ Rich document structure |
Lists (bullet, numbered) | ❌ Not yet | ❌ None | ✅ Yes | ✅ Yes | ✅ Yes |
Tables | ❌ Not yet | ❌ None | ✅ Basic tables | ✅ Rich tables | ✅ Rich tables |
Syntax highlighting (code snippets) | ✅ With language tag | ❌ None | ✅ (Limited, via extensions) | ✅ With language tag | ✅ Full, with JS/CSS |
Terminal rendering support | ✅ Auto-adapts (ANSI, plain, HTML, Markdown) | ✅ Terminal only | ❌ Not terminal-targeted | ❌ Not terminal-targeted | ❌ Not terminal-targeted |
Portability across environments | ✅ Designed for CLI and convertible to HTML/Markdown | ❌ Terminal only | ✅ Editor/docs only | ✅ Editor/docs only | ✅ Web/browser only |
Ease of authoring for CLI output | ✅ Very high (compact, readable, intuitive) | ❌ Low (escape-heavy) | ⚠️ Limited styling | ⚠️ Verbose | ❌ Too verbose for CLI |
Summary
The Nuts Text Format is a modern, terminal-optimized markup language that:
- Improves readability and maintainability of CLI output markup.
- Enables richly formatted, colorful, and structured terminal output.
- Supports conversion to Markdown and HTML for seamless documentation.
- Enhances the Nuts ecosystem by unifying CLI and documentation presentation.
NTF represents a thoughtful balance between simplicity, expressiveness, and portability, empowering Nuts users and developers to build sophisticated, professional command-line applications with minimal effort.
nuts comes up with a simple coloring syntax that helps writing better looking portable command line programs. standard output is automatically configured to accept the "Nuts Text Format" (NTF) syntax. Though it remains possible to disable this ability using the --!color standard option (or programmatically, see nuts API documentation). NTF will be translated to the underlying terminal implementation using ANSI escape code on linux/windows terminals if available.
Here after a showcase of available NTF syntax.




6.2 Rendering Text
Bring your CLI, logs, or console output to life with NTextArt. Render text as classic ASCII banners, pixel-style visuals, or even image-like representations, with multiple renderers at your fingertips — all workspace-aware and fully embeddable.
Basic Usage
NTextArt art = NTextArt.of();
NText text = NText.of("hello world");
NOut.println(art.getTextRenderer("figlet:standard").get().render(text));
Output:
_ _ _ _ _
| | | | | | | | | |
| |__ ___ | | | | ___ __ __ ___ _ __ | | __| |
| '_ \ / _ \ | | | | / _ \ \ \ /\ / / / _ \ | '__| | | / _` |
| | | | | __/ | | | | | (_) | \ V V / | (_) | | | | | | (_| |
|_| |_| \___| |_| |_| \___/ \_/\_/ \___/ |_| |_| \__,_|
You can choose from multiple built-in figlet renderers or even use your own.
NTextArt art = NTextArt.of();
NOut.println(art.getImageRenderer("pixel:standard").get()
.fontSize(20) .outputColumns(60) .render(text));
Output:
█ ░██ ███ ███ █
█ █ ▓█ ░█ █
█ █▒ ░██ █ ▓█ █▒ █ █ █▓ ██░█░ ░█ ▒█ █
█▓▓█░░█ █░ █ ▓█ ░█ ▓▒ █ █ █░█ ▓▓ ██▓▓ ░█ █░▒█
█ █░█████ █ ▓█ ▓█ ▒█ █▓█ ▓▒█ ░█ ██ ░█ ░█ █
█ █░ █ ██ ▓█ █ █░ ██░█▓ █ █▒ ██ ░█░ ░█░██
▓ ▓░ ░▓▓ ▓▓ ▓▓░ ▓░ ░▓ ▓ ▓▒ ▒▒ ░▓▓ ░▓ ▓
6.3 Rendering Tables
One of the most powerful text rendering features in NAF is its ability to render structured tables directly in the terminal. This is made possible through the NTextArt API, which can render tabular data with automatic alignment, wrapping, spanning, and per-cell styling — all while remaining fully compatible with Nuts’ messaging and text system (NText, NMsg, NOut, etc.).
Unlike ad-hoc printf-based formatting, NTextArt tables are aware of structure, style, and layout. They handle complex cases such as multiline cells, column and row spanning, per-cell styling, and even semantic rendering (e.g., italic, success, error) without losing readability.
Basic Usage
To render a table, create an NMutableTableModel using NTableModel.of(), populate it with rows and optional headers, and pass it to an NTextArtTableRenderer.
NMutableTableModel table = NTableModel.of()
.addHeaderRow(NTableCell.of(NText.of("Name")), NTableCell.of(NText.of("Status")))
.addRow(NTableCell.of(NText.of("adam")), NTableCell.of(NText.ofStyled("active", NTextStyle.italic())))
.addRow(NTableCell.of(NText.of("eve")), NTableCell.of(NText.ofStyled("inactive", NTextStyle.success())));
NOut.println(NTextArt.of().tableRenderer().get().render(table));
Output:
+------+----------+
| Name | Status |
+------+----------+
| adam | active |
| eve | inactive |
+------+----------+
You can choose from multiple built-in renderers (e.g. "table:ascii", "table:spaces") or register your own custom renderer.
Advanced Features
Multiline Cells
Cells can contain multiple lines of text, and the renderer automatically adjusts row heights:
NMutableTableModel table = NTableModel.of()
.addRow(NTableCell.of(NText.of("adam\nwas\nhere")), NTableCell.of(NText.of("active")))
.addRow(NTableCell.of(NText.of("eve")), NTableCell.of(NText.of("inactive")));
Output:
+------+----------+
| adam | active |
| was | |
| here | |
+------+----------+
| eve | inactive |
+------+----------+
Column Spanning (colspan)
Cells can span across multiple columns. You can define this directly using NTableCell.of(content, colspan, rowspan) or via NTableCellBuilder:
// Using factory method: (colspan = 2, rowspan = 1)
NMutableTableModel table = NTableModel.of()
.addRow(NTableCell.of(NText.of("adam\nwas\nhere"), 2, 1))
.addRow(NTableCell.of(NText.of("adam\nhere")), NTableCell.of(NText.of("adam\nis\nhere")));
// Using the builder:
NMutableTableModel sameTable = NTableModel.of()
.addRow(
NTableCellBuilder.of(NText.of("adam\nwas\nhere"))
.colspan(2)
.horizontalAlign(NPositionType.FIRST)
.build()
)
.addRow(NTableCell.of(NText.of("adam\nhere")), NTableCell.of(NText.of("adam\nis\nhere")));
Result:
+------------------------+
| adam |
| was |
| here |
+------------+-----------+
| adam | adam |
| here | is here |
+------------+-----------+
Row Spanning (rowspan)
Cells can also span vertically across multiple rows:
// Using factory method: (colspan = 1, rowspan = 2)
NMutableTableModel table = NTableModel.of()
.addRow(NTableCell.of(NText.of("tall\ncell\nvery\ntall"), 1, 2), NTableCell.of(NText.of("short")))
.addRow(NTableCell.of(NText.of("another")));
// Using existing cell builder:
NMutableTableModel sameTable = NTableModel.of()
.addRow(
NTableCell.of(NText.of("tall\ncell\nvery\ntall"))
.builder()
.rowspan(2)
.horizontalAlign(NPositionType.FIRST)
.build(),
NTableCell.of(NText.of("short"))
)
.addRow(NTableCell.of(NText.of("another")));
Mixed Column Counts
Rows can have variable numbers of cells to align with spanning:
NMutableTableModel table = NTableModel.of()
.addRow(NTableCell.of(NText.of("adam\nwas\nhere"), 3, 1))
.addRow(
NTableCell.of(NText.of("adam\nhere")),
NTableCell.of(NText.of("adam\nis\nhere")),
NTableCell.of(NText.of("3"))
);
Per-Cell Styling & Alignment
Cells carry both style definitions via NTextStyle and alignment configurations via NPositionType:
NTableCell styledCell = NTableCellBuilder.of(NText.ofStyled("warning", NTextStyle.warn()))
.horizontalAlign(NPositionType.CENTER)
.verticalAlign(NPositionType.CENTER)
.build();
NMutableTableModel table = NTableModel.of()
.addRow(NTableCell.of(NText.of("status")), styledCell);
Multiple Renderers
Different renderers can be used for different table aesthetics or output contexts:
NTextArt art = NTextArt.of();
// Using space padding instead of ASCII borders
NOut.println(art.tableRenderer("table:spaces").get().render(table));
You can also iterate over all registered renderers:
for (NTextArtTableRenderer renderer : art.tableRenderers()) {
NOut.println(renderer.getName() + "::");
NOut.println(renderer.render(table));
}
Performance Considerations
Rendering tables is efficient, but when dealing with thousands of rows, consider paginating or streaming rows instead of rendering all at once.
Cell layout calculations (especially with spanning) are cached internally to minimize overhead.
Why NTextArt Tables Matter
Because NTextArt tables are semantic and structure-aware:
- They understand cell spanning, multiline content, and style.
- They integrate seamlessly with NText, NMsg, and NOut.
- They’re renderer-agnostic — the same model can be rendered as ASCII, space-aligned text, or even graphical pixel art in the future.
- They form a foundation for higher-level features like search results, dependency trees, and diagnostics in Nuts CLI.
6.4 Rendering Trees
For hierarchical data like dependency graphs, process hierarchies, or nested objects, NAF provides tree rendering through the NTextArt API.
Basic Usage
To render a tree, construct a hierarchy of NTreeNode instances and render them using NTextArtTreeRenderer:
NTreeNode root = NTreeNode.of(NText.of("Root"),
NTreeNode.of(NText.of("Child 1")),
NTreeNode.of(NText.of("Child 2"),
NTreeNode.of(NText.of("Grandchild A")),
NTreeNode.of(NText.of("Grandchild B"))
)
);
NOut.println(NTextArt.of().treeRenderer().get().render(root));
Output:
Root
├─ Child 1
└─ Child 2
├─ Grandchild A
└─ Grandchild B
Configuring the Renderer
You can customize rendering behavior directly on the NTextArtTreeRenderer instance, such as hiding the root element or selecting specific renderers:
NTextArt art = NTextArt.of();
// Hide the root node
NText result = art.treeRenderer().get()
.omitRoot(true)
.render(root);
// Or load a named renderer
NTextArtTreeRenderer customRenderer = NTextArtTreeRenderer.of("tree:compact");
NOut.println(customRenderer.render(root));
Custom Node Models & Nested Components
NTreeNode is an interface with two core methods: content() and children(). Because content() returns an NText, you can embed complex rich text or even rendered components (like NTableModel) inside tree nodes:
static class TableNode implements NTreeNode {
private final int value;
private final NTextArt art;
public TableNode(int value, NTextArt art) {
this.value = value;
this.art = art;
}
@Override
public NText content() {
return art.tableRenderer().get().render(
NTableModel.of().addRow(NTableCell.of(NText.of(value)))
);
}
@Override
public List<NTreeNode> children() {
return (value < 3)
? Arrays.asList(value + 1, value + 2).stream()
.map(v -> new TableNode(v, art))
.collect(Collectors.toList())
: Collections.emptyList();
}
}
NTextArt art = NTextArt.of();
NTreeNode tree = new TableNode(1, art);
NOut.println(art.treeRenderer().get().render(tree));
Output:
╭─╮
│1│
╰─╯
├── ╭─╮
│ │2│
│ ╰─╯
│ ├── ╭─╮
│ │ │3│
│ │ ╰─╯
│ └── ╭─╮
│ │4│
│ ╰─╯
└── ╭─╮
│3│
╰─╯
Anonymous Nodes
Nodes can carry blank content (via NText.ofBlank()), allowing you to structure groupings without introducing extra label text:
NTreeNode tree = NTreeNode.of(NText.ofBlank(),
NTreeNode.of(NText.of("siblings"),
NTreeNode.of(NText.ofBlank(),
NTreeNode.of(NText.of("id=1")),
NTreeNode.of(NText.of("label=first"))
),
NTreeNode.of(NText.ofBlank(),
NTreeNode.of(NText.of("id=2")),
NTreeNode.of(NText.of("label=second"))
)
)
);
Rendering Hierarchical Objects
You can also serialize structured objects directly to tree format using NObjectObjectWriter and NContentType.TREE:
Map<String, Object> map = NMaps.of(
"a", 2,
"b", NMaps.of("c", new Object[]{ NMaps.of("e", 3), NMaps.of("e", 3), 3 }, "d", 3),
"d", NMaps.of("e", 3)
);
NObjectObjectWriter.of()
.outputFormat(NContentType.TREE)
.println(map, NOut.get());
7 ANSI Theme Customization
Customizing ANSI Themes
Overview
Nuts provides a powerful theming system for ANSI and NTF (Nuts Text Format) formatted output. Themes map semantic text styles (such as PRIMARY, KEYWORD, ERROR, WARN, INFO, PATH, etc.) to specific terminal colors, supporting 16-color ANSI, 256-color palettes, and 24-bit RGB true-colors.
The --theme option and the NTextTheme API support specifying theme parameters by theme name (for built-in or cached themes) or by file path / URL (for custom .ntf-theme files).
Built-in Themes & Default Names
Nuts includes several built-in themes available on the classpath (META-INF/ntf-themes/). You can reference them directly by name:
default – OS-dependent default theme (grass on Windows, standard theme on Unix/Linux).
ansi – Basic 16-color ANSI palette theme.
grass – Green/nature-toned palette, optimized for Windows terminals.
horizon – Dark blue horizon theme, default on Unix/Linux.
whiteboard – Light background theme using 24-bit true colors.
When no theme name or path is provided (or when set to default), Nuts automatically selects the appropriate default theme for the running operating system environment.
Setting Themes at Boot or Runtime
Via Command Line Option (--theme)
The --theme CLI option accepts either a built-in theme name or a file path/URL to a custom theme file.
1. By Theme Name
Pass one of the default theme names (default, ansi, grass, horizon, whiteboard):
nuts --theme=horizon
2. By File Path or URL
Pass a file path (relative or absolute) or URL to a .ntf-theme file:
nuts --theme=/path/to/my-theme.ntf-theme
Via Java API
The NTextTheme.of(String nameOrPath) factory method resolves themes seamlessly:
Simple Name: Loads built-in theme resources from
classpath:/META-INF/ntf-themes/<name>.ntf-themeor user themes from~/.config/nuts/.../themes/<name>. Themes loaded by name are cached.File Path or URL: Loads the theme from the specified filesystem path or URL via NPath.
Null or Blank: Loads the default theme configured for the workspace/OS environment.
Example Usage
import net.thevpc.nuts.text.NTextTheme;
import net.thevpc.nuts.io.NPath;
// Load a theme by built-in name
NTextTheme themeByName = NTextTheme.of("horizon").orNull();
if (themeByName != null) {
NTextTheme.set(themeByName);
}
// Load a theme by file path
NTextTheme themeByPath = NTextTheme.of("/path/to/my-theme.ntf-theme").orNull();
if (themeByPath != null) {
NTextTheme.set(themeByPath);
}
// Using NPath explicitly
NTextTheme themeFromNPath = NTextTheme.of(NPath.of("/path/to/my-theme.ntf-theme")).orNull();
Defining Your Own Theme
Themes are defined in .ntf-theme property files. A theme file consists of key-value pairs defining: 1. Optional theme metadata (e.g. theme-name=my-theme). 2. Optional custom color/palette variables (e.g., MY_BLUE=4, DARK_RED=#670000). 3. Mapping rules for semantic token styles.
Syntax & Format
# example.ntf-theme
theme-name=my-theme
# Palette variables (ANSI numbers 0-255 or 24-bit hex colors)
DARK_BLUE=4
BRIGHT_BLUE=12
DARK_SKY=6
DARK_RED=#670000
# Primary and Secondary base palette styles with variant index
PRIMARY(0)=foregroundColor(DARK_BLUE)
PRIMARY(1)=foregroundColor(BRIGHT_BLUE)
PRIMARY(*)=PRIMARY(*%16)
SECONDARY(0)=backgroundColor(DARK_BLUE)
SECONDARY(*)=SECONDARY(*%16)
# Title style combining primary and underline
TITLE(*)=primary(*),underlined()
# Syntax & Token Styles
KEYWORD(0)=foregroundColor(BRIGHT_BLUE)
KEYWORD(1)=foregroundColor(DARK_SKY)
KEYWORD(*)=KEYWORD(*%4)
OPTION(0)=foregroundColor(DARK_SKY)
OPTION(*)=KEYWORD(*%4)
# Semantic UI & Status Styles
ERROR(*)=foregroundColor(DARK_RED)
SUCCESS(*)=foregroundColor(2)
WARN(*)=foregroundColor(3)
INFO(*)=foregroundColor(DARK_SKY)
CONFIG(*)=foregroundColor(5)
DATE(*)=foregroundColor(6)
NUMBER(*)=foregroundColor(6)
BOOLEAN(*)=foregroundColor(6)
STRING(*)=foregroundColor(8)
SEPARATOR(*)=foregroundColor(208)
OPERATOR(*)=foregroundColor(208)
INPUT(*)=foregroundColor(11)
FAIL(*)=foregroundColor(DARK_RED)
DANGER(*)=foregroundColor(DARK_RED)
VAR(*)=foregroundColor(190)
PALE(*)=foregroundColor(250)
COMMENTS(*)=foregroundColor(250)
VERSION(*)=foregroundColor(220)
PATH(*)=foregroundColor(114)
Supported Token Styles
Supported semantic style tokens include:
Base: PRIMARY, SECONDARY, TITLE
Syntax: KEYWORD, ENTITY, ACTION, ANNOTATION, VAR, OPERATOR, SEPARATOR, COMMENTS
Literals: STRING, INPUT, PATH, VERSION, NUMBER, DATE, BOOLEAN, OPTION, PLACEHOLDER
UI Status: INFO, CONFIG, SUCCESS, WARN, ERROR, DANGER, FAIL, PALE
Supported Styling Functions
Modifiers: plain, underlined, bold, blink, striked, reversed, italic
Colors:
foregroundColor(val)/foreground(val),backgroundColor(val)/background(val),foregroundTrueColor(val),backgroundTrueColor(val)or direct#RRGGBBhex values.
Custom Theme Locations
Place custom theme files in: 1. The application classpath under META-INF/ntf-themes/<name>.ntf-theme. 2. The Nuts user configuration directory under ~/.config/nuts/.../themes/<name>. 3. Any accessible filesystem location loaded by path or URL using --theme=/path/to/theme.ntf-theme or NTextTheme.of(NPath.of(...)).
Further Reading
NTextTheme interface:
net.thevpc.nuts.text.NTextThemeDefault theme implementation:
net.thevpc.nuts.runtime.standalone.text.theme.NTextPropertiesThemeBuilt-in theme resources:
META-INF/ntf-themes/
For more details, refer to the Styling Messages section.
8 IO & Filesystem Abstractions
The Nuts library offers a comprehensive and flexible input/output system designed for modern CLI and application needs, including:
NIn: Simplifies and unifies input handling, supporting both interactive and programmatic input sources.
NOut: The standard output stream abstraction supporting colorful, structured, and formatted output that respects the current session context and output format (plain text, JSON, XML, tables, etc).
NErr: Dedicated error output stream, separate from standard output, ensuring proper logging and error visibility.
NTrace: Specialized output stream for trace/debug information that is only displayed when trace mode is enabled, helping users debug without cluttering normal output.
NLog: A logging interface integrating with Nuts' session and workspace model to produce contextual logs with flexible verbosity and output controls.
NMsg: A message abstraction enabling rich, multi-language, and parameterized messages with support for localization and structured formatting.
NTF: Nuts Text Format — a powerful formatting system that extends standard text output with features like color, styles, and structured data rendering, allowing consistent and attractive CLI outputs.
Together, these components create a rich and developer-friendly IO system, enabling:
- Colorful and readable console output.
- Structured output formats automatically adapting to context.
- Clear separation of standard, error, and debug outputs.
- Localizable and parameterized messages.
- Simplified user input handling.
This system is integrated tightly with the Nuts session concept, meaning output behavior automatically adapts to user preferences, environment, and command-line options, making CLI tools more usable and professional.
The Nuts ecosystem provides a comprehensive set of tools designed to simplify and unify file and resource management across different protocols and formats.
What Nuts Supports
NPath: A versatile path abstraction that extends beyond traditional file system paths. It supports local files, URLs (HTTP/HTTPS), classpath resources, and Maven-style artifact resources. This unified API enables seamless access and manipulation of diverse resource types.
NCp: An advanced copying utility capable of copying files and directories with support for validation, progress tracking, overwrite policies, and more. It provides robust features to reliably transfer resources locally or remotely.
NCompress / NUncompress: Utilities to compress and decompress files and folders using popular archive formats such as ZIP and TAR. These tools handle format detection, filtering, and extraction/compression workflows transparently.
NDigest: A digest computation tool for calculating checksums and hashes (e.g., SHA-256) of files, streams, and folders. It supports integrity verification and recursive digesting to ensure file content authenticity.
Together, these components offer a flexible and powerful framework to interact with files and resources efficiently, whether for local development or distributed environments.
8.1 NOut Standard output
The NOut class is a simple and powerful utility for writing to the standard output in Nuts. It provides a consistent and extensible way to print text, formatted messages, and structured data.
By default, NOut delegates to the session's configured output stream, defined as an NPrintStream in the current NSession. This stream is customizable, structured, and NTF-aware, making it suitable for both human-readable and machine-readable outputs (JSON, XML, etc.).
Unlike System.out, NOut provides enhanced capabilities:
Intelligent rendering of objects (beyond basic
toString()),Colorized and formatted output via NTF (Nuts Text Format),
- Support for various structured formats (e.g., JSON, YAML, XML, TSON),
- Support for formatted messages with placeholders,
- Table and tree rendering.
Basic Usage
The simplest way to print a message to the console:
Nuts.require();
NOut.println("Hello");
Using NTF
NTF enables you to add rich formatting and colorization:
NOut.println("##Hello colored## ##:_:Hello underlined## ");
NOut.println("##:yellow:Hello in yellow##");
NOut.println("##:warn:this is a warning##");
NOut.println("##:fxFF0000:this is a red message##");
Rendering structured output
NOut can render structured output based on the active format in the NSession.
class Customer{String id;String name;}
Customer customer1,customer2,customer3; ...
// configure le current output to render objects as json
// to display the curstomer list as a json array
NSession.of().json();
NOut.println(Arrays.asList(customer1,customer2,customer3));
// you can do the same for yaml,tson,xml,table and tree (as formats)
NSession.of().tree();
NOut.println(Arrays.asList(customer1,customer2,customer3));
Formatted Messages
You can build formatted messages using NMsg, with placeholder support and type-aware formatting:
NOut.println(NMsg.of("this is a %s message that is %s %% beautiful",true,100));
Values such as booleans and numbers are rendered with distinct styles (e.g., colors) for better readability.
Or you can build your own styled arguments :
NOut.println(NMsg.of("this is a %s ",NMsg.ofStyledPrimary1("message")));
Working with Tables
To have full control over tabular output, use NMutableTableModel:
NSession session=...;
Object a,b,c,d; ...
NMutableTableModel m = NTableModel.of();
m.newRow().addCells(a,b,c,d);
NOut.println(m);
Working with Trees
To render hierarchical structures, you can implement a custom NTreeModel:
NOut.println(
new NTreeModel() {
@Override
public Object getRoot () {
return "/";
}
@Override
public List<NDependencyTreeNodeAndFormat> getChildren (Object node){
if ("/".equals(node)) {
return Arrays.asList(1,2,3);
}
return Arrays.asList();
}
}
);
Summary
The NOut class provides a robust and extensible mechanism for console output in the Nuts ecosystem. Whether you're logging simple messages, displaying structured data, or building CLI tools, NOut ensures consistent and powerful rendering—fully aligned with Nuts' NTF and output formatting infrastructure.
8.2 NErr Standard Error
NErr is the error-stream counterpart to NOut, providing structured, colored, and format-aware error output in the Nuts ecosystem.
It writes to the standard error stream configured in the current NSession, represented by a customizable NPrintStream. Like NOut, this stream is fully NTF-aware and supports a wide range of formats such as JSON, YAML, TSON, XML, tree, and table.
Key Features
Delegates to
NSession.err()(an NPrintStream)Fully supports NTF (Nuts Text Format) for colored and styled messages
- Supports structured output in multiple formats
Works seamlessly with NMsg, NMutableTableModel, and NTreeModel
- Ideal for logging warnings, errors, diagnostics, and debugging information
Basic Example
Nuts.require();
NErr.println("An error occurred");
Styled Error Messages
You can leverage NTF for expressive and styled output:
NErr.println("##:error:Something went wrong!##");
NErr.println("##:warn:Warning:## Potential issue detected");
NErr.println("##:fxFF0000:Critical failure##");
Structured Error Reporting
Just like with NOut, you can render structured error data using the current session format:
NSession.of().json();
NErr.println(errorList); // errorList = List<ErrorDetail>
You can switch to any other supported format (yaml, xml, table, tree, tson, etc.) using:
NSession.of().table();
NErr.println(errorList);
Formatted Diagnostic Messages
Use NMsg to build dynamic, strongly typed error messages:
NErr.println(NMsg.of("Task %s failed after %d attempts", "SyncJob", 3));
Use Cases
- Displaying runtime errors or exceptions in a user-friendly way
- Emitting machine-readable diagnostics for automation tools
- Rendering hierarchical error trees or tabular summaries
- Debugging output during CLI tool development
Summary
NErr brings all the expressive power of NOut to the standard error stream. Whether you're showing simple warnings or structured diagnostic trees, NErr ensures your error messages are readable, styled, and format-compliant with the Nuts session configuration.
8.3 NTrace, the output companion
NTrace — Conditional Trace Output Utility
NTrace is a structured output utility in Nuts used to emit optional diagnostic or trace information to the standard output stream. It behaves like NOut, but only prints output when tracing is explicitly enabled in the current session.
🔍 When to Use NTrace
Use NTrace to provide optional messages that :
- Help during development or debugging,
- Provide insights into internal steps,
- Are not critical and should not mix with standard output (NOut) or error messages (NErr).
Unlike NOut, NTrace output is optional and controlled by the trace flag, so it won’t clutter output if tracing is turned off.
Output Destination
Note: NTrace writes to NSession.out() — the same output stream used by NOut. In contrast, NErr writes to NSession.err().
This means trace messages can be redirected, styled, and formatted consistently with standard output, but only appear when tracing is active.
Trace Mode Behavior
Trace is enabled by default.
To disable trace output, users can:
- Pass --trace=false or --!trace on the command line:
nuts --trace=false my-app
nuts --!trace my-app
- Programmatically disable it in the session:
NSession.of().setTrace(false);
When trace is disabled, all calls to NTrace.println(...) are ignored silently.
Example Usage
Nuts.require();
// This will print only if trace is enabled (default is true)
NTrace.println("Loading configuration from default path...");
Features (Same as NOut)
NTrace supports the complete feature set of NOut, including:
- NTF formatting for colors and styling,
- Structured rendering (e.g., JSON, YAML, XML, TSON, table, tree),
- Formatted messages using NMsg,
- Integration with the current session’s output configuration.
Best Practices
Use NTrace to display less relevant or verbose messages that:
- Are helpful for end users who want to better understand what the tool is doing,
- Should not appear during normal usage but may provide useful context when verbosity is desired (e.g., progress steps, skipped actions, fallback behavior),
- Can be safely ignored without impacting the understanding of the main output.
NTrace is not a developer logging mechanism. For internal developer-oriented logging, use NLog.
8.4 NIn for simplified Input
NIn — Structured Input Utility
The NIn class is the interactive input utility of the Nuts platform. It provides a simple and consistent interface to read from the standard input stream (NSession::in()), with built-in support for prompts, password masking, and type-safe values.
Basic Input Reading
Reading a Line
String line = NIn.readLine();
Reads a full line from the user input.
You can also provide a prompt using an NMsg:
String name = NIn.readLine(NMsg.ofC("Enter your ##name##: "));
Reading a Password
char[] pwd = NIn.readPassword();
Reads a password without echoing characters (as far as the required extensions are loaded) to the terminal.
With prompt:
char[] pwd = NIn.readPassword(NMsg.ofPlain("Password: "));
Reading a Literal
NLiteral lit = NIn.readLiteral();
Reads a string input and wraps it in an NLiteral, allowing you to safely extract typed values.
NLiteral lit = NIn.readLiteral(NMsg.ofPlain("Enter a number: "));
int value = lit.asInt().get();
use the NLiteral::asXYZ series of methods to convert the string input to common types like double, boolean, etc...
NLiteral lit = NIn.readLiteral(NMsg.ofPlain("Enter a number: "));
double value = lit.asDouble().get();
Interactive and Typed Input with NAsk
For complex or type-safe input, NIn.ask() (or NAsk.of()) provides a fluent API to build interactive prompts with support for:
- Custom messages,
- Typed inputs (String, int, boolean, enum, etc.),
- Default values,
- Validators,
- Accepted values,
- Password input,
- "Remember me" options,
- Custom parsing and formatting.
It will re-prompt indefinitely until a valid input is provided, based on type and validation constraints, or until the user cancels the prompt (e.g., by sending an interrupt like Ctrl+C or entering an empty value when allowed).
NAsk will re-prompt indefinitely in an interactive loop until:
- A valid value is provided (based on expected type and validation),
- Or the user explicitly cancels the prompt (e.g., by interrupting input or when input is blank and optional). This ensures reliable and robust user interaction with clear guidance and fallback behavior.
Password Input Example
char[] password = NIn.ask()
.forPassword(NMsg.ofPlain("Password for user " + user))
.getValue();
Prompts for a password securely (input not echoed), and returns a char[].
Boolean Confirmation with Context
boolean usePcp = NIn.ask()
.forBoolean(
NMsg.ofPlain(
remote
? "Use PCP users the same as the instances hosts users?"
: "Use PCP user as the same as the current user?"
)
)
.getValue();
Prompts the user with a yes/no question.
"Remember Me" with Default
boolean override = NIn.ask()
.setDefaultValue(true)
.setRememberMeKey(
rememberMeKey == null ? null : ("Override." + rememberMeKey)
)
.forBoolean(
NMsg.ofC("Override %s?",
NText.ofStyled(
betterPath(out.toString()),
NTextStyle.path()
)
)
)
.getBooleanValue();
This example:
- Proposes a default answer (true),
- Persists the answer under the given key (rememberMeKey), so the question may be skipped next time,
- Uses styled output in the question message.
Custom Validation Example
String mainClass = NIn.ask()
.forString(NMsg.ofNtf("Enter the name or index:"))
.setValidator((value, question) -> {
Integer index = NLiteral.of(value).asInt().orNull();
if (index != null && index >= 1 && index <= possibleClasses.size()) {
return possibleClasses.get(index - 1);
}
if (possibleClasses.contains(value)) {
return value;
}
throw new NValidationException(); // Triggers re-prompt
})
.getValue();
NAsk Supported NAsk Features
forString(...)Prompt for a StringforInt(...)Prompt for an intforDouble(...)Prompt for a doubleforBoolean(...)Prompt for a boolean (yes/no, true/false)forEnum(Class<E> enumType, ...)Prompt for an enum valueforPassword(...)Prompt for a password (char[])setDefaultValue(T)Sets a default value used when input is blanksetHintMessage(NMsg)Displays hint under the questionsetAcceptedValues(List<Object>)Restricts accepted values and may display suggestionssetRememberMeKey(String)Automatically stores and reuses the answer based on a keysetValidator(NAskValidator<T>)Adds input validation logicsetParser(NAskParser<T>)Custom parsing from String to TsetFormat(NAskFormat<T>)Custom formatting of expected values for user display
NAsk Re-prompting Behavior
NAsk will loop until a valid answer is provided, according to:
- Type expectations (e.g., integer, enum),
- Custom validators (if any),
- Accepted values (if defined).
This ensures robust, user-friendly interaction without premature failure.
8.5 NPath
NPath
NPath is a powerful, protocol-aware abstraction introduced by Nuts to handle resource locations in a uniform way. Similar to Java's URL or Path, but with extended capabilities, built-in protocol support, and a fluent, intuitive API, NPath bridges the gap between local files, remote URLs, classpath resources, and virtual in-memory locations.
Key Features
- Unified Abstraction: Seamlessly handle local files, HTTP/SSH URLs, classpath resources, in-memory buffers, and artifact repositories with a single API.
- Protocol-Aware: Natively supports file, http, https, ssh, classpath, mem, and custom Nuts protocols (e.g., htmlfs+https://).
- Rich I/O Operations: First-class support for reading/writing bytes, strings, structured objects, and streaming, with automatic parent directory creation.
- Fluent Metadata & Behavior: Dynamically attach content hints (charset, content type, kind) and behavioral flags (cache, temporary) via fluent builders.
- Advanced Navigation: Robust path manipulation including relativize, stripParent, smart extension parsing (nameParts), and glob/DFS tree walking.
- Lifecycle Management: Built-in support for temporary files, auto-cleanup (deleteOnDispose), and OS-compliant user/system store locations.
Supported Protocols
| Protocol | Example | Description |
|---|---|---|
| Local File | /path/to/resource, C:path | Standard filesystem paths (protocol is implicitly ""). |
| File URL | file:/path/to/resource | Explicit file URL scheme. |
| HTTP/HTTPS | Remote web resources. | |
| SSH | ssh://user@server/path/to/resource | Secure remote file access. |
| Classpath | classpath:/com/myapp/config.xml | Resources bundled in JARs or classpath folders. |
| In-Memory | mem://sandbox/data.txt | Virtual in-memory filesystem. Zero disk I/O, ideal for testing or transient data. |
| Nuts Resource | resource://group:artifact#version/path | Nuts-specific artifact resolution. |
| HTML FS | Browses Apache-style HTML directory listings as a virtual filesystem. |
Creating an NPath
Basic Creation
// From String (local, URL, classpath, or memory)
NPath localFile = NPath.of("/path/to/resource.txt");
NPath remoteFile = NPath.of("https://example.com/data.json");
NPath memFile = NPath.of("mem://sandbox/temp-data.txt");
// From standard Java types
NPath fromUrl = NPath.of(new URL("file:///tmp/test.txt"));
NPath fromFile = NPath.of(new File("/tmp/test.txt"));
NPath fromNio = NPath.of(Paths.get("/tmp/test.txt"));
// From a Nuts Connection String
NPath fromConn = NPath.of(NConnectionString.of("ssh://user@host/path"));
Classpath & Origins
// Resolve with a specific ClassLoader
NPath cpResource = NPath.of("classpath:/config.properties", MyClass.class.getClassLoader());
// Find where a class was loaded from (e.g., which JAR)
NOptional<NPath> origin = NPath.ofOrigin(MyClass.class);
List<NPath> allOrigins = NPath.ofOrigins(MyClass.class);
Behavioral Flags & Content Metadata
While structural operations (resolve, normalize) return new path instances, NPath provides fluent methods to adjust its behavior and attach content metadata. This is especially useful for virtual paths (mem://), HTTP responses, or when you need to override inferred file characteristics.
Behavioral Flags
NPath tempFile = NPath.ofTempFile("report.pdf");
// Mark this path to be treated as user cache (affects storage/cleanup policies)
NPath cachedFile = tempFile.userCache(true);
// Explicitly mark as temporary (may influence disposal or OS-level temp handling)
NPath explicitTemp = cachedFile.userTemporary(true);
Content Metadata (NContentMetadata)
The NContentMetadata interface acts as a fluent builder to attach or override metadata without altering the underlying physical resource. This is heavily utilized when NPath acts as an NInputSource or NOutputTarget.
NPath memResponse = NPath.of("mem://api/response");
// Build metadata fluently
NContentMetadata meta = memResponse.metaData()
.name("user-profile.json") // Override the logical name
.contentType("application/json") // Force content type (bypasses extension guessing)
.charset("UTF-8") // Explicit charset
.kind("api-response") // Custom semantic kind
.message(NMsg.ofInfo("Generated successfully")) // Attach a status/description
.contentLength(1024L); // Pre-declare length if known
Special Locations: Stores & Temporary Files
User and System Stores
NPath provides OS-compliant storage locations (aligning with XDG Base Directory Specification on Linux). Use NStoreKey to target a specific application (GAV) and store type.
NId appId = NId.of("com.mycompany:myapp");
NStoreKey configKey = new NStoreKey(appId, NStoreType.CONF);
NPath configFolder = NPath.of(configKey);
configFolder.mkdirs();
configFolder.resolve("settings.json").writeString("{\"theme\": \"dark\"}");
Supported NStoreTypes:
| StoreType | Purpose | Linux Equivalent |
|---|---|---|
| BIN | User-specific executable binaries | $HOME/.local/bin |
| CONF | Configuration files | $XDG_CONFIG_HOME or $HOME/.config |
| VAR | Modifiable data files | $XDG_DATA_HOME or $HOME/.local/share |
| LOG | Runtime logs and audit trails | $XDG_LOG_HOME or $HOME/.local/log |
| TEMP | Temporary files | $TMPDIR or /tmp |
| CACHE | Non-essential cached data | $XDG_CACHE_HOME or $HOME/.cache |
| LIB | Non-executable libraries | $HOME/.local/lib |
| RUN | Runtime files (sockets, PID files) | $XDG_RUNTIME_DIR |
Temporary Files and Folders
// Workspace-level temp file/folder
NPath tempFile = NPath.ofTempFile("buffer.bin");
NPath tempFolder = NPath.ofTempFolder("project-workspace");
// Repository-scoped temp file
NPath repoTemp = NPath.ofTempRepositoryFile("download.tmp", myRepository);
// ID-scoped temp folder
NPath idTemp = NPath.ofTempIdFolder("build-output", NId.of("com.example:lib:1.0"));
// AUTO-CLEANUP: Mark a temp path to be deleted when the JVM exits or session ends
tempFile.deleteOnDispose(true);
Path Manipulation & Navigation
NPath base = NPath.of("/var/log/myapp");
// Resolve: standard resolution
NPath logFile = base.resolve("app.log");
// ResolveChild: ignores leading slashes in the child (safer for dynamic concatenation)
NPath safeChild = base.resolveChild("/app.log");
// ResolveSibling: replaces the last name element
NPath sibling = logFile.resolveSibling("error.log");
// Normalize and Absolute
NPath normalized = NPath.of("/a/b/../c").normalize();
NPath absolute = NPath.of("relative.txt").toAbsolute();
Advanced Navigation: relativize vs stripParent
NPath path = NPath.of("/a/b/c");
// stripParent: Strict prefix removal. Returns empty if not a direct descendant.
path.stripParent(NPath.of("/a")); // Optional["b/c"]
path.stripParent(NPath.of("/x")); // Optional.empty()
// relativize: Navigational. Calculates the route from origin to this path.
path.relativize(NPath.of("/a/b")); // Optional["c"]
NPath.of("/a/c").relativize(NPath.of("/a/b")); // Optional["../b"]
Name Parsing (nameParts)
NPath p = NPath.of("/archive/my.backup.tar.gz");
// SHORT: splits at the last dot
p.nameParts(NPathExtensionType.SHORT);
// base="my.backup.tar", ext=".gz", fullExt=".gz"
// LONG: splits at the first dot
p.nameParts(NPathExtensionType.LONG);
// base="my", ext=".backup.tar.gz", fullExt=".backup.tar.gz"
// SMART: heuristic-based (e.g., knows about .tar.gz)
p.nameParts(NPathExtensionType.SMART);
// base="my.backup", ext=".tar.gz", fullExt=".tar.gz"
Content I/O
Reading
byte[] data = path.readBytes();
String text = path.readString(); // Defaults to UTF-8
String textIso = path.readString(StandardCharsets.ISO_8859_1);
// Streaming
try (InputStream is = path.getInputStream()) { /* process */ }
try (BufferedReader reader = path.getBufferedReader()) {
reader.lines().forEach(System.out::println);
}
Writing
path.writeBytes(new byte[]{1, 2, 3});
path.writeString("Hello World", StandardCharsets.UTF_8);
// Write structured objects (uses Nuts formatting/serialization)
path.writeObject(myPojo);
path.writeText(NText.of("Formatted text"));
path.writeMsg(NMsg.ofInfo("Operation completed"));
Copying and Moving
NPath source = NPath.of("/tmp/data.txt");
NPath target = NPath.of("/backup/data.txt");
source.copyTo(target, NPathOption.CREATE_PARENTS);
source.moveTo(target, NPathOption.REPLACE_EXISTING);
// Copy from external streams
try (InputStream is = new URL("https://example.com/file").openStream()) {
target.copyFromInputStream(is, NPathOption.CREATE_PARENTS);
}
File & Directory Operations
NPath dir = NPath.of("/tmp/myapp/data");
// Creation
dir.mkdirs(); // Create directory and all missing parents
dir.mkParentDirs(); // Only create parents of the current path
dir.ensureEmptyDirectory(); // Creates if missing, or deletes contents if exists
// Inspection
boolean exists = dir.exists();
boolean isDir = dir.isDirectory();
boolean isRemote = dir.isRemote(); // true for http://, ssh://, mem://, etc.
// Deletion
dir.delete(); // Delete file or empty directory
dir.deleteTree(); // Recursively delete directory and contents
File Tree & Searching
NPath dir = NPath.of("/var/log");
// Simple list
List<NPath> files = dir.list();
List<NPathInfo> infos = dir.listInfos(); // Includes size, type, etc.
// Stream with filtering (Lazy evaluation)
try (NStream<NPath> stream = dir.stream()) {
List<NPath> txtFiles = stream.filter(p -> p.getName().endsWith(".log")).toList();
}
// Glob pattern matching
dir.walkGlob("**/*.java").forEach(p -> System.out.println("Found: " + p));
// Digest and Checksums
List<NPathChildStringDigestInfo> digests = dir.listStringDigestInfo("SHA-256");
Metadata & Permissions (Filesystem Level)
NPath file = NPath.of("/etc/secure/config.yml");
// Basic File Metadata
NPathInfo info = file.info();
long size = file.contentLength();
Instant modified = file.lastModifiedInstant();
// Ownership & Permissions (POSIX)
String owner = file.owner();
Set<NPathPermission> perms = file.permissions();
// Modify permissions
file.addPermissions(NPathPermission.OWNER_READ, NPathPermission.OWNER_WRITE);
file.removePermissions(NPathPermission.OTHERS_EXECUTE);
Conversion & Introspection
Convert NPath back to standard Java types when interoperability is required. All return NOptional to safely handle unsupported conversions.
NPath path = NPath.of("/tmp/test.txt");
// Safe conversions
Optional<Path> nioPath = path.toPath().asOptional();
Optional<File> javaFile = path.toFile().asOptional();
Optional<URL> javaUrl = path.toURL().asOptional();
// Introspection
String protocol = path.protocol(); // "" for local, "https" for web, "mem" for memory
String location = path.location(); // The raw string representation
NPath compressed = path.toCompressedForm(); // Shortened form (e.g., using ~ for home)
Advanced Example: Robust Remote Download with Metadata & In-Memory Staging
public void downloadArtifact(String urlStr, NId artifactId) {
NPath remote = NPath.of(urlStr);
// 1. Stage the download in memory first (zero disk I/O until verified)
NPath memStaging = NPath.of("mem://staging/" + artifactId.getName() + ".tmp")
.userTemporary(true); // Explicitly mark as transient
// 2. Copy with automatic parent creation
remote.copyTo(memStaging, NPathOption.CREATE_PARENTS);
// 3. Attach rich metadata before further processing
NContentMetadata meta = memStaging.metaData()
.name(artifactId.getName() + ".jar")
.contentType("application/java-archive")
.kind("downloaded-artifact");
// 4. Verify integrity using metadata
if (meta.contentLength().orElse(0L) == 0) {
throw new IOException("Downloaded payload is empty");
}
// 5. Move to final destination on disk atomically
NPath finalDest = NPath.ofUserStore(new NStoreKey(artifactId, NStoreType.CACHE))
.resolve(artifactId.getName() + ".jar");
finalDest.mkParentDirs();
memStaging.copyTo(finalDest, NPathOption.REPLACE_EXISTING);
}
Summary
NPath is a unified resource locator that goes beyond simple string manipulation. By abstracting away the differences between local disks, remote servers, in-memory buffers (mem://), and virtual repositories, it allows you to write clean, portable, and resilient I/O code. Its fluent API for behavioral flags and NContentMetadata, combined with Nuts-specific features like NStoreKey and deleteOnDispose, makes it the ideal foundation for any tool requiring flexible, context-aware resource access.
8.6 Working with files
nuts Library allows multiple files to be processed
NCp
NCp.of()
.from("http://my-server.com/file.pdf")
.to("/home/my-file")
.progressMonitor(true)
.validator((in)->checkSHA1Hash(in))
.run();
NPs ps=NPs.of()
if(ps.isSupportedKillProcess()){
ps.killProcess("1234");
}
NCompress/NUncompress
NCompress aa = NCompress.of()
.setTarget(options.outZip);
for (NPath file : options.files) {
aa.source(file);
}
aa.run();
NUncompress.of()
.from(is)
.visit(new NUncompressVisitor() {
@Override
public boolean visitFolder(String path) {
return true;
}
@Override
public boolean visitFile(String path, InputStream inputStream) {
if ("META-INF/MANIFEST.MF".equals(path)) {
...
} else) {
...
}
return true;
}
}).run();
NDigest
String digest=NDigest.of().setSource(x.getPath().getBytes()).computeString();
}).run();
8.7 HTTP Web Client (NHttpClient)
For communicating with external HTTP web services, NAF provides NHttpClient[cite: 7]. It offers a fluent, highly configurable, and context-aware builder API to construct synchronous or asynchronous HTTP requests, handle payloads (JSON, URL-encoded forms, multipart), manage headers/cookies, and process raw or structured JSON responses[cite: 7, 8, 9].
Key features:
Fluent Request Building – Construct requests naturally using method shortcuts like
GET(),POST(),PUT(),DELETE(), etc., passing relative or absolute target paths[cite: 7, 8].Flexible Body Serialization – Send payloads easily with
jsonRequestBody(Object)[cite: 8], raw text withrequestBody(String)[cite: 8], or handle multipart file uploads viaaddPart()[cite: 8].Integrated JSON Mapping – Automatically bind response payloads directly into Java POJOs using
contentAsJson(Class<T>)[cite: 9], or interact with them dynamically viacontentAsJsonMap()orcontentAsJsonList()[cite: 9].Stateful Cookie & Header Management – Attach global cookies and base URIs to the client instance (NHttpClient)[cite: 7], or override them granularly on a per-request layer (NHttpRequest)[cite: 8].
Robust Error Handling – Inspect status codes quickly using semantic utilities like
isOk(),isClientError(), andisServerError()[cite: 5, 9], or useifErrorThrow()to chain defensive error state processing[cite: 9].Timeout Adaptability – Configure explicit connection and read deadlines via
connectTimeout(NDuration)andreadTimeout(NDuration)directly on the client[cite: 7] or specific requests[cite: 8].
// Fast execution instance retrieving a payload
String payload = NHttpClient.of()
.GET("https://api.example.com/status")
.run()
.contentAsString();
Example: JSON Request and Response Parsing
The client simplifies interactions with REST APIs by abstracting the explicit manual mapping boilerplate of JSON payloads back and forth:
public void login(String login, String password) {
String url="http://my-server/do-this";
NHttpResponse response = NHttpClient.of()
.POST(url)
.jsonRequestBody(
NMapBuilder.ofLinked()
.put("userName", login)
.put("password", password)
.build()
)
.run();
if (response.isOk()) {
LoginResult rr = response.contentAsJson(LoginResult.class);
if (rr != null && !NBlankable.isBlank(rr.accessToken)) {
this.loginResult = rr;
return;
}
} else {
throw new NIllegalArgumentException(
NMsg.ofC("unable to login to %s", url)
);
}
throw new NIllegalArgumentException(
NMsg.ofC("unable to login to %s", url)
);
}
Example: Seamless Multipart Form Uploads
NHttpClient provides highly optimized overloads for uploading files (File, Path, or NPath). It automatically infers the form parameters and filenames behind the scenes:
// Upload a file where the form name matches the local filename
NHttpClient.of()
.baseUri("http://my-server/app")
.POST("/upload")
.addPart(myFile)
.run();
// Or specify an explicit form parameter name
NHttpClient.of().POST("/upload")
.addPart("avatar", myFile)
.run();
Resilience & Asynchronous Execution
To keep your application non-blocking, you can submit tasks asynchronously. Additionally, rather than bloating the HTTP client with retry logic, NHttpClient integrates cleanly with your workspace's concurrency engine:
// Run asynchronously using a custom Executor or your NConcurrent ExecutorService
CompletableFuture<NHttpResponse> asyncResponse = NHttpClient.of()
.GET("/long-task")
.runAsync(NConcurrent.executorService());
// Combine NHttpClient and NConcurrent executor service for robust network retry behaviors
try(NRetryCall retry = NRetryCall.of(() -> NHttpClient.of().GET("/flaky-service").run())){
NHttpResponse resilientResponse=retry.run();
}
8.8 File system
nuts
manages multiple workspaces. It has a default one located at ~/.config/nuts (~ is the user home directory). Each workspace handles a database and files related to the installed applications. The workspace has a specific layout to store different types of files relatives to your applications.
nuts
is largely inspired by XDG Base Directory Specification and hence defines several store locations for each file type. Such organization of folders is called Layout and is dependent on the current operating system, the layout strategy and any custom configuration.
Store Locations
Supported Store Locations are :
nuts File System defines the following folders :
config : defines the base directory relative to which application specific configuration files should be stored.
apps : defines the base directory relative to which application executable binaries should be stored
lib : defines the base directory relative to which application non executable binaries should be stored
var : defines the base directory relative to which application specific data files (other than config) should be stored
log : defines the base directory relative to which application specific log and trace files should be stored
temp : defines the base directory relative to which application specific temporary files should be stored
cache : defines the base directory relative to which application non-essential data and binary files should be stored to optimize bandwidth or performance
run : defines the base directory relative to which application-specific non-essential runtime files and other file objects (such as sockets, named pipes, ...) should be stored
nuts defines such distinct folders (named Store Locations) for storing different types of application data according to your operating system.
On Windows Systems the default locations are :
- apps : "$HOME/AppData/Roaming/nuts/apps"
- lib : "$HOME/AppData/Roaming/nuts/lib"
- config : "$HOME/AppData/Roaming/nuts/config"
- var : "$HOME/AppData/Roaming/nuts/var"
- log : "$HOME/AppData/Roaming/nuts/log"
- temp : "$HOME/AppData/Local/nuts/temp"
- cache : "$HOME/AppData/Local/nuts/cache"
- run : "$HOME/AppData/Local/nuts/run"
On Linux, Unix, MacOS and any POSIX System the default locations are :
- config : "$HOME/.config/nuts"
- apps : "$HOME/.local/share/nuts/apps"
- lib : "$HOME/.local/share/nuts/lib"
- var : "$HOME/.local/share/nuts/var"
- log : "$HOME/.local/log/nuts"
- cache : "$HOME/.cache/nuts"
- temp : "$java.io.tmpdir/$username/nuts"
- run : "/run/user/$USER_ID/nuts"
As an example, the configuration folder for the artifact net.thevpc.app:netbeans-launcher#1.2.4 in the default workspace in a Linux environment is
home/me/.config/nuts/default-workspace/config/id/net/vpc/app/netbeans-launcher/1.2.4/
And the log file "app.log" for the same artifact in the workspace named "personal" in a Windows environment is located at
C:/Users/me/AppData/Roaming/nuts/log/nuts/personal/config/id/net/vpc/app/netbeans-launcher/1.2.4/app.log
Store Location Strategies
When you install any application using the nuts command a set of specific folders for the presented Store Locations are created. For that, two strategies exist : Exploded strategy (the default) and Standalone strategy.
In Exploded strategy nuts defines top level folders (in linux ~/.config for config Store Location etc), and then creates withing each top level Store Location a sub folder for the given application (or application version to be more specific). This helps putting all your config files in a SSD partition for instance and make nuts run faster. However if you are interested in the backup or roaming of your workspace, this may be not the best approach.
The Standalone strategy is indeed provided mainly for Roaming workspaces that can be shared, copied, moved to other locations. A single root folder will contain all of the Store Locations.
As an example, in "Standalone Strategy", the configuration folder for the artifact net.thevpc.app:netbeans-launcher#1.2.4 in the default workspace in a Linux environment is
home/me/.config/nuts/default-workspace/config/id/net/vpc/app/netbeans-launcher/1.2.4/
And the log file "app.log" for the same artifact in the workspace named "personal" in the same Linux environment is located at
/home/me/.config/nuts/default-workspace/log/id/net/vpc/app/netbeans-launcher/1.2.4/
You can see here that the following folder will contain ALL the data files of the workspace.
/home/me/.config/nuts/default-workspace
whereas in the Exploded strategy the Store Location are "exploded" into multiple root folders.
Custom Store Locations
Of course, you are able to configure separately each Store Location to meet your needs.
Selecting strategies
The following command will create an exploded workspace
nuts -w my-workspace --exploded
The following command will create a standalone workspace
nuts -w my-workspace --standalone
Finer Customization
The following command will create an exploded workspace and moves all config files to the SSD partition folder /myssd/myconfig
nuts -w my-workspace --system-conf-home=/myssd/myconfig
You can type help for more details.
nuts help
9 Concurrency & Stability
In Nuts, building reliable and high-performance applications requires tools that manage shared state, caching, rate-limiting, and progress tracking safely across threads and execution contexts. The Nuts framework provides a suite of concurrency primitives and stability helpers to simplify these common challenges:
NScopedValue<T>– Represents a thread- or context-local value that can be temporarily overridden within a scope. This allows safe modifications of variables without risking unintended side effects outside the current scope. Useful for contextual configurations or temporary overrides in multi-threaded environments.NCachedValue<T>– Encapsulates a lazily computed value with automatic caching. It ensures that expensive computations are performed only once, optionally refreshing based on custom invalidation rules, and is safe for concurrent access.NRateLimitValue<T>– Provides rate-limited access to a value or operation. It tracks usage counts over time and enforces constraints, helping prevent overuse of resources, APIs, or services in a thread-safe manner.NLock – A versatile locking mechanism that abstracts traditional concurrency locks. NLock can be used to synchronize access to shared resources, enforce critical sections, and implement safe multi-threaded algorithms. It also supports inter-process locking via files, enabling coordination between separate JVMs or applications.
NProgressMonitor – Facilitates tracking and reporting of long-running tasks. It allows threads to report progress, handle cancellation, and provide estimated completion information. In addition, it supports structured progress monitoring, letting developers split tasks and assign weights to subtasks for fine-grained progress aggregation.
Together, these primitives provide a robust foundation for concurrency, caching, and stability in Nuts-based applications, ensuring that multi-threaded code behaves predictably, resources are protected, and operations are safely monitored.
9.1 Once Value
NOnceValue stores a value that is computed lazily — the supplier is not invoked until the first call to get(). Once evaluated, the value remains stable and is reused for all subsequent accesses. This is perfect for expensive computations, constants, or resources that should only be initialized once.
// Example 1: Lazy initialization
NOnceValue<Double> onceRandom = NOnceValue.of(Math::random);
// Value is computed on first access
NOut.println("First value = " + stableRandom.get());
// Subsequent accesses return the same value
NOut.println("Same value = " + stableRandom.get());
// Check evaluation status
NOut.println("Evaluated? " + stableRandom.isEvaluated());
NOut.println("Valid? " + stableRandom.isValid());
9.2 Cached Value
NCachedValue helps you cache expensive computations or resources. It evaluates a Supplier once, stores the result, and reuses it until the cache expires or is invalidated. You can configure expiry policies to automatically refresh values.
// Example 1: Cache with expiry
NCachedValue<Double> cachedRandom = NCachedValue.of(Math::random)
.setExpiry(Duration.ofSeconds(5));
// First call computes and caches the value
NOut.println("First value = " + cachedRandom.get());
// Subsequent calls reuse the cached value (within 5 seconds)
NOut.println("Cached value = " + cachedRandom.get());
// Invalidate to force recomputation
cachedRandom.invalidate();
NOut.println("New value after invalidate = " + cachedRandom.get());
Sometimes a computation may fail (for example, a remote call).NCachedValue can automatically retry, retain the last good value on failure, and recover gracefully. This makes it ideal for unstable resources or intermittent network services.
// Example 2: Cache with retries and fallback
AtomicInteger counter = new AtomicInteger();
NCachedValue<Integer> cached = NCachedValue.of(() -> {
int attempt = counter.incrementAndGet();
if (attempt % 2 == 0) {
throw new RuntimeException("Simulated failure");
}
return attempt;
})
.setRetry(3, Duration.ofMillis(100)) // retry up to 3 times
.retainLastOnFailure(true); // keep last value if failure occurs
// First call computes and caches
NOut.println("Value = " + cached.get());
// Next call may fail internally but still returns last good value
NOut.println("Resilient value = " + cached.get());
9.3 Task Set
NTaskSet lets you manage multiple asynchronous tasks as a single logical set. You can submit tasks as Future, CompletableFuture, Callable, Runnable, Supplier, or NCallable, and then wait for completion, get results, or handle errors in a consistent way.
This is useful for executing batches of work concurrently, coordinating results, or stopping remaining tasks once one finishes.
Creating a Task Set
NTaskSet tasks = NTaskSet.of()
.call(() -> "Hello from callable")
.run(() -> NOut.println("Running a simple task"))
.supply(() -> computeValue());
Waiting for All Tasks
Use join() to block until all tasks complete. Any exceptions are captured and can be retrieved via errors().
tasks.join(); // waits for all tasks
List<?> results = tasks.results(); // collect all results (null for failed tasks)
List<Throwable> errors = tasks.errors(); // collect exceptions if any
Getting the First Completed Result
first() returns the result of the first task that completes. Other tasks continue to run unless you pass true to cancel them:
String firstResult = tasks.first(); // peek first result, do not cancel others
String firstAndCancel = tasks.first(true); // returns first and cancels remaining tasks
firstOnly() is equivalent to first(true):
String winner = tasks.firstOnly();
Requiring All Tasks to Succeed
If you want to ensure all tasks completed successfully, use requireAll(). The first exception encountered will be thrown.
tasks.requireAll(); // throws CompletionException if any task failed
List<?> allResults = tasks.results();
Adding Tasks Dynamically
You can add tasks at any time:
tasks.add(CompletableFuture.supplyAsync(() -> "Dynamic task"));
tasks.call(() -> "Callable with executor", executorService);
tasks.run(() -> cleanup());
Cancelling All Tasks
You can cancel all running or pending tasks:
tasks.cancelAll(true); // true = may interrupt running tasks
Checking Task State
if (tasks.isDone()) {
NOut.println("All tasks completed");
}
if (tasks.hasError()) {
NOut.println("Some tasks failed");
}
NTaskSet provides a simple, fluent API for orchestrating concurrent work with flexible error handling and cancellation strategies.
9.4 Rate Limit Value
NRateLimitValue lets you control how often actions can be executed. For example, you can allow only 10 actions every 2 minutes, using a strategy like sliding window. This helps protect APIs, services, or expensive operations from overuse.
Example 1: Basic sliding window rate limit
NRateLimitedValue limiter = NRateLimitedValue.ofBuilder("example")
.withLimit("calls", 10).per(Duration.ofMinutes(2))
.withStrategy(NRateLimitDefaultStrategy.SLIDING_WINDOW)
.build();
for (int i = 0; i < 15; i++) {
NRateLimitValueResult res = limiter.take();
if (res.success()) {
NOut.println("Action " + i + " allowed at " + Instant.now());
} else {
NOut.println("Action " + i + " rejected. Retry after "
+ res.getRetryAfter().orElse(Duration.ZERO));
}
}
Example 2: Using claimAndRun
limiter.claimAndRun(() -> {
NOut.println("Doing a limited action...");
});
// Multiple independent limits (per minute, per day, etc.)
NRateLimitValue limiter2 = NRateLimitValue.ofBuilder("api-calls")
.withLimit("per-minute", 60).per(Duration.ofMinutes(1))
.withLimit("per-day", 1000).per(Duration.ofDays(1))
.build();
limiter2.claimAndRun(() -> {
NOut.println("API call allowed");
});
Example 3: Using claimAndCall
limiter.claimAndCall(() -> {
return expensiveOperation();
});
Features
Immediate vs Deferred Execution:
take()vsclaim()- Fluent builder for limits
- Multiple independent limits per value
- Optional persistence via store
- Strategy-based control (sliding window, custom)
- Thread-safe and suitable for concurrent environments
Notes
claimAndRun blocks until a permit is available and then executes the runnable.
take()or takeAndCall try to acquire permits immediately.NRateLimitValueResult provides fluent callbacks for success/failure handling.
- Multiple strategies can be defined via the factory.
Limits can be defined in duration units (
per(Duration)).
9.5 NRetryCall
NRetryCall
NRetryCall lets you execute tasks with retry capabilities, including configurable retry periods, recover actions, and handlers. This is useful for tasks that may fail intermittently, such as network calls or database operations.
Basic Example
// Create a simple retry call
NRetryCall<String> retryCall = NRetryCall.of(() -> {
NOut.println("Trying...");
if (new Random().nextBoolean()) {
throw new RuntimeException("Failed");
}
return "Success";
});
// Set maximum retries and retry period
retryCall.setMaxRetries(5)
.setRetryPeriod(Duration.ofSeconds(1));
// Execute blocking
String result = retryCall.call();
NOut.println("Result: " + result);
Asynchronous Execution
retryCall.callAsync();
// Or retrieve a future
Future<NRetryResult<String>> future = retryCall.callFuture();
NRetryResult<String> result = future.get();
if (result.isValid()) {
NOut.println("Succeeded: " + result.result());
} else {
NOut.println("Failed after retries");
}
Linear Backoff
Linear backoff increases the wait linearly with each attempt.
retryCall.setMultipliedRetryPeriod(Duration.ofSeconds(1), 2.0);
// Waits: 0s, 2s, 4s, 6s, 8s...
Exponential Backoff
Exponential backoff increases the wait exponentially with each attempt.
retryCall.setExponentialRetryPeriod(Duration.ofSeconds(1), 2.0);
// Waits: 1s, 2s, 4s, 8s, 16s...
Custom Recover Action
You can provide a recovery callable if all retries fail.
retryCall.setRecover(() -> {
NOut.println("Recovering...");
return "Recovered Result";
});
String result = retryCall.callOrElse(() -> "Default Result");
Custom Handler
Handlers are notified of each result, success or failure.
retryCall.setHandler(result -> {
if (result.isValid()) {
NOut.println("Success: " + result.result());
} else {
NOut.println("Failure for retry call id: " + result.id());
}
});
Factory Usage with ID, persistent
NRetryCallFactory factory = NRetryCallFactory.of();
NRetryCall<String> retryCallWithId = factory.of("myCallId", () -> {
return "Task Result";
});
retryCallWithId.setMaxRetries(3)
.setRetryPeriod(Duration.ofSeconds(2))
.call();
Factory Usage with ID, non persistent
User random id, and dispose (try with resource)
NRetryCallFactory factory = NRetryCallFactory.of();
try(NRetryCall<String> retryCallWithId = factory.of("myCallId-"+UUID.randomUUID(), () -> {
return "Task Result";
}).maxRetries(3)
.retryPeriod(Duration.ofSeconds(2))){
retryCallWithId.call();
}
9.6 Load Balancer
NWorkBalancer lets you distribute jobs across multiple workers using configurable strategies. You can define weights, choose strategies like round-robin or least-load, and track running jobs for observability and control.
Note: The NWorkBalancer is decoupled from execution. It does not run the job when you submit it. Instead, it returns a NCallable that selects a worker according to the strategy and tracks metrics. You must explicitly call the returned NCallable to execute the task.
Basic Example: Weighted Distribution
NWorkBalancerFactory factory = NWorkBalancerFactory.of();
NWorkBalancer<String> workBalancer = factory.<String>ofBuilder("example")
.addWorker("WorkerA").withWeight(1)
.addWorker("WorkerB").withWeight(2)
.build();
NCallable<String> callable = workBalancer.of("hello", context -> {
NOut.println(NMsg.ofC(
"call worker %s/%s:%s jobName:%s jobId:%s",
context.getWorkerIndex() + 1,
context.getWorkersCount(),
context.getWorkerName(),
context.getJobName(),
context.getJobId()
));
NConcurrent.sleep(50 + new Random().nextInt(50));
return "hello from " + context.getWorkerName();
});
NTaskSet tasks = NTaskSet.of();
for (int i = 0; i < 50; i++) {
tasks.call(callable);
}
tasks.join();
Example: Using a Custom Strategy
NWorkBalancerFactory factory = NWorkBalancerFactory.of();
NWorkBalancer<String> workBalancer = factory.<String>ofBuilder("example")
.addWorker("WorkerA").withWeight(1)
.addWorker("WorkerB").withWeight(2)
.then()
.setStrategy(NWorkBalancerDefaultStrategy.ROUND_ROBIN)
.build();
NCallable<String> callable = workBalancer.of("hello", context -> {
NOut.println(NMsg.ofC(
"call worker %s/%s:%s jobName:%s jobId:%s",
context.getWorkerIndex() + 1,
context.getWorkersCount(),
context.getWorkerName(),
context.getJobName(),
context.getJobId()
));
NConcurrent.sleep(50 + new Random().nextInt(50));
return "hello from " + context.getWorkerName();
});
NTaskSet tasks = NTaskSet.of();
for (int i = 0; i < 50; i++) {
tasks.call(callable);
}
tasks.join();
NOut.println("-------------------------------------------------------------");
NOut.println(NMsg.ofC("runningJobsCount %s", workBalancer.getRunningJobsCount()));
NOut.println(NMsg.ofC("workerLoads %s", workBalancer.getWorkerLoads()));
Key Methods
getRunningJobs()
Returns a snapshot of all currently running jobs. Useful for monitoring, metrics aggregation, or custom cancellation logic.
getRunningJobsCount()
Returns the number of currently active jobs for this balancer.
getWorkers()
Returns the list of registered workers.
getWorkerLoad(String workerName)
Returns the load metrics for a specific worker.
of(String name, NWorkBalancerJob<T> job)
Wraps a job into an NCallable that will execute according to the balancer strategy. Each call tracks metrics independently.
getOption(String name) / getOptions()
Retrieve global options configured for this balancer. Options can customize worker behavior at runtime.
Factory Usage: NWorkBalancerFactory
Create balancers using a factory:
NWorkBalancerFactory factory = NWorkBalancerFactory.of();
NWorkBalancer<MyResult> balancer = factory.ofBuilder("my-balancer")
.addWorker("A").withWeight(1)
.addWorker("B").withWeight(2)
.setStrategy(NWorkBalancerDefaultStrategy.LEAST_LOAD)
.build();
You can also register custom strategies:
factory.defineStrategy("myStrategy", new MyCustomStrategy());
NWorkBalancer<MyResult> balancer2 = factory.ofBuilder("callId")
.setStrategy("myStrategy")
.build();
Notes
Each job submitted through
of()is automatically tracked.- You can inspect running jobs, worker loads, and metrics at any time.
- Strategies can be swapped dynamically to optimize load distribution.
9.7 Saga
NSagaCallable provides a structured way to define a series of steps with automatic compensation in case of failure. Each step can succeed or fail, and the saga system ensures that failed steps are undone according to the defined strategy.
This is useful for workflows that require atomicity across multiple independent operations, like distributed transactions, workflow orchestration, or resilient pipelines.
Example 1: Simple Saga with failure and compensation
@Test
public void testSaga() {
try(NSagaCallable<Object> saga = NSagaCallableBuilder.of()
.start()
.then("step 1", MyNSagaStep.asSuccessful(1))
.then("step 2", MyNSagaStep.asSuccessful(2))
.then("step 3", MyNSagaStep.asErroneous(3))
.then("step 4", MyNSagaStep.asSuccessful(4))
.end().build()) {
saga.call();
}
}
private static class MyNSagaStep implements NSagaStep {
String name;
boolean err;
public MyNSagaStep(String name, boolean err) {
this.name = name;
this.err = err;
}
public static MyNSagaStep asSuccessful(int name) {
return new MyNSagaStep("step " + name, false);
}
public static MyNSagaStep asErroneous(int name) {
return new MyNSagaStep("step " + name, true);
}
@Override
public Object call(NSagaContext context) {
if (err) {
NErr.println(Instant.now() + " : err call " + name);
throw new NIllegalStateException(NMsg.ofC("unexpected error at %s", name));
} else {
NOut.println(Instant.now() + " : call " + name);
}
return name;
}
@Override
public void undo(NSagaContext context) {
NOut.println(Instant.now() + " : undo " + name);
}
}
Example 2: Conditional Saga
NSagaCallable<Object> conditionalSaga = NSagaCallableBuilder.of()
.start()
.then("step 1", MyNSagaStep.asSuccessful(1))
.thenIf("conditional step", ctx -> ctx.getVar("shouldRun") != null && (boolean)ctx.getVar("shouldRun"))
.then("step 2", MyNSagaStep.asSuccessful(2))
.otherwise()
.then("step 3", MyNSagaStep.asSuccessful(3))
.end()
.build();
conditionalSaga.call();
Example 3: Saga with While Loop
NSagaCallable<Object> loopSaga = NSagaCallableBuilder.of()
.start()
.then("init counter", ctx -> {
ctx.setVar("counter", 0);
NOut.println("Counter initialized");
return null;
})
.thenWhile("loop while counter < 3", ctx -> (int)ctx.getVar("counter") < 3)
.then("increment counter", ctx -> {
int c = (int) ctx.getVar("counter");
ctx.setVar("counter", c + 1);
NOut.println("Counter incremented to " + (c + 1));
return null;
})
.end()
.then("final step", ctx -> {
NOut.println("Final counter value: " + ctx.getVar("counter"));
return null;
})
.end()
.build();
loopSaga.call();
Key Interfaces
NSagaCallable<T>: Represents the full saga callable.NSagaStep: A single step, with call and undo.
NSagaContext: Context for storing variables and passing data between steps.
NSagaCallableBuilder: Fluent builder for defining sagas.
NSagaCondition: Conditional branching in a saga (for thenIf or thenWhile).
NSagaStore: Optional persistence for saga state.
Status and Node Enums
NSagaNodeStatus: Status of a node (PENDING, RUNNING, FINISHED, FAILED, COMPENSATING, etc.).
NSagaStatus: Overall saga status (PENDING, RUNNING, SUCCESS, ROLLED_BACK, PARTIAL_ROLLBACK, FAILED).
Notes
Execution vs Definition: Building a saga only defines the workflow. Steps are executed when call() is invoked.Automatic Compensation: If a step fails, prior executed steps are automatically undone using their undo methods.Variable Sharing: Use NSagaContext to store and access variables across steps.Persistence: Implement NSagaStore to persist saga state for long-running workflows or distributed systems.
Step Status: Use NSagaCallable.status() to inspect progress, including compensations in progress.
9.8 Persistent Locks
NLock provides a flexible, high-level way to lock resources across threads and even processes. You can create locks from objects, paths, or resource IDs, and execute code safely while the lock is held. This ensures that critical sections are executed exclusively and consistently.
// Example 1: Create a lock from an object
NLock lock = NLock.ofPath(NPath.of("/path/to/resource.txt");
// Check lock status
Nout.println("Is locked? " + lock.isLocked());
// Run code while holding the lock
lock.runWith(() -> {
NOut.println("Executing critical section...");
});
// Check if current thread holds the lock
Nout.println("Held by current thread? " + lock.isHeldByCurrentThread());
NLock also supports locks tied to workspace resources or IDs, allowing inter-process synchronization. You can execute tasks immediately, with a timeout, or safely retrieve results using callWith.
NId resourceId = NId.of("net.thevpc.nuts:nuts#0.8.9");
// Lock tied to workspace resource
NLock idLock = NLock.ofIdPath(resourceId);
// Execute a task and get result
// The lock is process-safe and prevents other processes from entering the critical section
String result = idLock.callWith(() -> {
NOut.println("Working with locked resource...");
return "done";
}, 5, TimeUnit.SECONDS).orNull();
NOut.println("Result = " + result);
// Run immediately if lock is free
boolean executed = idLock.runWithImmediately(() -> {
NOut.println("Quick task executed under lock");
});
NOut.println("Was executed? " + executed);
10 Time Tracking
In Nuts,
- NProgressMonitor – Facilitates tracking and reporting of long-running tasks. It allows threads to report progress, handle cancellation, and provide estimated completion information. In addition, it supports structured progress monitoring, letting developers split tasks and assign weights to subtasks for fine-grained progress aggregation.
10.1 Progress Monitoring
Long-running or multi-step operations benefit from structured progress tracking. NProgressMonitor provides a flexible, hierarchical, and thread-safe way to track, report, and manage task progress. It supports splitting, weighting, cancellation, suspension, and undoing progress, making it suitable for both simple and complex workflows.
Key features:
- Basic Progress Tracking – Update progress using setProgress(double progress) or as a fraction of total work setProgress(long current, long max). Events are emitted for start, progress, completion, undo, cancellation, and suspension.
- Hierarchical Progress – Split a monitor into subtasks using split(int count) or split(double... weights) to assign relative weights. This allows aggregation of multiple subtask progress into a single overall progress.
- Listeners & Reporting – Attach listeners via addListener(NProgressListener) to handle progress events programmatically. Output can be printed to streams or loggers using NProgressMonitors.of().ofPrintStream(...) or ofLogger(...).
- Structured Execution – runWith(Runnable) and runWithAll(Runnable...) integrate progress tracking into actual task execution. Each runnable can be associated with a subtask and progress weight.
- Indeterminate Progress – Supports tasks with unknown total duration using setIndeterminate().
- Estimated Duration – Automatically calculates elapsed and remaining time via getEstimatedRemainingDuration() and getEstimatedTotalDuration().
- Convenient Defaults – NProgressMonitor.of() returns the current monitor if one exists, or a silent fallback otherwise.
- ANSI-friendly Output – Works seamlessly with NText/NTextStyle for styled terminal output.
NProgressMonitor monitor = NProgressMonitor.of(event -> {
NOut.println(event);
});
monitor.setProgress(0);
monitor.setProgress(0.2);
monitor.setProgress(1.0);
monitor.complete();
Example: Structured Subtasks
NProgressMonitor monitor = NProgressMonitor.of(); // scoped monitor or silenced one
NProgressMonitor[] subtasks = monitor.split(3); // 3 weighted subtasks
subtasks[0].runWith(() -> doWork("Task A"));
subtasks[1].runWith(() -> doWork("Task B"));
subtasks[2].runWith(() -> doWork("Task C"));
Example: Integration with Runnable Execution
NProgressMonitor.of().runWithAll(
tasks.stream()
.map(task -> (Runnable) () -> processTask(task))
.toArray(Runnable[]::new)
);
These capabilities make NProgressMonitor a robust tool to structure, visualize, and control progress in multi-step or parallel operations, including support for cancellation, suspension, and progress weighting.
Terminal & ASCII Progress Rendering
In addition to structured progress monitoring, NAF allows rendering progress directly in the terminal, including an ASCII progress bar with messages:
for (int i = 0; i < 100; i++) {
Thread.sleep(100);
NTerminal.of()
.printProgress((i / 100f), NMsg.ofC("Processing item %s", i));
}
This will render a live progress bar with the associated message, updating in-place in the terminal. It works on both POSIX terminals and Windows terminals via Jansi, leveraging the same styling framework (NText/NTextStyle) used elsewhere.
Integration with IO Streams
For monitoring IO progress, Nuts provides NInputStreamMonitor, which wraps an input stream to log or trace progress dynamically:
NInputStreamMonitor monitor = NInputStreamMonitor.of()
.source(new FileInputStream("/some/path"))
.logProgress(true)
.traceProgress(false);
NProgressListener listener= event->NOut.println(NMsg.of("progress : %s",event.getProgress()));
NInputSource monitoredSource = NInputSource.of(
monitor.progressFactory(()->listener)
.length(NPath.of("/some/path").length()) //estimated length
.create()
);
// Reading the bytes will show progress in the console
byte[] bytes=monitoredSource.readBytes();
- NInputStreamMonitor wraps the source stream.
- NProgressListener receives events with progress information.
- setLength() allows estimating progress if the total size is known.
- This integrates seamlessly with the terminal output, leveraging NOut and NMsg for styled messages. This demonstrates that progress monitoring in Nuts is not limited to computations—it can be applied to IO operations in a clean, observable way.
11 External Commands & Processes
Nuts provides a robust and unified API to manage external processes, execute commands, and interact with the underlying OS in a platform-independent way. This includes:
- Executing shell commands or external binaries.
- Capturing output, error streams, and exit codes.
- Filtering and manipulating running processes.
- Gracefully handling interactivity and embedded execution.
11.1 Process Discovery and Management
NPs allows listing and filtering running processes across platforms and optionally killing them:
To create a new process
// List all visible processes
for (NPsInfo nPsInfo : NPs.of().getResultList()) {
NOut.println(nPsInfo);
}
// Kill all Tomcat Java processes
if (NPs.of().isSupportedKillProcess()) {
NPsInfo[] tomcats = NPs.of()
.setPlatformFamily(NPlatformFamily.JAVA)
.getResultList()
.stream()
.filter(p -> p.getName().equals("org.apache.catalina.startup.Bootstrap"))
.toArray(NPsInfo[]::new);
for (NPsInfo ps : tomcats) {
if (NPs.of().killProcess(ps.getPid())) {
NOut.print(NMsg.ofC("Tomcat process killed (%s).\n", ps.getPid()));
} else {
NOut.print(NMsg.ofC("Tomcat process could not be killed (%s).\n", ps.getPid()));
}
}
}
- NPs.of() provides a snapshot of running processes.
- Processes can be filtered by name, platform family, or other criteria.
- killProcess(pid) supports inter-process termination when the platform allows it.
11.2 Executing External Commands
NAF provides a high-level API to execute external commands, whether locally or remotely, in a concise, structured, and cross-platform manner. Using NExec, you can run processes, capture their output, handle errors, and even execute them on remote hosts via SSH—all without the verbosity of the standard Java Process API.
Key Features
- Embedded & Local Execution: Run commands in the current JVM or OS environment.
- Remote Execution: Execute commands on SSH-enabled hosts transparently.
- Automatic Output Capture: Grab stdout and stderr without manually reading streams.
- Run Java Artifacts Directly: Download a JAR and its dependencies from Maven or remote repositories and execute it automatically.
- Fail-Fast Control: Stop execution immediately on errors if desired.
- Minimal Boilerplate: Avoid complex Runtime.exec() and thread-based stream consumption.
To create a new process
// Simple embedded command execution
String result = NExec.of("info").grabbedAll();
NOut.println(result);
.of("info")creates the command to execute..grabbedAll()captures the full output (stdout + stderr) as a string.No exceptions are thrown automatically; the caller can inspect result or
resultCode()to decide if the execution succeeded.
Example: Executing a shell command with NSH
String result = NExec.of()
.ommand(NConstants.Ids.NSH, "-c", "ls")
.failFast(true)
.grabbedOut(); // implicitly calls .grabOut().run
NOut.println("Result:");
NOut.println(result);
- Nuts provides embedded shells (NSH) for running commands in a sandboxed environment.
- Output is automatically sanitized to remove terminal formatting unless explicitly requested.
Special Executor IDs
Nuts can handle commands with special identifiers or remote resources:
String result = NExec.of()
.executorOptions("--bot")
.command("com.mycompany:my-remote-artifact")
.command("list", "-i")
.grabbedAll();
NOut.println(result);
- Useful for running workspace-bound tools or remote executables.
- Works with Maven coordinates or URLs pointing to executable jars.
Remote & Structured Command Execution
NExec is not limited to local commands: you can execute processes on remote systems via SSH (or any supported executor) and easily capture stdout/stderr with minimal boilerplate:
NExec u = NExec.of()
.at("ssh://me@myserver") // Execute on remote server
.in(NExecInput.ofNull()) // No stdin input
.command("ps", "-eo", // Command + arguments
"user,pid,%cpu,%mem,vsz,rss,tty,stat,lstart,time,command")
.grabErr() // Capture stderr
.failFast(true) // Optional: fail immediately on error
.grabOut(); // Capture stdout
// Access results
String stdout = u.grabbedOut();
String stderr = u.grabbedErr();
int exitCode = u.getResultCode();
NOut.println("Exit code: " + exitCode);
NOut.println("Output:\n" + stdout);
Key points:
- at(...) allows specifying a remote target (SSH, etc.).
- grabOut() / grabErr() capture output streams automatically.
- failFast(true) ensures that any failure will stop execution immediately.
- No need for Runtime.exec() boilerplate or manual stream handling.
This makes NExec a powerful, concise, and cross-platform alternative to the traditional Java Process API.
Why NExec is Better than Runtime.exec()
Using Runtime.exec() requires verbose boilerplate to:
- manage stdin/stdout/stderr streams,
- handle threading to avoid blocking,
- wait for process completion,
- and manually check exit codes.
With NExec, all of this is simplified and made cross-platform, while also supporting structured output handling, remote execution, and direct execution of downloaded Java artifacts transparently.
12 Utilities and Support
12.1 NLog for elegant Logging
NLog — Structured Developer Logging in Nuts
NLog is the structured logging engine for the Nuts platform. It unifies semantic operations (NMsgIntent), rich ANSI/NTF terminal rendering, thread-scoped context propagation (NLogScope), execution metrics, and object-preserving structured messages (NMsg).
Because NMsg retains raw parameter objects and AST nodes without eagerly flattening them to strings, NLog adapts seamlessly to any destination—rendering rich ANSI text for terminal debugging, plain text for rolling files, and fully typed JSON payloads for centralized observability pipelines (ELK, Loki, OpenTelemetry).
1. Architectural Overview & Comparison
Classic logging frameworks (JUL, SLF4J, Log4j2) typically interpolate arguments into fixed strings at the log boundary. NLog preserves raw objects and metadata inside NMsg, allowing the downstream appender or sink to determine the final representation.
Traditional Logging (SLF4J / Log4j / JUL):
[Caller] ─── (String template + Args) ───► Eager toString() ───► [Fixed Text Appender]
NLog Pipeline:
[Caller] ───► [NMsg: Raw Objects + AST + Intent + Metrics]
│
├──► Terminal Console (ANSI Colors & NTF Markdown Styles)
├──► Workspace File (Clean Plain Text)
├──► JSON / Collector (Structured Key-Value Payload)
└──► SLF4J Bridge (Location-Aware Delegation)
Feature Comparison
| Capability | Standard Java Logging (JUL) | SLF4J / Log4j2 | Nuts NLog |
|---|---|---|---|
Object Preservation | Discarded after formatting | Flattened or requires JSON wrappers |
Retained in NMsg parameters |
JSON / Structured Output | Requires external appenders | Requires Jackson/Logstash encoders | Native (objects preserved in AST) |
Message Styling | Raw text only | Manual ANSI escape sequences | Native NTF (colors, backticks, styles) |
Context Propagation | Thread-bound MDC (String-only) | Thread-bound MDC |
NLogScope (prefixes, objects, custom sinks) |
Operation Semantics | Tied to level (INFO, WARN) | Unstructured markers |
First-class NMsgIntent (START, CACHE, etc.) |
Workspace Awareness | Static per ClassLoader | Static per ClassLoader |
Contextual per workspace/session via |
Template Reusability | None | Limited |
Reusable NMsgBuilder streams |
2. Basic Usage
Obtain an NLog instance using a class reference or category name:
public class ServiceRunner {
private /* not static*/ final NLog LOG = NLog.of(ServiceRunner.class);
public void execute(String path, long timeoutMs) {
LOG.info(NMsg.ofC("Starting worker on path %s", path));
try {
// Task execution
LOG.debug(NMsg.ofC("Polling resource with timeout %d ms", timeoutMs));
} catch (Exception ex) {
LOG.error(NMsg.ofC("Failed to execute worker on %s", path)
.withThrowable(ex)
);
}
}
}
Direct Log Levels
Convenience methods accept NMsg instances or lazy Supplier
12.2 NOptional
NOptional
NOptional<T> is a tri-state container that evolves Java’s Optional for real-world enterprise and library code. By moving beyond the simple Present/Absent model, NOptional enables safer, more expressive, and composable null-safe code, closely mirroring the capabilities of modern languages like TypeScript and Kotlin.
It explicitly models three distinct outcomes of any computation or data lookup:
| State | Meaning | Typical Origin | Java Optional Equivalent |
|---|---|---|---|
PRESENT | A value is available (may be null) | Successful evaluation | |
EMPTY | The value is logically absent | Not found, filtered out, blank input | |
ERROR | A technical or logical failure occurred | Exception during evaluation, explicit error | (not supported) |
This distinction is fundamental: a missing configuration key (EMPTY) is not the same as a malformed XML file that could not be parsed (ERROR). Collapsing both into a single “absent” state loses diagnostic power and forces awkward external try/catch blocks.
NOptional is designed for fluent, composable, null-safe code while remaining fully interoperable with the JDK (asOptional(), jstream(), etc.).
1. Core Design Principles
1. Tri-state semantics – Present / Empty / Error are first-class and never collapsed.
2. Named values & rich diagnostics – Every empty or error state can carry a descriptive NMsg. Calling
get()produces meaningful exceptions automatically.3. Configurable exception factories – Applications and libraries can plug in their own exception types via ExceptionFactory.
4. Deep, short-circuiting navigation –
then(...)is the direct equivalent of the safe-navigation operator (?.) found in Kotlin, TypeScript, C#, etc.5. Blank-aware operations – Integration with NBlankable makes empty strings, whitespace-only strings, empty collections/arrays, and custom blank objects first-class citizens.
6. Explicit recovery points – Dedicated methods for recovering from empty vs. error (ifEmptyUse, onErrorUse, ifErrorThrow, …).
7. Zero-surprise terminal operations –
get(),orNull(),orElse(...),orDefault(), etc. have precise, documented contracts.
2. Creating NOptionals
Basic Factories
// Explicitly allows null (PRESENT holding null)
NOptional.ofNullable(value); // value may be null
NOptional.ofNullable(value, () -> NMsg.ofC("custom empty message"));
// Present only if non-null; otherwise EMPTY
// Treats null as EMPTY
NOptional.of(value);
NOptional.of(value, () -> NMsg.ofC("missing %s", "user"));
// Explicit empty
NOptional.ofEmpty();
NOptional.ofEmpty(() -> NMsg.ofC("user not found"));
NOptional.ofNamedEmpty("user"); // → "missing user"
NOptional.ofNamedEmpty(NMsg.ofC("user"));
// Explicit error
NOptional.ofError(() -> NMsg.ofC("failed to load config"));
NOptional.ofError(throwable);
NOptional.ofNamedError("config", throwable);
// From Java Optional
NOptional.ofOptional(javaOptional);
NOptional.ofNamedOptional(javaOptional, "user");
Important Distinction: Present-with-null
Unlike Java’s Optional, NOptional can hold an explicit null value in the PRESENT state:
| Call | State | | | Notes |
|---|---|---|---|---|
| PRESENT | true | true | Explicit null is preserved |
| EMPTY | false | false | Null is treated as absence |
| empty | false | — | Java collapses null into empty |
This allows callers to distinguish between:
- “I received a null” (PRESENT + null)
- “The value is missing / not found” (EMPTY)
- “An error occurred while retrieving the value” (ERROR)
Collection Helpers
// Exactly one element expected
NOptional.ofSingleton(collection); // EMPTY if 0, PRESENT if 1, ERROR if >1
NOptional.ofNamedSingleton(collection, "user");
// First element (ignore the rest)
NOptional.ofFirst(collection);
NOptional.ofNamedFirst(collection, "user");
Lazy / Deferred Evaluation
NOptional.ofSupplier(() -> expensiveLookup());
NOptional.ofCallable(() -> service.findUser(id));
Named Values and Custom Messages
By using ofNamed("user"), your resulting exception (when calling get()) is automatically generated with a descriptive message like "Missing required value: user." This eliminates the need for manual exception message creation and relies on the configurable ExceptionFactory for consistent error types.
3. Terminal Operations – Retrieving the Value
| Method | Present | Empty | Error |
|---|---|---|---|
| returns value | throws NEmptyOptionalException | throws NErrorOptionalException |
| returns value | throws NEmptyOptionalException with custom message | throws NErrorOptionalException with cutom message |
| returns value | returns null | returns null |
| returns value | returns fallback | returns fallback |
| returns value | returns evaluated supplier | returns evaluated supplier |
| returns value | returns configured default | returns configured default |
| returns value | returns configured or JVM default | returns configured or JVM default |
| returns value | throws supplied exception | throws supplied exception |
Boolean helpers (useful for flags):
boolean flag = optional.orFalse(); // EMPTY/ERROR → false
boolean flag = optional.orTrue(); // EMPTY/ERROR → true
4. State Inspection
boolean isPresent() / isNotPresent()
boolean isEmpty()
boolean isNull() // PRESENT holding null
boolean isError()
NOptionalType type() // PRESENT | EMPTY | ERROR
Supplier<NMsg> message()
Throwable getError()
ExceptionFactory getExceptionFactory()
Rule of thumb
Use
get()when the value must exist (assertion).Use
orNull()/orElse(...)when absence is acceptable.Prefer
orDefault()when a sensible default has been declared withwithDefault(...).
5. Transformations (Map/Filter)
Mapping Family
| Method | Behaviour |
|---|---|
| Classic map; EMPTY/ERROR stay EMPTY/ERROR |
| Maps only when PRESENT |
| Maps only when PRESENT and value ≠ null |
| Maps only when PRESENT and not blank (NBlankable) |
| Alias of mapIfNotBlank |
| Conditional map (same type) |
| Full if/else map |
| Respects withDefault(...) |
| Maps only when not ERROR |
| Flat-map to another NOptional |
// NOptional: uses the integrated NBlankable logic
String cleanToken = NOptional.of(readProperty("auth.token"))
.mapIfNotBlank(String::trim) // Filters null, "", and " "
.orNull();
Fluent Navigation & Mapping
Safe Deep Traversal – then(...)
NOptional introduces clean mechanisms for asserting a value's presence and providing context-rich exceptions, dramatically improving debugging and developer experience.
then short-circuits on EMPTY or ERROR and never throws.
// Classic Java
Number roadNumber = (app != null
&& app.person != null
&& app.person.address != null
&& app.person.address.road != null)
? app.person.address.road.number
: null;
// NOptional
Number roadNumber = NOptional.of(app)
.then(a -> a.person)
.then(p -> p.address)
.then(a -> a.road)
.then(r -> r.number)
.orNull();
Combining Assertion and Chaining
NOptional chains can freely combine passive operations (then(...)) with assertive ones (get()) to enforce mandatory states within a larger flow.
// Equivalent to: var roadNumber = app?.person?.address!.road?.number ?? 0;
Number roadNumber = NOptional.of(app)
.then(v -> v.person)
.then(v -> v.address)
.get() // ASSERT: Throws if address is null/empty
.then(v -> v.road)
.then(v -> v.number)
.orElse(0); // Coalesce: Fallback to 0 if road or number is null/empty
| Concept | Java Equivalent (Verbose) | NOptional (Expressive) | Equivalent TS |
|---|---|---|---|
Mandatory Value Check | | | |
Null-Safe Mapping | | | |
Nullish Coalescing | | | |
Error Recovery | | | (No direct TS equivalent) |
Optional Chaining for Deep Traversal (then(...))
NOptional provides the then(...) method for fluent and safe traversal of deep object graphs, acting as a direct analog to the safe-navigation operator (?.) in modern languages. It short-circuits the chain if any part returns null or is EMPTY.
| Code Style | Example |
|---|---|
Java (Verbose) | |
NOptional | |
Combining Assertion and Navigation
// Equivalent to: app?.person?.address!.road?.number ?? 0
Number roadNumber = NOptional.of(app)
.then(a -> a.person)
.then(p -> p.address)
.get() // ASSERT: address must be present
.then(a -> a.road)
.then(r -> r.number)
.orElse(0);
Filtering
optional
.filter(u -> u.getAge() >= 18)
.filter(u -> u.getAge() >= 18, () -> NMsg.ofC("must be 18+, got %d", u.getAge()))
.filter(NMessagedPredicate...); // predicate that carries its own message
6. Error & Empty Recovery
// Fail fast on ERROR (EMPTY is left alone)
optional.ifErrorThrow();
// Recover from ERROR
optional.onErrorUse(() -> fallbackOptional);
optional.onError(defaultValue);
optional.onErrorEmpty(); // ERROR → EMPTY
// Recover from EMPTY
optional.ifEmptyUse(() -> fallbackOptional);
optional.onEmpty(defaultValue);
// Blank-aware recovery
optional.onBlank(defaultValue);
optional.onBlankUse(() -> fallback);
optional.onBlankEmpty();
optional.onNullEmpty();
optional.onNullUse(() -> fallback);
7. Side-effect & Conditional Execution
optional
.ifPresent(value -> log.info("got {}", value))
.ifNonPresent(() -> log.warn("missing"))
.ifNull(() -> log.debug("explicit null"))
.ifError(ex -> log.error("failed", ex))
.ifCondition(opt -> opt.isPresent() && someFlag, opt -> ...);
8. Defaults, Messages & Exception Factories
// Attach a default that is used by orDefault() / orDefaultOptional()
public NOptional<NFetchStrategy> getFetchStrategy() {
return NOptional.ofNamed(strategy, "fetchStrategy")
.withDefault(() -> NFetchStrategy.ONLINE);
}
// Usage
NFetchStrategy fs = session.getFetchStrategy()
.mapIfNotBlank(s -> NFetchStrategy.parse(s).orNull())
.orDefault();
// Custom messages
optional.withName("user email"); // → "missing user email"
optional.withMessage(() -> NMsg.ofC("..."));
// Per-instance exception factory
optional.withExceptionFactory(myFactory);
// Global factory (affects all NOptionals)
NOptional.setDefaultExceptionFactory(myFactory);
The default factory produces:
- NEmptyOptionalException / NDetachedEmptyOptionalException
- NErrorOptionalException / NDetachedErrorOptionalException depending on whether an NWorkspace context is available.
9. Interoperability
Optional<T> jdk = noptional.asOptional(); // ERROR becomes empty
NStream<T> stream = noptional.stream();
Stream<T> jstream = noptional.jstream();
10. Why NOptional Instead of java.util.Optional?
Limitation of Optional | How NOptional solves it |
|---|---|
| No ERROR state | First-class ERROR + recovery methods |
Verbose deep chaining (flatMap) | |
| No named values / poor diagnostics | ofNamed, withName, rich NMsg exceptions |
| No blank handling | mapIfNotBlank, onBlank… via NBlankable |
| Exception type hard-coded | Pluggable NOptionalExceptionFactory |
| Defaults must be supplied at every call site | withDefault + |
| No collection helpers | ofSingleton, ofFirst, … |
NOptional remains fully compatible with functional style and can always be converted back to a JDK Optional when required.
11. Related Types
- NOptionalType – the three-state enum (PRESENT, EMPTY, ERROR)
- NBlankable – blank detection contract used by many methods
- NMsg – structured, localizable messages
- NOptionalExceptionFactory – custom exception creation
- NStream – the streaming counterpart
> NOptional is part of the Nuts framework. It is designed to be used both inside Nuts applications and as a standalone utility in any Java project that needs robust optional handling.
12.3 NStream
NStream<T> is A lazy, describable, sequential pipeline that can be executed when a terminal operation is called — exactly like Stream, plus the ability to ask “what is this pipeline?” at any moment before execution.
It wraps and extends Java’s Stream API to deliver describable, inspectable, and framework-integrated pipelines while remaining fully compatible with standard stream operations.
NStream is not a replacement for java.util.stream.Stream. It is a thin, zero-overhead wrapper that adds:
Structured pipeline descriptions (
describe())Seamless integration with NAF types (NElement, NMsg, NOptional, etc.)
- Convenient factories for arrays, iterables, iterators, optionals, and empty streams
Additional terminal helpers (
findSingleton(),toSortedSet(), typed primitive arrays, etc.)- Safe consumption semantics and explicit close handling
It implements Iterable<T>, NRedescribable<NStream<T>>, and AutoCloseable.
1. Examples of usage
// From values
NStream<Integer> s1 = NStream.of(1, 2, 3, 4, 5);
// From a Java Stream
Stream<String> javaStream = Stream.of("a","b","c");
NStream<String> s2 = NStream.ofStream(javaStream);
// From Iterable or Iterator
List<Double> numbers = List.of(0.1, 0.2, 0.3);
NStream<Double> s3 = NStream.of(numbers);
// From any object
NStream<Double> s3 = NStream.ofSingleton(1.0);
// From Optional
NOptional<Double> number = NOptional.of(1);
NStream<Double> s3 = NStream.ofOptional(number);
NStream works transparently over all these, giving you a single, uniform API.
NStream supports familiar operations:
NStream<Integer> s = NStream.of(1,2,3,4,5)
.filter(x -> x % 2 == 0)
.map(x -> x * 10);
You can use any combination of map, filter, flatMap, sorted, etc., just like a standard Java Stream.
NStream<Integer> s = NStream.ofArray(1,2,3,4,5)
.filter(NPredicate.of(x -> x % 2 == 0)
.withDesc(() -> NElement.ofString("even numbers")))
.map(NFunction.of(x -> x * 10)
.withDesc(NElement.ofObject("mul", NElement.ofNumber(10))));
NElement description = s.describe();
NOut.println(description);
2. Creating an NStream
Quick Reference – Factory Methods
NStream.ofArray(T...)
NStream.ofIntArray(int...)
NStream.ofLongArray(long...)
NStream.ofDoubleArray(double...)
NStream.ofBooleanArray(boolean...)
NStream.ofByteArray(byte...)
NStream.ofCharArray(char...)
NStream.ofShortArray(short...)
NStream.ofFloatArray(float...)
NStream.ofStream(Stream<T>)
NStream.ofIterable(Iterable<T>)
NStream.ofIterator(Iterator<T>)
NStream.ofOptional(NOptional<T> | Optional<T>)
NStream.ofSingleton(T)
NStream.ofEmpty()
From values / arrays
// Varargs
NStream<Integer> s1 = NStream.ofArray(1, 2, 3, 4, 5);
// Primitive arrays (with automatic description)
NStream<Integer> ints = NStream.ofIntArray(1, 2, 3);
NStream<Long> longs = NStream.ofLongArray(10L, 20L);
NStream<Double> doubles= NStream.ofDoubleArray(1.1, 2.2);
NStream<Boolean> bools = NStream.ofBooleanArray(true, false);
// similarly: ofByteArray, ofCharArray, ofShortArray, ofFloatArray
From Java Stream
Stream<String> javaStream = Stream.of("a", "b", "c");
NStream<String> s2 = NStream.ofStream(javaStream);
From Iterable / Iterator
List<Double> numbers = List.of(0.1, 0.2, 0.3);
NStream<Double> s3 = NStream.ofIterable(numbers);
Iterator<String> it = ...;
NStream<String> s4 = NStream.ofIterator(it);
From Optional
NOptional<Double> nOpt = NOptional.of(1.0);
NStream<Double> s5 = NStream.ofOptional(nOpt);
Optional<String> jOpt = Optional.of("hello");
NStream<String> s6 = NStream.ofOptional(jOpt);
Singleton & empty
NStream<Double> singleton = NStream.ofSingleton(1.0);
NStream<String> empty = NStream.ofEmpty();
All factory methods produce a uniform NStream surface, so downstream code never needs to know the original source type.
3. Core Stream Operations
NStream supports the familiar intermediate and terminal operations of Java Streams:
NStream<Integer> result = NStream.ofArray(1, 2, 3, 4, 5)
.filter(x -> x % 2 == 0)
.map(x -> x * 10)
.sorted()
.distinct();
Intermediate operations
| Method | Description |
|---|---|
| Transform each element |
| Transform that may throw checked exceptions |
| Transform with error recovery |
| Keep matching elements |
| Flatten nested structures |
flatMapToInt / flatMapToLong / flatMapToDouble | Flatten to primitive streams |
| Keep only instances of the given type (cast) |
nonNull | Drop null elements |
nonBlank | Drop null / blank strings and any NBlankable |
distinct / distinct | Remove duplicates |
sorted / | Sort |
| Windowing |
| Append another stream / iterator |
Terminal operations
| Method | Description |
|---|---|
| MethodDescriptiontoList() / toSet() / toSortedSet() / toOrderedSet() | Materialize collections |
| toArray(IntFunction) | Typed array |
| toIntArray() / toLongArray() / … | Primitive arrays |
| toMap / toOrderedMap / toSortedMap | Build maps |
| groupBy / groupedBy | Grouping |
| findFirst() / findAny() / findLast() | Find elements |
| findSingleton() | Exactly one element (throws otherwise) |
| count() | Element count |
| min / max | ExtremumanyMatch / allMatch / noneMatchPredicates |
| collect(...) | Custom collectors |
| jstream() | Convert back to a Java Stream |
| iterator() | Obtain an NIterator |
Important: Most terminal operations consume the stream. Calling them twice yields undefined behaviour (empty result or exception). Prefer collecting once into a list/set if you need multiple passes.
4. Describable Pipelines
The primary value of NStream is the ability to describe the pipeline for logging, debugging, reporting, and NAF search/command introspection. Use the describable functional interfaces NFunction, NPredicate, NComparator etc.:
NStream<Integer> pipeline = NStream.ofArray(1, 2, 3, 4, 5)
.filter(NPredicate.of(x -> x % 2 == 0)
.withDesc(() -> NElement.ofString("even numbers")))
.map(NFunction.of(x -> x * 10)
.withDesc(NElement.ofObject("mul", NElement.ofNumber(10))));
NElement description = pipeline.describe();
NOut.println(description);
Typical structured output:
{
"source": [1, 2, 3, 4, 5],
"operations": [
{ "filter": "even numbers" },
{ "map": { "mul": 10 } }
]
}
- Descriptions are optional. You can freely mix plain Java lambdas with describable ones.
withDescription(Supplier<NElement>)can also be applied at the stream level.Because NStream implements NRedescribable, you can re-describe an existing pipeline without rebuilding it.
5. Resource Management
NStream implements AutoCloseable. Always close streams that hold resources (files, network, etc.):
try (NStream<String> lines = NStream.ofStream(Files.lines(path))) {
lines.filter(...).forEach(...);
}
You can also register close handlers:
stream.onClose(() -> resource.release());
6. Why Use NStream?
- Visibility : describe() yields a structured NElement view of the entire pipeline – ideal for debugging, logging and NAF search commands.
- Uniform API : One surface for arrays, iterables, iterators, optionals and Java streams.
- NAF Integration : Native support for NElement, NMsg, NOptional, NComparator, etc.
- Extra helpers : findSingleton(), typed primitive arrays, nonBlank(), coalesce(), ordered/sorted maps & sets.
- Optional overhead : If you never call describe(), behaviour and performance are essentially identical to a plain Java Stream.
NStream shines in NAF search pipelines, command implementations, logging, reporting, and any place where you want inspectable data-processing steps.
Notes & Best Practices
1. Prefer
NStream.ofArray(...)/ofIntArray(...)when you want automatic source description.2. Use
findSingleton()when the pipeline is expected to produce exactly one element; it fails fast otherwise.3. Convert back to a Java Stream only when you need APIs that accept Stream (e.g. third-party libraries):
stream.jstream().- 4. Descriptions are evaluated lazily; expensive description suppliers are safe.
- 5. Because the stream is consumable, collect early if you need multiple terminal operations.
For the full method list and Javadoc, see the NStream interface.
13 Application Lifecycle
13.1 Nuts Application Framework
Using Nuts Application Framework (NAF)
Using nuts is transparent as we have seen so far. It's transparent both at build time and runtime. However, nuts can provide our application a set of unique helpful features, such as install and uninstall hooks, comprehensive command line support and so on.
To create your first NAF application, you will need to add nuts as a dependency and change your pom.xml as follows:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>net.thevpc.nuts</groupId>
<artifactId>nuts-lib</artifactId>
<version></version>
</dependency>
<dependency>
<groupId>jexcelapi</groupId>
<artifactId>jxl</artifactId>
<version>2.4.2</version>
</dependency>
</dependencies>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<nuts.application>true</nuts.application>
</properties>
</project>
Please take note that we have added a property nuts.application=true. Actually this is not mandatory, but this will help nuts package manager detect that this application uses NAF before downloading its jar (the information will be available in the pom.xml descriptor on the remote repository).
Then we will add some cool features to our application. We write a dummy message whenever the application is installed, uninstalled or updated. We will also add support to "--file=[path]" argument to specify the workbook path.
package com.mycompany.app;
import java.io.File;
import jxl.Workbook;
import jxl.write.WritableWorkbook;
public class App implements NApplication {
public static void main(String[] args) {
// just create an instance and call runAndExit in the main method
// this method ensures that exist code is well propagated
// from exceptions to caller processes
NApp.builder(args).run();
}
@Override
public void run() {
NCmdLine cmd = NApplication.of().cmdLine();
NRef<File> file = NRef.of(new File("file.xls"));
cmd.matcher()
.when("--file").asEntry(a->ref.set(a.stringValue()))
.when("--fill").asEntry(a->{}/*process other options here ... */)
.withDefaults()
.requireAll();
if(cmd.isCompleteMode()){
cmd.printCompleteResult();
return;
}
try {
WritableWorkbook w = Workbook.createWorkbook(file.get());
NOut.printf("Workbook just created at %s%n", file.get());
} catch (Exception ex) {
ex.printStackTrace(NErr.err().asPrintStream());
}
}
@Override // this method is not required, implement when needed
public void onInstallApplication() {
NOut.println(NMsg.ofC("we are installing My Application : %s%n", NApplication.of().getId()));
}
@Override // this method is not required, implement when needed
public void onUninstallApplication() {
NOut.println(NMsg.ofC("we are uninstalling My Application : %s%n", NApplication.of().getId()));
}
@Override // this method is not required, implement when needed
public void onUpdateApplication() {
NOut.println(NMsg.ofC("we are updating My Application : %s%n", NApplication.of().getId()));
}
}
Now we can install or uninstall the application and see the expected messages.
nuts -y install com.mycompany.app:my-app
nuts -y uninstall com.mycompany.app:my-app
13.2 Your first Application using nuts
Building Applications with Nuts Application Framework (NAF)
This guide walks through packaging and running a Java application with the nuts package manager, then progressively adopting the Nuts Application Framework (NAF) to get lifecycle hooks, structured command-line parsing, and shell completion — all from the same codebase.
1. The problem NAF solves
A plain Maven project with runtime dependencies is not directly executable. The usual fixes — maven-shade-plugin, maven-assembly-plugin, manually editing META-INF/MANIFEST.MF — all require baking a specific packaging strategy into the build, and none of them give you application lifecycle hooks (install/update/uninstall) or shell completion for free.
nuts sidesteps this: it resolves dependencies and the main class at run time, from the artifact's own metadata, so a standard mvn clean install output is already runnable.
1.1 Generate a project
mvn archetype:generate -DgroupId=com.mycompany.app -DartifactId=my-app \
-DarchetypeArtifactId=maven-archetype-simple -DarchetypeVersion=1.4 -DinteractiveMode=false
1.2 Add a dependency
<dependency>
<groupId>jexcelapi</groupId>
<artifactId>jxl</artifactId>
<version>2.4.2</version>
</dependency>
1.3 Write the app
package com.mycompany.app;
import java.io.File;
import jxl.Workbook;
import jxl.write.WritableWorkbook;
public class App {
public static void main(String[] args) {
try {
WritableWorkbook w = Workbook.createWorkbook(new File("any-file.xls"));
System.out.println("Workbook just created");
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
1.4 Build and run — no shading required
mvn clean install
nuts install com.mycompany.app:my-app
nuts my-app
nuts detects, resolves, and downloads dependencies at run time. The app is installed from the local Maven repository; deploy it to a public repository to make it accessible elsewhere. You can also skip installation entirely and run a jar directly:
nuts -y com my-app-1.0.0-SNAPSHOT.jar
If a jar defines multiple public static void main classes, nuts prompts for which one to run, interactively.
2. Opting into NAF
NAF adds install/update/uninstall hooks, structured command-line parsing, and shell completion support. Enable it by adding nuts as a compile dependency and flagging the artifact:
<dependency>
<groupId>net.thevpc.nuts</groupId>
<artifactId>nuts</artifactId>
<version>1.0.0.0</version>
</dependency>
<properties>
<nuts.application>true</nuts.application>
</properties>
nuts.application=true is optional but recommended: it lets nuts recognize a NAF app from the remote POM before downloading the jar.
3. Application lifecycle hooks
NAF dispatches to your class based on the invocation mode. Each mode has a dedicated annotation, all optional except
@NApp
@NAppRun:
| Annotation | Invoked when… | Required |
|---|---|---|
| Marks the class as a NAF application entry point | Yes |
| Normal execution ( | Yes |
| Shell requests completion candidates (Tab press) | No |
| | No |
| | No |
| | No |
Internally, dispatch is a single switch over the resolved mode:
switch (nApp.mode()) {
case RUN: nApp.application().run(); return;
case COMPLETE: /* isolated session, see §5 */ return;
case INSTALL: nApp.application().onInstallApplication(); return;
case UPDATE: nApp.application().onUpdateApplication(); return;
case UNINSTALL: nApp.application().onUninstallApplication(); return;
}
RUN and INSTALL/UPDATE/UNINSTALL all execute under the normal, interactive session — these are user-initiated actions, so confirmation prompts and terminal output are expected and appropriate. COMPLETE is different: see §5.
4. A complete example
package com.mycompany.app;
import java.io.File;
import jxl.Workbook;
import jxl.write.WritableWorkbook;
@NApp
public class App {
public static void main(String[] args) {
// NApp.builder(args).run() ensures exit codes propagate
// correctly from exceptions to the calling process.
NApp.builder(args).run();
}
static class Options {
File file = new File("file.xls");
}
@NAppRun
public void run() {
NCmdLine cmdLine = NApplication.of().cmdLine();
Options options = process(cmdLine);
try {
WritableWorkbook w = Workbook.createWorkbook(options.file);
NOut.printf("Workbook just created at %s%n", options.file);
} catch (Exception ex) {
ex.printStackTrace(NErr.err().asPrintStream());
}
}
@NAppComplete
public void complete() {
NCmdLine cmdLine = NApplication.of().cmdLine();
process(cmdLine);
cmdLine.printCompleteResult();
}
/**
* Single command-line walk, shared by run() and complete().
* NCmdLine already knows its own mode (cmdLine.isCompleteMode()),
* so the matcher calls behave correctly for both callers without
* any branching here — this method has no notion of "mode" at all.
*/
private Options process(NCmdLine cmdLine) {
Options options = new Options();
cmdLine.matcher()
.when("--file").asEntry(a -> options.file = new File(a.stringValue()))
.when("--fill").asEntry(a -> { /* handle other options here */ })
.withDefaults()
.requireAll();
return options;
}
@NAppInstall
public void onInstallApplication() {
NOut.println(NMsg.ofC("installing My Application: %s%n", NApplication.of().getId()));
}
@NAppUninstall
public void onUninstallApplication() {
NOut.println(NMsg.ofC("uninstalling My Application: %s%n", NApplication.of().getId()));
}
@NAppUpdate
public void onUpdateApplication() {
NOut.println(NMsg.ofC("updating My Application: %s%n", NApplication.of().getId()));
}
}
nuts -y install com.mycompany.app:my-app
nuts -y uninstall com.mycompany.app:my-app
4.1 Why process() is shared, not duplicated
Shell completion is not a separate feature bolted onto argument parsing — it is the same matcher walk, just consulted for candidates instead of run to completion with side effects. Writing two independent parsing bodies for run() and complete() guarantees they drift: someone adds a flag to run(), forgets complete() exists, and tab-completion silently stops reflecting reality.
Keeping one process() method means:
Every
.when(...)block is defined exactly once.run()andcomplete()differ only in what they do after parsing (execute vs. print candidates).- New options are automatically completion-aware.
4.2 Why run stays unprefixed
run() is left without an on*Application prefix deliberately. The on* naming is meant to be distinctive enough that a class implementing NAF's interfaces directly (rather than via annotations) doesn't accidentally collide with unrelated method signatures elsewhere. run is common vocabulary and doesn't need that protection, since it isn't trying to avoid collision the way the lifecycle hooks are.
4.3 Enforcement level
Mode dispatch (§3) routes RUN and COMPLETE to separate branches before either handler is invoked, so run() never executes under completion — there's no isCompleteMode() check to add inside it, since it would always be false there. The real risk is narrower but still real: nothing prevents a developer from leaving @NAppComplete unimplemented, or implementing complete() with logic that doesn't call the same process() as run(), in which case completion candidates silently diverge from (or omit) what run() actually accepts.
If @NAppComplete is not implemented, NAF falls back to a no-op: the shell simply receives no candidates at that point. This is the correct default — the alternative (e.g. having the framework try to automatically reuse run()'s logic on the developer's behalf) risks executing real side effects during a non-interactive shell callback, which is exactly what the COMPLETE session isolation in §5 exists to prevent. A missing completion handler degrades gracefully to "no suggestions"; it does not degrade to "runs the app."
The shared-process() pattern is a convention, not a compiler-enforced contract — consistent with the rest of NAF's fluent, annotation-driven style (e.g. the when*/as*/require/anyMatch matcher vocabulary is agreed-upon shape, not a declarative spec the framework introspects). Document it as the recommended shape for teams adopting NAF; don't rely on the annotation's existence alone to imply safety.
5. Session isolation during completion
COMPLETE mode is invoked non-interactively by the shell — on every keystroke or Tab press, with no human reading the output. This makes side effects that are perfectly fine in RUN mode actively harmful in COMPLETE mode: a confirmation prompt has no one to answer it, and stray terminal output can corrupt the shell's own line redraw.
NAF isolates COMPLETE mode by running it under a modified session copy:
case COMPLETE: {
NSession s = NSession.of();
s.copy()
.bot(true)
.trace(false)
.confirm(NConfirmationMode.NO)
.logTermLevel(Level.OFF)
.runWith(() -> {
nApp.application().onCompleteApplication();
});
return;
}
| Setting | Why |
|---|---|
| Marks the session as non-interactive/automated. |
| Suppresses execution tracing that has no audience. |
| Never blocks on a confirmation prompt during a shell-driven invocation. |
| Forces terminal logging off for this session, regardless of any |
5.1 Why terminal logging specifically matters
Shell dynamic-completion mechanisms (bash complete -C, zsh, fish) capture stdout only and parse it into candidates. stderr passes straight through to the terminal — the same file descriptor the shell is using to redraw the prompt and command line. Logging output on stderr during a completion invocation isn't ignored; it can visibly corrupt the terminal mid-keystroke. logTermLevel(Level.OFF) is therefore in the same category as confirm(NO): it removes a channel a human isn't watching but the terminal itself depends on.
nuts supports both terminal and file logging independently:
nuts --log-level-severe my-app # both term and file
nuts --log-term-severe my-app # terminal only
nuts --log-file-severe my-app # file only
nuts --verbose # equivalent to --log-finest
Nothing is enabled by default. logTermLevel(Level.OFF) in the COMPLETE branch is therefore a defensive floor, not a fix for an active default problem: it guarantees that a user who has enabled --log-term-* for their normal interactive use (e.g. while debugging something) doesn't get that verbosity leaking into every subsequent Tab press. File logging (--log-file-*) is left untouched by this override — it's independent of the terminal handler, so a developer debugging "why did completion return zero candidates" can still enable --log-file-finest and tail the file without any terminal noise.
5.2 A known limitation: the bootstrap window
logTermLevel(Level.OFF) is applied inside the COMPLETE branch of mode dispatch — but nuts bootstrap (workspace initialization, early argument parsing, mode resolution itself) necessarily runs before NAF knows which mode it's in. If a user has --log-term-* set globally, output emitted during this bootstrap window is not covered by the session override, because the override doesn't exist yet at that point in the lifecycle.
In practice this window is narrow — bootstrap is fast, and high term-verbosity during ordinary use is uncommon — but it's worth documenting explicitly rather than assuming logTermLevel(OFF) guarantees silence end-to-end. Closing it properly would mean making mode detection happen early enough in bootstrap that the term log handler can consult it directly, since both are effectively answering the same question ("is this a completion invocation?") at two different layers today.
6. Summary checklist
[ ] Add nuts as a dependency; set
nuts.application=true.[ ] Annotate the class with
@NApp.[ ] Implement
@NAppRun; delegate parsing to a sharedprocess()method.[ ] Implement
@NAppCompletecalling the sameprocess(), thencmdLine.printCompleteResult().[ ] Implement
@NAppInstall/@NAppUpdate/@NAppUninstallonly if needed.[ ] Trust NAF's session isolation (bot,
confirm(NO),logTermLevel(OFF)) for COMPLETE mode — don't reintroduce interactive prompts or terminal logging insideonCompleteApplication().[ ] Remember file logging (
--log-file-*) is unaffected by completion-mode term suppression and remains available for debugging.
13.3 Command Line Arguments
Nuts Application Framework CommandLine
Application Command line can be retrieved via NApp instance:
NCmdLine c1= NApplication.of().cmdLine();
Exec / Autocomplete modes
NCmdLine c= NApplication.of().cmdLine();
if(c.isExecMode()){
///
}
13.4 Nuts Descriptor Integration
Nuts Descriptor Integration
- Seamless integration
- Maven Solver
Nuts and Maven
nuts.executable=<true|false>: when true the artifact is an executable (contains main class)nuts.application=<true|false>: when true the artifact is an executable application (implements NutsApplication)nuts.gui=<true|false>: when true the requires a gui environment to executenuts.term=<true|false>: when true the artifact is a command line executablenuts.icons=<icon-path-string-array>: an array (separated with ',' or new lines) of icon paths (url in the NPath format)nuts.genericName=<genericNameString>: a generic name for the application like 'Text Editor'nuts.categories=<categories-string-array>: an array (separated with ',' or new lines) of categories. the categories should be compatible with Free Desktop Menu specification (https://specifications.freedesktop.org/menu-spec/menu-spec-1.0.html)
nuts.<os>-os-dependencies: list (':',';' or line separated) of short ids of dependencies that shall be appended to classpath only if running on the given os (see NutsOsFamily). This is a ways more simple than using the builtin ' profile' concept of Maven (which is of course supported as well)nuts.<arch>-arch-dependencies: list (':',';' or line separated) of short ids of dependencies that shall be appended to classpath only if running on the given hardware architecture (see NutsArchFamily). This is a ways more simple than using the builtin 'profile' concept of Maven (which is of course supported as well)nuts.<os>-os-<arch>-arch-dependencies: list (':',';' or line separated) of short ids of dependencies that shall be appended to classpath only if running on the given hardware architecture and os family
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>your-group</groupId>
<artifactId>your-project</artifactId>
<version>1.2.3</version>
<packaging>jar</packaging>
<properties>
<!--properties having special meanings in Nuts-->
<maven.compiler.target>1.8</maven.compiler.target>
<!--properties specific to nuts for developers extending nuts-->
<nuts.runtime>true</nuts.runtime> <!--if you implement a whole new runtime-->
<nuts.extension>true</nuts.extension> <!--if you implement an extension-->
<!--other properties specific to nuts-->
<nuts.genericName>A Generic Name</nuts.genericName>
<nuts.executable>true</nuts.executable>
<nuts.application>true</nuts.application>
<nuts.gui>true</nuts.gui>
<nuts.term>true</nuts.term>
<nuts.categories>
/Settings/YourCategory
</nuts.categories>
<nuts.icons>
classpath://net/yourpackage/yourapp/icon.svg
classpath://net/yourpackage/yourapp/icon.png
classpath://net/yourpackage/yourapp/icon.ico
</nuts.icons>
<nuts.windows-os-dependencies>
org.fusesource.jansi:jansi
com.github.vatbub:mslinks
</nuts.windows-os-dependencies>
<nuts.windows-os-x86_32-arch-dependencies>
org.fusesource.jansi:jansi
com.github.vatbub:mslinks
</nuts.windows-os-x86_32-arch-dependencies>
</properties>
<dependencies>
</dependencies>
</project>
Nuts and Java MANIFEST.MF
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: vpc
Created-By: Apache Maven 3.8.1
Build-Jdk: 1.8.0_302
Nuts-Id: groupid:artifactid#version
Nuts-Dependencies: org.fusesource.jansi:jansi#1.2?os=windows;com.github.vatbub:mslinks#1.3?os=windows
Nuts-Name: Your App Name
Nuts-Generic-Name: Your App Generic Name
Nuts-Description: Your App Description
Nuts-Categories: /Settings/YourCategory;/Settings/YourCategory2
Nuts-Icons: classpath://net/yourpackage/yourapp/icon.svg;classpath://net/yourpackage/yourapp/icon.png
Nuts-Property-YourProp: YourValue
Comment: if the Nuts-Id could not be found, best effort will be used from the following
Automatic-Module-Name: yourgroupid.yourartifactid.YourClass
Main-Class: groupid.artifactid.YourClass
Implementation-Version: 1.2.3
Nuts and Java 9 (jdeps)
Nuts supports Automatic-Module-Name.
Automatic-Module-Name: yourgroupid.yourartifactid.YourClass
Nuts and Gradle (TODO)
14 Integration & Interoperability
NAF (Nuts Application Framework) is designed not only to provide powerful core utilities but also to integrate seamlessly with other Java ecosystems and frameworks. This section explores how NAF can interoperate with common tools and frameworks, allowing you to leverage its features without sacrificing compatibility or flexibility.
Key Points
Spring Boot Integration
NAF can be used alongside Spring Boot, providing its services (like NWorkspace, NExec, NLog, and NTextArt) as beans or components in a Spring-managed application. This enables combining Spring’s dependency injection and lifecycle management with NAF’s runtime utilities.
SLF4J Backend for NLog
While NAF provides its own logging API (NLog) with advanced features like structured logging, scopes, and message formatting, it can delegate to widely-used logging frameworks like SLF4J. This allows applications to retain NLog capabilities while integrating with existing logging infrastructure, including Logback or Log4j2.
Flexible Output & Messaging
NAF’s components—such as NTextArt, NProgressMonitor, and NExec—can output to different destinations, including standard streams, files, or logging backends. This makes it easy to integrate visual and progress feedback into GUI applications, web services, or enterprise pipelines.
Cross-Framework Reusability
Because NAF is modular and decoupled, its APIs can coexist with other libraries without requiring significant refactoring. You can use Nuts for dependency resolution, task execution, logging, and progress monitoring in the context of Spring Boot, Micronaut, or even plain Java SE projects.
14.1 NAF Spring Boot Integration
nuts can work flawlessly with spring boot applications. You just need one dependency and one annotation @NApp to mark your SpringBootApplication.
Add the following dependency to you spring boot project
<dependency>
<groupId>net.thevpc.nuts</groupId>
<artifactId>nuts-spring-boot</artifactId>
<version>1.0.0.0</version>
</dependency>
Add @NApp in your SpringBootApplication top class.
@NApp
@SpringBootApplication
@Import(NutsSpringBootConfig.class)
public class AppExample {
public static void main(String[] args) {
SpringApplication.run(AppExample.class, args);
}
@NAppRun // optional runner
public void run() {
NOut.println("Hello ##World##");
}
}
Now you can inject Nuts objects in your beans
@Component
public class MyBean {
@Autowired NSession session;
@Autowired NWorkspace workspace;
@Autowired NTerminal term;
@Autowired NPrintStream out;
}
Using Spring-Managed Beans in Nuts
Nuts can automatically integrate with Spring's application context. By default, NBeanContainer is wired with the Spring context, which allows any object managed by Spring to be referenced in Nuts using NBeanRef.
:: Important: Only the NBeanRef itself is serialized or persisted. At runtime, the actual bean is resolved dynamically from the current bean container (e.g., Spring context). This means you can safely serialize or store Nuts objects without worrying about serializing the full bean state.
Example: Referencing Spring Beans
// Assume jdbcStore is an instance of a persistent store
NRetryCallFactory factory = NRetryCallFactory.of(jdbcStore); // optional persistence
// Register Spring beans by reference using NBeanRef
factory.of("something", NBeanRef.of("callSomeThingBean").as(NCallable.class))
.handler(NBeanRef.of("resultSomeThingBean").as(NRetryHandler.class))
.maxRetries(5)
.retryPeriod(NRetryPeriodFunction.ofMultipliedPeriod(NDuration.ofSeconds(1), 1))
.callAsync();
// Example: Custom handler implementation
public class ResultSomeThingHandler implements NRetryHandler {
@Override
public void handle(NRetryResult result) {
if (result.isSuccess()) {
logger.info("Retry call succeeded: {}", result.getValue());
} else {
logger.error("Retry call failed after {} attempts", result.getAttempts(), result.getException());
}
}
}
Bean Resolution and Error Handling
The NBeanRef.of("beanName").as(ClassType.class) call returns a proxy to the specified interface. No validation occurs at this point—the reference is simply stored. Bean resolution happens at runtime when you invoke methods on the proxy. If the referenced bean doesn't exist in the container when a method is called, a NEmptyOptionalException will be thrown at that moment, not when creating the reference.
// This creates a reference but doesn't fail
NBeanRef.of("nonExistentBean").as(SomeInterface.class);
// This will throw an exception when resolve the bean and invoke the method
NBeanRef.of("nonExistentBean").as(SomeInterface.class).someMethod(); // ← Error thrown here
Notes
NBeanRef.of("beanName").as(ClassType.class)works for any Spring-managed bean, not just retry calls.- Bean references are resolved lazily at method invocation time, allowing for flexible deployment scenarios where beans may be reconfigured or reloaded.
Persistent stores (like jdbcStore) are optional and provide state recovery when needed.
- This approach demonstrates how Nuts and Spring can work together, enabling robust integration for retries, sagas, or other managed workflows while maintaining safe serialization boundaries.
14.2 NAF SLF4J Integration
Nuts SLF4J Integration
Nuts provides seamless integration with SLF4J, the standard logging facade for Java applications. This integration allows you to use Nuts' powerful NLog structured logging system while maintaining compatibility with your existing SLF4J infrastructure.
Installation
Add the following dependency to your Spring Boot project:
<dependency>
<groupId>net.thevpc.nuts</groupId>
<artifactId>nuts-nuts-slf4j</artifactId>
<version>1.0.0.0</version>
</dependency>
Why SLF4J Integration?
SLF4J is the de facto standard logging facade in the Java ecosystem. By integrating Nuts with SLF4J, you get:
Unified logging: Use NLog alongside your existing SLF4J loggers without conflicts
Flexible backend support: Route logs to Logback, Log4j2, or any SLF4J-compatible backend
Structured logging: Leverage Nuts' NLog for rich, semantically meaningful logs with NMsg
Backward compatibility: Existing SLF4J code continues to work without modification
Context propagation: MDC (Mapped Diagnostic Context) values flow seamlessly between Nuts and SLF4J
Basic Usage
Once the dependency is added, NLog automatically delegates to SLF4J:
import net.thevpc.nuts.NLog;
import net.thevpc.nuts.NMsg;
@Component
public class MyService {
private static final NLog log = NLog.of(MyService.class);
public void processData(String data) {
log.info(NMsg.ofC("Processing data: %s", data));
try {
// ... processing logic
log.debug(NMsg.ofC("Data processing completed successfully"));
} catch (Exception ex) {
log.error(NMsg.ofC("Failed to process data: %s", data)
.withThrowable(ex)
);
}
}
}
Structured Logging with NLog and SLF4J
The true power emerges when combining NLog with NMsg for structured, context-aware logging:
@Component
public class OrderProcessor {
private static final NLog log = NLog.of(OrderProcessor.class);
public void processOrder(Order order) {
long startTime = System.currentTimeMillis();
try {
log.info(NMsg.ofC("Processing order #%s from customer %s", order.getId(), order.getCustomerId())
.withIntent(NMsgIntent.START)
);
// ... order processing
long duration = System.currentTimeMillis() - startTime;
log.info(NMsg.ofC("Order #%s completed successfully", order.getId())
.withIntent(NMsgIntent.SUCCESS)
.withDurationMs(duration)
);
} catch (PaymentException ex) {
log.error(NMsg.ofC("Payment failed for order #%s", order.getId())
.withIntent(NMsgIntent.FAIL)
.withThrowable(ex)
);
throw ex;
}
}
}
Using NMsg Formatting Styles
The integration supports all NMsg formatting styles and automatically translates them for SLF4J:
private static final NLog log = NLog.of(MyClass.class);
// C-style formatting (printf-like)
log.info(NMsg.ofC("User %s logged in from %s", username, ipAddress));
// J-style formatting (Java Logging style)
log.warn(NMsg.ofJ("Configuration file not found: {0}", configPath));
// Variable-based formatting with placeholders
log.debug(NMsg.ofV("Cache miss for $key with TTL $ttl seconds",
NMaps.of("key", cacheKey, "ttl", ttlSeconds)
));
Semantic Logging with Intents
Attach semantic meaning to logs using NMsgIntent for better filtering, monitoring, and analysis:
// Operational events
log.info(NMsg.ofC("Service started on port %d", port)
.withIntent(NMsgIntent.START)
);
log.info(NMsg.ofC("Database connection established")
.withIntent(NMsgIntent.SUCCESS)
);
// Data operations
log.debug(NMsg.ofC("Reading user record: %s", userId)
.withIntent(NMsgIntent.READ)
);
log.info(NMsg.ofC("User profile updated for %s", userId)
.withIntent(NMsgIntent.UPDATE)
);
// Resource management
log.info(NMsg.ofC("Cache entry added for key: %s", key)
.withIntent(NMsgIntent.ADD)
);
log.info(NMsg.ofC("Temporary file removed: %s", tempFile)
.withIntent(NMsgIntent.REMOVE)
);
// Failure handling
log.error(NMsg.ofC("Retry attempt %d failed", attemptNumber)
.withIntent(NMsgIntent.FAIL)
.withThrowable(exception)
);
Scoped Logging in Spring Components
Use scoped logging to apply context across multiple Spring beans and method calls:
@Service
public class RequestHandler {
@Autowired
private UserService userService;
@Autowired
private OrderService orderService;
public void handleRequest(RequestContext context) {
// Set up a logging scope for this request
NLogs.of().runWith(
NLogContext.of()
.withMessagePrefix(NMsg.ofC("[Request %s]", context.getRequestId()))
.withPlaceholder("userId", context.getUserId())
.withPlaceholder("sessionId", context.getSessionId()),
() -> {
userService.validateUser(); // Logs inherit request context
orderService.processOrder(); // Logs inherit request context
}
);
}
}
@Service
public class UserService {
private static final NLog log = NLog.of(UserService.class);
public void validateUser() {
// This log automatically includes the request prefix and placeholders
log.info(NMsg.ofV("Validating user $userId in session $sessionId"));
}
}
Integration with Spring Boot Logging Configuration
By default, SLF4J routes Nuts logs through your Spring Boot logging backend (Logback, Log4j2, etc.). Configure your application.yml or logback-spring.xml as usual:
logging:
level:
net.thevpc.nuts: DEBUG
com.myapp: INFO
file:
name: logs/application.log
pattern:
console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
Nuts logs will respect these configurations automatically, allowing you to manage Nuts logging alongside your application's other loggers.
Best Practices
Use NLog for internal diagnostics: NLog is designed for developer-focused, structured diagnostics. Use it for tracing operations, debugging, and understanding application behavior.
Use NOut for user-facing output: Reserve NOut and NTrace for messages intended for end users or CLI output.
Attach semantic verbs: Always use NMsgIntent to classify your logs. This enables powerful filtering, monitoring, and analysis downstream.
Combine with Spring AOP for cross-cutting concerns: Use Spring's AOP and Nuts scoped logging for consistent request/transaction tracing:
@Aspect
@Component
public class LoggingAspect {
@Before("@annotation(com.myapp.Traced)")
public void beforeTracedMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
NLog.of(joinPoint.getTarget().getClass())
.info(NMsg.ofC("Entering method: %s", methodName)
.withIntent(NMsgIntent.START)
);
}
}
Learn More
For comprehensive details on
NLog
and
NMsg
, see the NLog for elegant Logging documentation, which covers advanced features like custom log handlers, MDC integration, and output formatting.
Summary
The Nuts SLF4J integration bridges the gap between Nuts' powerful structured logging capabilities and the Java ecosystem's standard logging facade. By combining NLog, NMsg, and SLF4J, you gain:
- Rich, semantic logging with intents and structured messages
- Full compatibility with existing SLF4J infrastructure
- Scoped logging for contextual diagnostics
- Flexible output routing and configuration
- Seamless integration with Spring Boot applications
15 Workspace & Package Context
NWorkspace and NSession are the foundation of Nuts runtime context. Understanding how they work is essential for correctly using all Nuts components.
What is a NWorkspace?
A NWorkspace represents the application context and component container in Nuts. It is:
Comparable to a Spring ApplicationContext,
A container for all globally or locally scoped Nuts services (like NOut, NLog, NWorkspaceService, etc.),
Required for all operations using Nuts APIs — at any moment, a Nuts component is always used in the context of a workspace.
What is a NSession?
A NSession is a lightweight, thread-scoped execution context associated with a NWorkspace. It:
- Controls options like verbosity, trace mode, output streams, and more,
Is thread-local by default and can be inherited by child threads,
- Provides runtime configuration for rendering, formatting, user input, log levels, etc.
Any Nuts operation (e.g., NOut.println(...)) implicitly uses the current NSession, and therefore accesses the NWorkspace bound to it.
15.1 Extensions
Nuts Extension Mechanism
Nuts provides a dynamic, score-driven Service Provider Interface (SPI) framework. It enables runtime discovery, context-sensitive implementation resolution, and transparent overrides of framework subsystems.
1. System Architecture
+-------------------------------------------------------------------------+
| NWorkspace |
| |
| +-------------------------------------------------------------------+ |
| | NExtensions | |
| | +-------------------------+ +-----------------------------+ | |
| | | Extension Lifecycle | | Component Registry | | |
| | | - loadExtension(NId) | | - registerType(...) | | |
| | | - unloadExtension(NId) | | - registerInstance(...) | | |
| | +-------------------------+ +-----------------------------+ | |
| | +-------------------------+ +-----------------------------+ | |
| | | Scoring Engine | | Instantiation & Resolution | | |
| | | - NScore evaluation | | - createSupported(...) | | |
| | | - NScoredValue metadata | | - createAllSupported(...) | | |
| | +-------------------------+ +-----------------------------+ | |
| +-------------------------------------------------------------------+ |
+-------------------------------------------------------------------------+
Key Capabilities & API Methods
- Extension Point: Any interface inheriting from net.thevpc.nuts.spi.NComponent.
- SPI Registration: All extensions register via a single service file: META-INF/services/net.thevpc.nuts.spi.NComponent.
- Registration Lifecycle: When a core or plugin JAR is loaded, its extensions are indexed in a one-pass scan into an in-memory registry managed by NExtensions.
- Resolution Engine: Evaluates candidate implementations against an NScorableContext and instantiates the candidate with the highest positive score. Ties default to registration order (first discovered wins).
Resolution & Factory Queries
- Single Best Match: NExtensions.of(NEnv.class) or createSupported(type, criteria) resolves the highest-scoring candidate (score > 0).
- Multi-Match / Chain Execution: createAllSupported(type, criteria) returns a list of all candidates with positive scores, sorted in descending order of precedence.
- Bulk Retrieval: createAll(type) returns instances of all registered types implementing that extension point.
Dynamic Lifecycle Management
- Runtime Loading: loadExtension(NId id) pulls new extensions into the workspace classpath and indexes their types.
- Hot Unloading: unloadExtension(NId id) detaches classes and instances associated with that artifact source.
2. Scoring System (NScorable)
Every candidate is evaluated against runtime criteria before instantiation.
| Score Level | Constant | Value | Role |
|---|---|---|---|
| Custom | | 1000+ | User-defined overrides (takes priority) |
| Default | | 10 | Standard built-in implementations |
| Unsupported | | -1 (<= 0) | Candidate is discarded for current context |
3. Component Scopes (NScopeType)
Components define their lifecycle using the @NComponentScope annotation:
| Scope | Behavior | Typical Use Case |
|---|---|---|
| WORKSPACE | One instance per NWorkspace reference. | Heavy tooling, format executors (ZipExecutorComponent), thread pools. |
| SESSION | One instance per NSession. Isolated to a specific session execution. | Session-specific caches, localized configuration. |
| SHARED_SESSION | Shared across a session and all sub-sessions/copies derived from it. | Shared state across child commands or task graphs. |
| TRANSITIVE_SESSION | Copied/cloned when a session is copied. | Session configurations that start with parent state but isolate child mutations. |
| PROTOTYPE (Default) | A fresh instance is created on every resolution call. | Stateful command builders, short-lived task processors. |
4 API, SPI, & RPI Specification
Nuts unifies all runtime components under the NComponent interface while distinguishing three functional tiers: API, SPI, and RPI.
1. The Three Component Tiers
| Tier | Name | Resolution Mechanism | Role | Example |
|---|---|---|---|---|
| API | Application Programming Interface | Static entry points (NEnv.of(), NEnv.get()) | Public contracts consumed by applications and commands. | NEnv, NSession, NWorkspace |
| SPI | Service Provider Interface | Dynamic discovery via manifest & scoring pipeline | Pluggable strategies, protocol extensions, custom engines. | NLogFactorySPI, NPathFactorySPI |
| RPI | Reserved Programming Interface | Fast-path hard-wired resolution in NExtensions | Essential runtime internals soldered by the core engine. Non-overridable. never used by applications | NTextRPI, internal session coordinators |
2. Internal Resolution Engine Mechanics
When NExtensions.of(Type.class) or NExtensions.createSupported(Type.class, criteria) is invoked, NExtensions executes one of two paths:
Path A: The RPI Fast-Path (Hard-Wired & Scoped)
For core interfaces, the implementation is explicitly wired to avoid reflection and dynamic scanning.
Path B: The SPI Dynamic Scoring Pipeline
For pluggable extension points, NExtensions scans registered classes, evaluates support levels, and lazily instantiates the highest-scoring candidate.
3. How APIs Consume RPIs
Public API convenience methods use RPIs as internal engines without exposing internal implementation details:
public interface NObjectWriter extends NCmdLineConfigurable, NComponent {
// API static factory delegates to RPI singleton in the current session
static NOptional<NObjectWriter> get(Object any) {
return NTextRPI.of().createWriter(any);
}
}
5. Implementation Declaration Patterns
Pattern A: Static Score Annotation (Class-Level)
Best for unconditional singletons, adapters, or service providers.
@NScore(fixed = NScorable.CUSTOM_SCORE)
public class Slf4JNLogFactorySPI implements NLogFactorySPI {
public Slf4JNLogFactorySPI() {
// Default constructor resolved automatically
}
@Override
public NLogSPI getLogSPI(String name) {
return new Slf4JNLogSPI(name);
}
}
Pattern B: Dynamic Context Scoring (Method-Level)
Best for transport protocols, OS-specific hooks, or format decoders. Any public static method annotated with @NScore returning int is evaluated.
@NComponentScope(NScopeType.PROTOTYPE)
public class NEnvSshImpl implements NEnv {
public static final String PROTOCOL = "ssh";
// Priority 1: Context constructor
public NEnvSshImpl(NScorableContext context) {
NConnectionString conn=context.criteria();
//...
}
// Dynamic support evaluation
@NScore
public static int checkSshSupport(NScorableContext context) {
NConnectionString conn = context.criteria(NConnectionString.class);
if (conn != null && PROTOCOL.equalsIgnoreCase(conn.protocol())) {
return NScorable.DEFAULT_SCORE;
}
return NScorable.UNSUPPORTED_SCORE;
}
}
4. Constructor & Method Resolution Rules
- Scoring Methods:
Must be
public static int <anyName>(NScorableContext context).If multiple
@NScoremethods exist on a single class, all are evaluated.
- Constructor Precedence:
public MyImpl(NScorableContext context)(preferred if context is needed).public MyImpl() (default no-arg constructor).
5. Runtime Usage
Case 1: Resolving a Singleton / Factory (e.g., Logger)
// Automatic resolution of highest-priority factory (e.g., Slf4J over java.util.logging)
NLogFactorySPI loggerFactory = NExtensions.of(NLogFactorySPI.class);
NLogSPI logger = loggerFactory.getLogSPI("AppLogger");
Case 2: Resolving a Context-Dependent Provider (e.g., Transport Env)
NConnectionString conn = NConnectionString.of("ssh://user@remote-host");
// createSupported injects criteria into NScorableContext
NOptional<NEnv> env = NExtensions.of().createSupported(NEnv.class, conn);
Case 3: Pipeline / Filter Processing (All Supporting)
// Discover and execute all interceptors/validators supporting this input
List<NCommandValidator> validators = NExtensions.of()
.createAllSupported(NCommandValidator.class, currentCommand);
for (NCommandValidator validator : validators) {
validator.validate(currentCommand);
}
Case 4: Programmatic Registration
// Dynamic in-code registration of custom providers
NExtensions.of().registerType(NEnv.class, CustomDockerEnvImpl.class, NId.of("com.myorg:docker-ext#1.0.0").get());
16 Workspace & Package Context
NWorkspace and NSession are the foundation of Nuts runtime context. Understanding how they work is essential for correctly using all Nuts components.
What is a NWorkspace?
A NWorkspace represents the application context and component container in Nuts. It is:
Comparable to a Spring ApplicationContext,
A container for all globally or locally scoped Nuts services (like NOut, NLog, NWorkspaceService, etc.),
Required for all operations using Nuts APIs — at any moment, a Nuts component is always used in the context of a workspace.
What is a NSession?
A NSession is a lightweight, thread-scoped execution context associated with a NWorkspace. It:
- Controls options like verbosity, trace mode, output streams, and more,
Is thread-local by default and can be inherited by child threads,
- Provides runtime configuration for rendering, formatting, user input, log levels, etc.
Any Nuts operation (e.g., NOut.println(...)) implicitly uses the current NSession, and therefore accesses the NWorkspace bound to it.
16.1 NWorkspace
Opening and Sharing Workspaces
Default (global) workspace
NWorkspace.require();
- Returns the currently shared (global) workspace if one exists.
- If no global workspace is present, creates and shares one by delegating to
Nuts.openWorkspace("--reset-options", "--in-memory").share();
This ensures that a workspace is always available, without requiring manual setup.
This workspace is in-memory and ignores any inherited CLI options (--reset-options). It’s ideal for quick use cases, testing, or tools that need a minimal setup.
Scoped (local) workspace
If you need isolation or temporary workspace setup:
Nuts.openWorkspace().runWith(() -> {
// This code runs inside a thread-local scoped workspace
// Nuts components here use the scoped context
});
- Temporarily hides the global workspace inside the block,
- Scoped workspace is available in current and inherited threads,
- Ideal for frameworks, sandboxing, plugins and testing.
- Nuts.openWorkspace() uses persistent location (across processes) unless --in-memory is passed.
Sharing workspace globally
To explicitly promote a workspace to global:
Nuts.openWorkspace().share();
You may also customize it before sharing:
Nuts.openWorkspace("--in-memory", "--color").share();
Accessing the Current Workspace
To retrieve the current NWorkspace (i.e., the one bound to the current thread context), you have two options:
Elegant, fail-fast access
NWorkspace ws = NWorkspace.of();
- Returns the current workspace if one is available,
- Throws an exception if no workspace is present,
- Recommended when a workspace is expected to exist.
- Equivalent to NWorkspace.get().get() but more expressive and fails clearly.
Safe, optional access
Optional<NWorkspace> wsOpt = NWorkspace.get();
Returns an NOptional
16.2 NEnv
Environment & System Info
Environments provide access to environment metadata (bound to the workspace):
NEnv env = NEnv.of();
env.hostName(); // Host name
env.pid(); // Process ID
env.osFamily(); // Linux, Windows, Mac, etc.
env.shellFamily(); // bash, cmd, powershell, etc.
env.platform(); // Java, Android, etc.
env.os(); // Full OS ID
env.osDist(); // OS distribution (e.g. Ubuntu)
env.arch(); // CPU architecture (e.g. amd64)
env.archFamily(); // Arch family (e.g. x86_64)
env.desktopEnvironment(); // Gnome, KDE, etc.
env.desktopEnvironmentFamily(); // Gnome-like, etc.
env.graphicalDesktopEnvironment(); // true if graphical session
You can also list all available shell families or desktop environments:
env.shellFamilies(); // e.g. [BASH, ZSH, CMD]
env.desktopEnvironments(); // List of detected environments
16.3 NSession
NSession defines the current execution context within a Nuts NWorkspace. It encapsulates:
- Command-line options
- User preferences
- Output formatting
- Trace/log verbosity
- Interactive modes
- Runtime state
Every operation within Nuts is executed in the scope of a NSession.
Getting the Current Session
Elegant (fail-fast)
NSession session = NSession.of();
Safe (optional)
Optional<NSession> opt = NSession.get();
Returns an Optional session, or empty if not available.
What Does a Session Do?
- Holds contextual flags like --trace, --yes, --bot, --dry, --confirm
- Controls output formatting: plain, json, xml, tree, table, etc.
- Configures confirmation/interaction modes
- Tracks fetch/cache strategies and expiration
- Controls repository settings
- Defines runtime identity (root(), sudo(), etc.)
Thread-Scoped Context
NSession is thread-local and inherited by spawned threads unless explicitly changed. To run with a different session:
session.runWith(() -> {
// Executes within the session context
});
Or return a result:
String result = session.callWith(() -> computeSomething());
Common Flags and States
--trace
isTrace()Enables trace-mode output--yes
isYes()Assume “yes” for confirmations--no
isNo()Assume “no” for confirmations--ask
isAsk()Always ask for confirmation--bot
isBot()Enable non-interactive/script mode--dry
isDry()Dry-run only, no actual execution
Output Format
Control the rendering format of structured output:
session.json(); // JSON
session.table(); // Tabular
session.tree(); // Tree
session.xml(); // XML
session.props(); // Properties
session.plain(); // Default/plain text
Output formats affect rendering of NOut.println(...), logging, tables, etc.
Streams Access
Each session controls its I/O streams (also accessible via NOut, NErr and NIn):
session.out().println("Standard Output"); // equivalent to NOut.println(...)
session.err().println("Error Output"); // equivalent to NErr.println(...)
session.in().readLine(); // equivalent to NIn.readLine()
This enables custom I/O redirection (e.g., GUI, files, remote shells).
Trace Modes
Trace mode activates auxiliary output useful for end users (not developers):
isPlainTrace(): plain-text traceisIterableTrace(): structured trace with iterable formatisStructuredTrace(): structured trace without iterable mode
if (session.isTrace()) {
NOut.println("Tracing enabled...");
}
// same as
NTrace.println("Tracing enabled...");
Dependency Resolution Options And Fetch Strategy
Used when resolving artifacts and loading jars from repositories (dynamic classloading)
isTransitive()Use transitive repositoriesisCached()Use cached data when possibleisIndexed()Use indexed metadatagetExpireTime()Expire cache before this datesetFetchStrategy()Customize fetch strategy
session.setFetchStrategy(NFetchStrategy.ONLINE);
The fetch strategy determines how and where Nuts searches for artifacts (e.g., dependencies, packages) across local and remote repositories.
This affects resolution against repositories such as Maven Central, Nuts-based repositories, and custom remotes.
🔎 Available Strategies
| Strategy | Description |
|---|---|
ONLINE | Default mode. Searches locally first; if not found, falls back to remotes. |
OFFLINE | Searches only local caches. No remote access is allowed. |
ANYWHERE | Searches both local and remote repositories concurrently. |
REMOTE | Searches only remote repositories, ignoring local cache. |
Confirmation and Interaction
Control how user prompts are handled:
session.yes(); // Force auto-yes
session.no(); // Force auto-no
session.ask(); // Always prompt
session.setConfirm(NConfirmationMode.YES);
Interactive Session Features
Enable/disable progress output:
session.setProgressOptions("auto");
Control GUI/headless:
session.setGui(true);
The gui flag in a session determines whether user interactions should be performed using graphical UI dialogs or standard console input.
When gui is enabled, interactive methods like
NIn.readLine()orNIn.ask()may display graphical dialogs for input instead of using the terminal.When gui is disabled (default in headless or CLI environments), all interactions fall back to console-based prompts.
In GUI-enabled environments, this may pop up a dialog window rather than prompting in the console.
Customize output line prefixes:
session.setOutLinePrefix("[out] ");
session.setErrLinePrefix("[err] ");
Sample Use Case
NSession session = NSession.of().json().setTrace(true);
List<MyObject> data = ...;
NOut.out(session).println(data); // will output JSON trace if enabled
Advanced Configuration
You can clone and configure sessions:
NSession childSession = session.copy()
.setOutputFormat(NContentType.XML)
.setBot(true)
.setTrace(false);
Redirecting Session Streams (Advanced I/O Control)
Nuts allows you to redirect the I/O streams of a session to memory, files, or custom terminals. This is especially useful for scripting, capturing outputs programmatically, or testing. You can run a block of code using a customized session that redirects output to memory. This is useful for capturing the result of structured rendering (e.g., JSON, XML, table) without printing it to the console. Example:
String result = NSession.of().copy()
.setTerminal(NTerminal.ofMem()) // redirect all I/O to memory
.callWith(() -> {
NSession.of().json(); // structured output (e.g., JSON)
NOut.println(Arrays.asList("a", "b", "c"));
return NOut.out().toString(); // retrieve the rendered output as string
});
Explanation:
setTerminal(NTerminal.ofMem()): uses an in-memory terminal for all I/ONSession.of().json(): sets the output format to JSONNOut.println(...): renders the list to the output streamNOut.out().toString(): fetches the printed result from memory
This technique is useful when you want to render structured output for internal use (e.g., passing to another API or storing in a log file), rather than displaying it directly.
Log Configuration
Control logging levels and filters:
session.setLogTermLevel(Level.INFO);
session.setLogFileLevel(Level.FINE);
session.setLogFilter(log -> log.getVerb().equals(NLogIntent.FAIL));
Listeners
Register and listen to session/workspace events:
session.addListener(new NWorkspaceListener() {
...
});
Supported listener types:
- NWorkspaceListener
- NRepositoryListener
- NInstallListener
- NObservableMapListener
Best Practices
- Use NSession.of() only when you're sure a session context exists
- Always configure session flags (--yes, --bot, etc.) when parsing application commandlines