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

MethodMatchesNotes

when(String... names)

One or more named options or positional keywords.

The common case. Supports multi-patterns and multi-word sequences, e.g., when("install package", "install-package").

whenOption()

any token that looks like an option (-x, --xxx)

no name filtering

whenNonOption()

any token that does not look like an option

typically used to collect positional arguments

whenArg(Predicate<NArg> predicate)

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

whenRaw(Predicate<NCmdLine> predicate)

arbitrary, multi-token lookahead

the full escape hatch — inspect several upcoming tokens before deciding;

you are responsible for checking hasNext()/peek() presence yourself

The as* family — interpreting a match

MethodConsumer receivesValue rule

asFlag(Consumer<NArg>)

matched NArg

boolean toggle; never consumes a following token; honors negation (--!k)

asTrueFlag(Consumer<NArg>)

matched NArg

like asFlag, but the consumer only fires when the flag resolves to true

asEntry(Consumer<NArg>)

matched NArg

value via --k=v, or via a following token — unless that token looks like an option, in which case it's left untouched (never errors if absent)

asAttachedEntry(Consumer<NArg>)

matched NArg

value only via --k=v; never inspects or consumes a following token (never errors if absent)

asRequiredEntry(Consumer<NArg>)

matched NArg

value via --k=v, or unconditionally grabs the following token regardless of its shape (so values that start with -, like JVM options, are handled correctly); throws if no value can be found at all

asArg(Consumer<NArg>)

matched NArg

no value contract — hands back whatever matched, as-is (typically paired with whenNonOption() or whenArg(...))

asRaw(Consumer<NCmdLine>)

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. skipAll())

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

MethodReturnsBehavior

require()

void

tries to match one token against every registered rule; throws if nothing matched

requireAll()

void

equivalent to while (cmdLine.hasNext()) { require(); }throws on the first unmatched token

anyMatch()

boolean

tries every registered rule once against the current token; true if one matched, never throws

noMatch()

boolean

!anyMatch()

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 $name or ${name}

  • Moustache substitution (ofM) – named placeholders using

  • Sql substitution (ofS) – positional and named placeholders using ? or :name

  • Custom 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., $val123text vs ${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., $val123text vs ${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:

InterfaceDescription

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:

StyleInterventionUse 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:

DocCovers

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, $-interpolation, and embedding NExpr's templating in other languages via custom delimiters.

expr-tokenizer

NStreamTokenizer, and how/why it differs from java.io.StreamTokenizer.


1. Core Concepts

ConceptTypeRole
Context

NExprContext

Read-only evaluation environment: holds resolvable vars, functions, constructs, operators. Can parse() expression strings and eval*() calls directly.

Mutable Context

NExprMutableContext extends NExprContext

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 build() (→ NExprContext) or buildMutable() (→ NExprMutableContext).

Node

NExprNode (and subtypes)

The parsed AST. Has a nodeType(), a name(), children(), and can be eval(context).

Var

NExprVar

A named, gettable/settable value (variable or constant) bound into a context.
Function

NExprFunction

A named callable, used for both functions (f(args)) and constructs (new-like or keyword forms).

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:

MethodPurpose

declareBuiltins()

Registers the engine's default operators/functions/constructs (arithmetic, comparison, logical, indexing [ ], grouping ( ), member access ., assignment =, if, etc.) — see [NExpr-Operators.md §built-ins](./NExpr-Operators.md) and [NExpr-Evaluation.md §built-in functions](./NExpr-Evaluation.md).

declareMathConstants()

Adds pi/PI/π (Math.PI) and E (Math.E).

declarePhysicsConstants()

Adds a set of SI physics constants (C, ε0, μ0, η0, h, ħ, kB, NA, me, mp, G, g, R, σ, …) — see expr-evaluation for the full table and a worked example.

declareMathFunctions()

Adds standard java.lang.Math-backed functions (sin, cos, sqrt, pow, atan2, …) — full list in expr-evaluation.

declareVar(NExprVar) / declareVars(NExprVarResolver)

Register a single variable, or a resolver that lazily/dynamically resolves variables by name.

declareFunction(NExprFunction) / declareFunctions(NExprFunctionResolver)

Register a function or a function resolver.

declareConstruct(NExprFunction) / declareConstructs(NExprFunctionResolver)

Register a "construct" — syntactically function-like, semantically distinct (see §5 below).

declareOperator(...) (several overloads)

Register a custom operator — see [NExpr-Operators.md §declaring custom operators](./NExpr-Operators.md).

declareResolver(NExprResolver)

Register a generic, composite resolver — see below.

remove* counterparts

Symmetric removal methods for every declare*.

literalMapper(NExprLiteralMapper) / literalMapper()

Get/set the mapper responsible for turning raw literal tokens into typed values (numbers, strings, booleans, etc).

setAutoDeclareVariables(boolean)

If enabled, referencing an undeclared variable name auto-declares it instead of failing (useful for loosely-typed scripting contexts).

build()

Produces an immutable NExprContext.

buildMutable()

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 via NExprContextBuilder.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 via NExprContextBuilder.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:

FactorySemantics

ofVar(name)

Plain read/write variable, initially null.

ofVar(name, value)

Plain read/write variable with an initial value.

ofVar(name, reader, writer)

Fully custom get/set behavior (e.g. backed by an external object/bean).

ofLazyConst(name, reader)

Computed once, lazily, then treated as constant.

ofConst(name, value)

Fixed constant value.

ofReadOnly(name, reader)

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:

FactorySemantics

ofConst(Function<String,Object>)

Resolver whose values are treated as lazily-computed constants.

ofReadOnly(Function<String,Object>)

Resolver whose values are read-only/live.

ofMap(Map<String,Object>)

Backed by a plain Map — used in TemplateTest for injecting template variables.

ofReadOnlyMap(Map<String,Object>)

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,
}
ValueMeaningExample

WORD

A bare identifier/word token

a (test3); also the node checked in the built-in = and ++/-- handlers to confirm the left-hand side is an assignable variable name

OPERATOR

Any operator application (prefix/postfix/infix, including grouping (, indexing [, block {, and member access .)

a&b, (a&&b), a.b

LITERAL

A literal value token (number, quoted string, boolean, null)

the 1 in a.b>1 (test12) — see NExprLiteralNode / NExprNode.ofLiteral(Object)

FUNCTION

A function or construct call node, e.g. f(a, b)

printChunk(0) (test8)

INTERPOLATED_STR

A $-prefixed or Moustache-style interpolated string

$'something for $v' (test10) — see expr-templating

IF

An if [(cond)] then [else] expression

if (a) 'hello' else 'hella' (test6) — see §7 below

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 via NExprNode.ofWord(String).

  • NExprLiteralNode — a literal value node (numbers, quoted strings, booleans — e.g. the 1 in a.b>1, test12). Constructible directly via NExprNode.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 ("+", ".", "(", "[", ...) and children() 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 if chains 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)

MethodReturnsPurpose

getFunction(name, args...)

NOptional<NExprFunction>

Look up a function by name/args.

getConstruct(name, args...)

NOptional<NExprFunction>

Look up a construct by name/args.

getOperator(name, type, args...)

NOptional<NExprOperator>

Look up an operator by name+kind.

operators()

List<NExprOperator>

All currently registered operators.

getVar(name)

NOptional<NExprVar>

Look up a variable.

getVarValue(name)

NOptional<Object>

Shortcut for current value of a variable.

childContext()

NExprContextBuilder

Start a new builder layered on top of this context.

evalFunction/evalConstruct/evalOperator(...)

NOptional<Object>

Directly invoke by name without pre-parsing an expression.

evalInfixOperator/evalPrefixOperator/evalPostfixOperator(...)

NOptional<Object>

Directly invoke a specific operator kind.

parse(expression)

NOptional<NExprNode>

Parse a full expression string into an AST.

bindLiteral(Object) / bindNode(NExprNode)

NExprNodeValue

Wrap a raw value or an existing node as a call argument.

findCommonInfixOp/PrefixOp/PostfixOp(...)

see expr-operatorsType-based operator-overload resolution.

ofDollarInterpolatedString/ofMoustacheInterpolatedString(String)

NExprInterpolatedStringNode

Build interpolated string nodes programmatically.

ofTemplate()

NExprTemplate

Obtain the templating façade over this context.

literalMapper()

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 that NExprContextBuilder.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 (=, +=, ++, --, …) deliberately control when their operands get evaluated

    , by calling .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

KindMeaningExample

PREFIX

Unary, precedes its operand

-x, !x

POSTFIX

Unary, follows its operand

x++

INFIX

Binary, between two operands

a + b, a.b, a[i]

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):

ConstantValueOperator(s)

PARS / BRACKETS / BRACES / DOT

1600

( grouping, [ indexing, { block, . member access — all share the same, tightest tier

UNARY_POST

1400

postfix ++/--

NOT / UNARY_PRE

1300

prefix - (negation), prefix ! (NOT), prefix ++/--

POW

1250

**

COALESCE

1210

?? (null-coalescing)

MUL / DIV / MOD

1200

*, /

(NExpr's built-in % is registered at CMP, not MOD — see the caveat below)

PLUS / MINUS

1100

+, binary -

SHIFT

1000

<<, >>

CMP (LT/LTE/GT/GTE)

900

<, <=, >, >= — and, as actually registered by declareBuiltins(), also ==, !=/!==/<>, =~, ==~, %

EQ / NEQ

800

reserved tier for equality — declareBuiltins() registers ==/!= at CMP instead

AMP

700

bitwise &

COMPLEMENT

600reserved (likely a future bitwise-complement operator)

PIPE

500

bitwise \|

AND

400

logical && (declareBuiltins()'s single AND operator also aliases plain & here — see caveat)

OR

300

logical \|\|, and ^ (XOR)

TERNARY_CMP

200

reserved for an upcoming ternary ? : operator — not yet wired up

ASSIGN

100

= and all compound assigns (+=, -=, *=, /=, %=, ^=, **=)

STATEMENT_SEPARATOR

50

;

Caveat — the declareBuiltins() excerpt vs. the full engine:

the excerpt of 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:

CategoryAssociativity

Comparisons, &/\|/^ (as shown), arithmetic (+ - * / %, **), . (dot)

LEFT

Assignment = and all compound assigns (+=, -=, …)

RIGHT

Prefix -, prefix !

RIGHT

Prefix ++/--

LEFT (as literally registered — asymmetric vs. prefix -/!)

Postfix ++/--, ; statement separator, (/[/{ grouping markers

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):

NameValue

true

true

false

false

null

null

Operators:

Symbol(s)TypePrecedenceAssoc.Notes

&

INFIX

AND

LEFT

logical AND (see caveat in §4 re: distinct bitwise &)

\|

INFIX

OR

LEFT

logical OR (see caveat re: distinct bitwise \|)

<, <=, >, >=, ==, =~ (LIKE), ==~ (EQ_REGEX)

INFIX

CMP

LEFTcomparisons

!=, !==, <>

INFIX

CMP

LEFT

inequality, all aliasing NE

+, -

INFIX

PLUS

LEFTaddition, subtraction

*, /

INFIX

MUL

LEFTmultiplication, division

%

INFIX

CMP

LEFT

remainder — registered at comparison precedence, not MUL/MOD (see §4 caveat)

^

INFIX

OR

LEFT

XOR

**

INFIX

POW

LEFTexponentiation

.

INFIX

DOT (PARS tier)

LEFT

member access — handler evaluates the left side then delegates to an internal runDot(instance, rightArg, context) resolver

- (unary)

PREFIX

NOT

RIGHTunary negation

!

PREFIX

NOT

RIGHTlogical NOT

=

INFIX

ASSIGN

RIGHT

assignment; requires the LHS node to be NExprNodeType.WORD resolvable via context.getVar(...), else throws IllegalArgumentException

+=, -=, *=, /=, %=, ^=, **=

INFIX

ASSIGN

RIGHT

compound assignment; reads old value, evaluates RHS, delegates to context.evalInfixOperator(baseOp, old, part), then stores and returns the new value

++

PREFIX

NOT

LEFT

pre-increment; adds (byte) 1 via evalInfixOperator("+", …), stores, returns new value

++

POSTFIX

NOT

LEFT

post-increment; same as above but returns the old value

--

PREFIX

NOT

LEFT

pre-decrement (mirrors ++, using evalInfixOperator("-", …))

--

POSTFIX

NOT

LEFTpost-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 "printChunk(0);;printChunk(0);;printChunk(0)" (test9) execute all three calls in sequence, and why empty statements between repeated ;; (test8) don't error out

(

POSTFIX

PARS

LEFT

grouping marker; eval() throws — grouping is resolved structurally by the parser, not by evaluating this node

[

POSTFIX

BRACKETS (PARS tier)

LEFT

indexing marker; eval() throws, same reasoning

{

POSTFIX

BRACES (PARS tier)

LEFT

block marker; eval() throws, same reasoning

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 (name, type), not just name.

    This lets + 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 % precedence gotcha:

    because % shares CMP precedence rather than MUL, an expression like a % b > 0 parses 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:

CallUsed for

.get()

Unwrap, throwing if absent/errored — the default when failure should propagate loudly (e.g. n.eval(expr).get() throughout the test suite).

.orNull()

Unwrap to null on absence/error — for optional/best-effort values (e.g. inside firstNonNull, join).

.ifErrorThrow()

Combined with .orNull() in production code (x.value().ifErrorThrow().orNull()) — propagate any real error as an exception, but still tolerate a legitimately absent/null value.

.isPresent()

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. checking nodeType() == WORD the way the built-in = operator does — since NExprNodeValue inherits nodeType()/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. = checks args.get(0).nodeType() == WORD before evaluating anything, so it can validate the left-hand side is an assignable variable name and resolve it via context.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())

    NameAliasesBehavior

    string(x)

    Converts x to String via NLiteral.of(x).asString()

    boolean(x)

    Converts x to Boolean

    double(x)

    Converts x to Double

    long(x)

    Converts x to Long

    int(x)

    Converts x to Integer

    float(x)

    Converts x to Float

    isNumber(x)

    true if x parses as a number

    isBoolean(x)

    true if x parses as a boolean

    isBlank(x)

    true if x is blank per NBlankable.isBlank

    firstNonNull(a, b, …)

    Returns the first non-null argument

    firstNonBlank(a, b, …)

    Returns the first non-blank argument

    format(pattern, args…)

    formatC

    NMsg.ofC(pattern, args) — C-style message formatting

    formatJ(pattern, args…)

    NMsg.ofJ(pattern, args) — Java-style message formatting

    formatV(pattern, args…)

    NMsg.ofV(...) — supports a trailing Map, Function, NMsgParam, or NMsgParam[] as the substitution source

    join(sep, iterable)

    Joins an Iterable with sep; falls back to String.valueOf if the second arg isn't iterable

    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())

    NameValue

    pi, PI, π

    Math.PI

    E

    Math.E

    Physics constants (declarePhysicsConstants())

    NameValueMeaning

    C

    299792458.0speed of light (m/s)

    ε0

    8.8541878128×10⁻¹²vacuum permittivity (F/m)

    μ0

    1.25663706212×10⁻⁶vacuum permeability (H/m)

    η0

    376.730313668free-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.80665standard gravity (m/s²)

    R

    8.314462618ideal 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

    SyntaxMeaning

    <%: statement %>

    Execute a statement, emit nothing

    <% expression %>

    Evaluate an expression and emit its result

    <%:for varName:<expression %> ... <%:end %>

    Loop over an iterable expression, binding varName each iteration

    <%:for varName,index:<expression}} ... <%:end %>

    Same, also binding a loop index

    <%:if expression %> ... <%:else if expression %> ... <%:else %> ... <%:end %>

    Conditional block, with any number of else if branches and an optional else

    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 plain parse()/eval() or for ofTemplate() — 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 java.io.StreamTokenizer

    How NStreamTokenizer differs

    Numeric tokens are always coerced into a single double nval field — no way to distinguish 1 (int) from 1.0 (double) from a value too large for double to represent precisely.

    nval is a Number, and the tokenizer chooses the narrowest precise Java numeric type for each literal: IntegerLongBigInteger for integral literals (TT_INT/TT_LONG/TT_BIG_INT), and FloatDoubleBigDecimal 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 _read_op() path (isOp(c)) that greedily consumes a run of operator-class characters into a single TT_OP token — so <<, !=, **, ==~, or a custom multi-char operator you've declared, all come back as one token with image/sval set to the full operator string, not as several one-character tokens the parser would have to glue back together. Operator-class characters include the obvious ASCII set (+ - * / > < ! ~ # = & \| ^ % : ?) plus, notably, entire Unicode symbol categories (MATH_SYMBOL, OTHER_SYMBOL, MODIFIER_SYMBOL — covering things like ≤ ≥ ≠ × ÷ ± √ ∑ ∏ ∞ ∂ ∫), so mathematical/scientific notation can be used as operator symbols directly, fitting NExpr's physics/math-oriented built-ins.

    Only two configurable comment styles (slashSlashComments, slashStarComments), always silently discarded — no XML comments, no shell-style # comments, and no way to get comment text back as a token.

    Dedicated handling for four comment families — C ////* */ (slashSlashComments/slashStarComments), XML <!-- --> (xmlComments(boolean)), and shell/Python # (bundled into pythonComments()) — plus convenience presets javaComments() (enables both C styles) and pythonComments() (enables #-style). Critically, returnComments lets comments be returned as real tokens (TT_COMMENTS) carrying their full text via image, instead of always being silently swallowed — useful for tooling that needs to preserve or inspect comments (doc generation, formatters), not just skip them.

    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 ("1 .. 3" tokenizes to ["1", " ", "..", " ", "3"], not just ["1", "..", "3"]).

    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 (", '), and a $-prefixed variant of each is recognized as a distinct interpolated-string token type (TT_ISTR_SQ, TT_ISTR_DQ, TT_ISTR_AQ for $'...', $"...", $... respectively) — this is the lexical foundation for NExpr's $-interpolated string literals, distinguished at the tokenizer level so the parser can build an NExprInterpolatedStringNode directly instead of post-processing a plain string for embedded $var references.

    No concept of a range/ellipsis token — 1..3 and 1.3 are lexically ambiguous under the stock class's number-parsing rules.

    NStreamTokenizer's number reader explicitly looks ahead for a second . immediately following one already consumed as a decimal point, and if found, stops the numeric token before consuming either ., leaving .. to be tokenized separately by _read_op() as its own operator token. This is exactly what test18/test19 in ExprTest verify: "1..3"["1", "..", "3"], and "-1..-3"["-1", "..", "-3"] (with a correctly-tokenized signed number on each side).

    The token-type/flag surface is fixed — you can't register new "kinds" of parsable content without subclassing.

    acceptTokenType(int tt, boolean b) / isParsable(int tt) work against a parsableTokenTypes bitmask keyed by negative token-type constants (defined on the companion NToken class), so new comment/token kinds (like the XML-comment and Python-comment additions above) can be toggled through the same generic mechanism instead of each needing its own dedicated boolean field, as java.io.StreamTokenizer does for its two comment styles.

    Extended/accented Latin characters (128–255) aren't word-constituent by default.

    The private no-arg constructor pre-registers wordChars(128 + 32, 255) in addition to the ASCII letter ranges, so accented/extended-Latin identifier characters are usable out of the box — relevant for NExpr's own physics-constant names (ε0, μ0, η0, ħ, σ, π), which rely on non-ASCII characters being valid word constituents.

    2. Key configuration methods

    MethodEffect

    wordChars(low, hi) / wordChar(c)

    Mark a character range (or single char) as word-constituent.

    whitespaceChars(low, hi)

    Mark a range as whitespace-only (clears any other attribute).

    ordinaryChars(low, hi) / ordinaryChar(ch)

    Strip all special meaning from a range/character — returned as single-char tokens.

    commentChar(ch)

    Mark a character as starting a to-end-of-line comment.

    quoteChar(ch)

    Mark a character as a string delimiter.

    parseNumbers(boolean)

    Toggle numeric-literal recognition for 0-9, ., -.

    eolIsSignificant(boolean)

    Toggle whether line breaks are returned as TT_EOL tokens vs. treated as plain whitespace.

    slashStarComments(boolean) / slashSlashComments(boolean)

    Toggle C-style /* */ / // comment recognition individually.

    xmlComments(boolean)

    Toggle XML <!-- --> comment recognition (and, as implemented, also disables the two slash-comment styles when called).

    javaComments()

    Convenience preset: commentChar('/') + both slash-comment styles enabled.

    pythonComments()

    Convenience preset: #-style line comments enabled, XML/slash styles disabled.

    lowerCaseMode(boolean)

    Force word tokens to lowercase in sval (raw case preserved in image).

    acceptTokenType(int tt, boolean b) / isParsable(int tt)

    Generic enable/disable and query for extensible token kinds (comments, interpolated strings, …), keyed by NToken constants.

    pushBack()

    Un-consume the current token so the next nextToken() call returns it again.

    hasNext()

    Peek whether another character is available without consuming a token.

    3. Fields populated after nextToken()

    FieldMeaning

    ttype

    The token type just read — either one of the NToken.TT_* constants, or (for single-character/operator/quote tokens) the character value itself.

    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 \n, \t, octal escapes already resolved).

    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('-', '-') with parseNumbers(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 (##:red:word##, ##:info:word##, etc.)

    ❌ None❌ None⚠️ Manual via roles✅ Possible via class

    Nested/Combined styles

    ✅ Fully supported (e.g. ##{:red:##{:bold:word}##}##

    ❌ Complex / fragile❌ Not supported✅ Supported✅ Fully supported

    Structured sections (titles, subtitles)

    ✅ NTF supports semantic headers (#) Title, ##) Subtitle)

    ❌ 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.

    text-coloring-format

    text-coloring-format

    text-coloring-format

    text-coloring-format


    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-theme or 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 #RRGGBB hex 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.NTextTheme

    • Default theme implementation: net.thevpc.nuts.runtime.standalone.text.theme.NTextPropertiesTheme

    • Built-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 String

    • forInt(...) Prompt for an int

    • forDouble(...) Prompt for a double

    • forBoolean(...) Prompt for a boolean (yes/no, true/false)

    • forEnum(Class<E> enumType, ...) Prompt for an enum value

    • forPassword(...) Prompt for a password (char[])

    • setDefaultValue(T) Sets a default value used when input is blank

    • setHintMessage(NMsg) Displays hint under the question

    • setAcceptedValues(List<Object>) Restricts accepted values and may display suggestions

    • setRememberMeKey(String) Automatically stores and reuses the answer based on a key

    • setValidator(NAskValidator<T>) Adds input validation logic

    • setParser(NAskParser<T>) Custom parsing from String to T

    • setFormat(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

    ProtocolExampleDescription
    Local File/path/to/resource, C:pathStandard filesystem paths (protocol is implicitly "").
    File URLfile:/path/to/resourceExplicit file URL scheme.
    HTTP/HTTPS

    https://example.com/data.json

    Remote web resources.
    SSHssh://user@server/path/to/resourceSecure remote file access.
    Classpathclasspath:/com/myapp/config.xmlResources bundled in JARs or classpath folders.
    In-Memorymem://sandbox/data.txtVirtual in-memory filesystem. Zero disk I/O, ideal for testing or transient data.
    Nuts Resourceresource://group:artifact#version/pathNuts-specific artifact resolution.
    HTML FS

    htmlfs+https://archive.apache.org/dist/

    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:

    StoreTypePurposeLinux Equivalent
    BINUser-specific executable binaries$HOME/.local/bin
    CONFConfiguration files$XDG_CONFIG_HOME or $HOME/.config
    VARModifiable data files$XDG_DATA_HOME or $HOME/.local/share
    LOGRuntime logs and audit trails$XDG_LOG_HOME or $HOME/.local/log
    TEMPTemporary files$TMPDIR or /tmp
    CACHENon-essential cached data$XDG_CACHE_HOME or $HOME/.cache
    LIBNon-executable libraries$HOME/.local/lib
    RUNRuntime 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 with requestBody(String)[cite: 8], or handle multipart file uploads via addPart()[cite: 8].

    • Integrated JSON Mapping – Automatically bind response payloads directly into Java POJOs using contentAsJson(Class<T>)[cite: 9], or interact with them dynamically via contentAsJsonMap() or contentAsJsonList()[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(), and isServerError()[cite: 5, 9], or use ifErrorThrow() to chain defensive error state processing[cite: 9].

    • Timeout Adaptability – Configure explicit connection and read deadlines via connectTimeout(NDuration) and readTimeout(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() vs claim()

    • 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

    CapabilityStandard Java Logging (JUL)SLF4J / Log4j2

    Nuts NLog

    Object Preservation

    Discarded after formattingFlattened or requires JSON wrappers

    Retained in NMsg parameters

    JSON / Structured Output

    Requires external appendersRequires Jackson/Logstash encoders

    Native (objects preserved in AST)

    Message Styling

    Raw text onlyManual 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 ClassLoaderStatic per ClassLoader

    Contextual per workspace/session via NSession.of()

    Template Reusability

    NoneLimited

    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:

    StateMeaningTypical Origin

    Java Optional Equivalent

    PRESENT

    A value is available (may be null)

    Successful evaluation

    Optional.of(...) / ofNullable(...)

    EMPTY

    The value is logically absentNot found, filtered out, blank input

    Optional.empty()

    ERROR

    A technical or logical failure occurredException 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 navigationthen(...) 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 operationsget(), 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:

    CallState

    isPresent()

    isNull()

    Notes

    NOptional.ofNullable(null)

    PRESENT

    true

    true

    Explicit null is preserved

    NOptional.of(null)

    EMPTY

    false

    false

    Null is treated as absence

    Optional.ofNullable(null)

    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

    MethodPresentEmptyError

    get()

    returns valuethrows NEmptyOptionalExceptionthrows NErrorOptionalException

    get(Supplier<NMsg>)

    returns valuethrows NEmptyOptionalException with custom messagethrows NErrorOptionalException with cutom message

    orNull()

    returns valuereturns nullreturns null

    orElse(T)

    returns valuereturns fallbackreturns fallback

    orElseGet(Supplier)

    returns valuereturns evaluated supplierreturns evaluated supplier

    orDefault()

    returns valuereturns configured defaultreturns configured default

    orDefault(Class)

    returns valuereturns configured or JVM defaultreturns configured or JVM default

    orElseThrow(Supplier)

    returns valuethrows supplied exceptionthrows 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 with withDefault(...).

    5. Transformations (Map/Filter)

    Mapping Family

    MethodBehaviour

    map(Function)

    Classic map; EMPTY/ERROR stay EMPTY/ERROR

    mapIfPresent(Function)

    Maps only when PRESENT

    mapIfNotNull(Function)

    Maps only when PRESENT and value ≠ null

    mapIfNotBlank(Function)

    Maps only when PRESENT and not blank (NBlankable)

    mapIfNotEmpty(Function)

    Alias of mapIfNotBlank

    mapIf(Predicate, Function)

    Conditional map (same type)

    mapIf(Predicate, trueFn, falseFn)

    Full if/else map

    mapIfNotDefault / mapIfDefault

    Respects withDefault(...)

    mapIfNotError(Function)

    Maps only when not ERROR

    flatMap(Function)

    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
    
    ConceptJava Equivalent (Verbose)

    NOptional (Expressive)

    Equivalent TS

    Mandatory Value Check

    (user == null) throw new NEx("missing user")

    NOptional.ofNamed(user, "user").get()

    user!

    Null-Safe Mapping

    (user != null) user.toUpperCase()

    NOptional.of(user).map(String::toUpperCase).orNull()

    user?.toUpperCase()

    Nullish Coalescing

    != null ? user : "default"

    NOptional.of(user).orElse("default")

    ?? "default"

    Error Recovery

    { ... } catch (E) { fallback }

    load().ifErrorUse(() -> defaultOptional).get()

    (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 StyleExample

    Java (Verbose)

    roadNumber = (app != null && app.person != null && app.person.road != null) ? app.person.address.road.number : null;

    NOptional

    roadNumber = NOptional.of(app).then(v -> v.person).then(v -> v.road).then(v -> v.number).orNull();

    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)

    then(...) – concise safe navigation

    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 + orDefault()

    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

    MethodDescription

    map(Function)

    Transform each element

    mapUnsafe(UnsafeFunction)

    Transform that may throw checked exceptions

    mapUnsafe(UnsafeFunction, Function<Exception,R>)

    Transform with error recovery

    filter(Predicate)

    Keep matching elements

    flatMap(...) / flatMapStream / flatMapList / flatMapArray / flatMapIter

    Flatten nested structures

    flatMapToInt / flatMapToLong / flatMapToDouble

    Flatten to primitive streams

    instanceOf(Class<V>)

    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 / sorted(Comparator)

    Sort

    skip(long) / limit(long)

    Windowing

    concat(...) / coalesce(long)

    Append another stream / iterator

    Terminal operations

    MethodDescription
    MethodDescriptiontoList() / toSet() / toSortedSet() / toOrderedSet()Materialize collections
    toArray(IntFunction)Typed array
    toIntArray() / toLongArray() / …Primitive arrays
    toMap / toOrderedMap / toSortedMapBuild maps
    groupBy / groupedByGrouping
    findFirst() / findAny() / findLast()Find elements
    findSingleton()Exactly one element (throws otherwise)
    count()Element count
    min / maxExtremumanyMatch / 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:

  • AnnotationInvoked when…Required

    @NApp

    Marks the class as a NAF application entry pointYes

    @NAppRun

    Normal execution (nuts my-app ...)

    Yes

    @NAppComplete

    Shell requests completion candidates (Tab press)No

    @NAppInstall

    nuts install ...

    No

    @NAppUpdate

    nuts update ...

    No

    @NAppUninstall

    nuts uninstall ...

    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() and complete() 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;
    }
    
    SettingWhy

    bot(true)

    Marks the session as non-interactive/automated.

    trace(false)

    Suppresses execution tracing that has no audience.

    confirm(NConfirmationMode.NO)

    Never blocks on a confirmation prompt during a shell-driven invocation.

    logTermLevel(Level.OFF)

    Forces terminal logging off for this session, regardless of any --log-term-* the user has configured for their interactive sessions.

    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 shared process() method.

    • [ ] Implement @NAppComplete calling the same process(), then cmdLine.printCompleteResult().

    • [ ] Implement @NAppInstall / @NAppUpdate / @NAppUninstall only if needed.

    • [ ] Trust NAF's session isolation (bot, confirm(NO), logTermLevel(OFF)) for COMPLETE mode — don't reintroduce interactive prompts or terminal logging inside onCompleteApplication().

    • [ ] 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 execute

    • nuts.term=<true|false> : when true the artifact is a command line executable

    • nuts.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.<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 LevelConstantValueRole
    Custom

    NScorable.CUSTOM_SCORE

    1000+User-defined overrides (takes priority)
    Default

    NScorable.DEFAULT_SCORE

    10Standard built-in implementations
    Unsupported

    NScorable.UNSUPPORTED_SCORE

    -1 (<= 0)Candidate is discarded for current context

    3. Component Scopes (NScopeType)

    Components define their lifecycle using the @NComponentScope annotation:

    ScopeBehaviorTypical Use Case
    WORKSPACEOne instance per NWorkspace reference.Heavy tooling, format executors (ZipExecutorComponent), thread pools.
    SESSIONOne instance per NSession. Isolated to a specific session execution.Session-specific caches, localized configuration.
    SHARED_SESSIONShared across a session and all sub-sessions/copies derived from it.Shared state across child commands or task graphs.
    TRANSITIVE_SESSIONCopied/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

    TierNameResolution MechanismRoleExample
    APIApplication Programming InterfaceStatic entry points (NEnv.of(), NEnv.get())Public contracts consumed by applications and commands.NEnv, NSession, NWorkspace
    SPIService Provider InterfaceDynamic discovery via manifest & scoring pipelinePluggable strategies, protocol extensions, custom engines.NLogFactorySPI, NPathFactorySPI
    RPIReserved Programming InterfaceFast-path hard-wired resolution in NExtensionsEssential runtime internals soldered by the core engine. Non-overridable. never used by applicationsNTextRPI, 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 @NScore methods 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 trace

    • isIterableTrace(): structured trace with iterable format

    • isStructuredTrace(): 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 repositories

    • isCached() Use cached data when possible

    • isIndexed() Use indexed metadata

    • getExpireTime() Expire cache before this date

    • setFetchStrategy() 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

    StrategyDescription

    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() or NIn.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/O

    • NSession.of().json(): sets the output format to JSON

    • NOut.println(...): renders the list to the output stream

    • NOut.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