Editors

Register custom PropertyEditor implementations with @Editor for CLI argument parsing and table cell formatting.

The @Editor annotation registers a PropertyEditor for a specific Java type. Editors control how CLI string arguments are converted to objects and how objects are displayed in table output.

AbstractConverter – Minimal Editor

For simple string-to-object conversion, extend AbstractConverter and implement toObjectImpl:

@Editor(LocalDate.class)
public class LocalDateEditor extends AbstractConverter {
    @Override
    protected Object toObjectImpl(final String s) {
        return LocalDate.parse(s);
    }
}

This is sufficient when you only need CLI argument parsing and toString() provides acceptable display output.

PropertyEditorSupport – Full Control

For full control over both parsing and display, extend PropertyEditorSupport and implement setAsText (string-to-object) and getAsText (object-to-string):

@Editor(Environment.class)
public class EnvironmentEditor extends PropertyEditorSupport {
    @Override
    public void setAsText(final String text) throws IllegalArgumentException {
        final Environment env = Arrays.stream(Environment.values())
                .filter(e -> e.getName().equalsIgnoreCase(text))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Invalid environment: " + text));
        setValue(env);
    }

    @Override
    public String getAsText() {
        final Environment env = (Environment) getValue();
        return env != null ? env.getName() : "";
    }
}

Instant Formatting Example

A common use case is formatting java.time.Instant for readable table output:

@Editor(Instant.class)
public class InstantEditor extends PropertyEditorSupport {
    private static final DateTimeFormatter FMT =
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
                    .withZone(ZoneOffset.UTC);

    @Override
    public void setAsText(final String text) {
        setValue(Instant.parse(text));
    }

    @Override
    public String getAsText() {
        final Instant instant = (Instant) getValue();
        return instant != null ? FMT.format(instant) : "";
    }
}

With this editor, 2025-03-08T14:30:00Z on the CLI is parsed as an Instant, and in table output it renders as 2025-03-08 14:30.

Registration

Editors must be registered with Crest so the framework can discover them. There are two approaches:

Via Loader

Include the editor class in your Loader implementation alongside command and interceptor classes:

public class MyLoader implements Loader {
    @Override
    public Iterator<Class<?>> iterator() {
        return Loader.of(
            ConfigCommands.class,
            InstantEditor.class,
            LocalDateEditor.class
        ).iterator();
    }
}

Via Main.builder()

Pass the editor class to load() on the builder:

Main.builder()
        .command(ConfigCommands.class)
        .load(InstantEditor.class)
        .load(LocalDateEditor.class)
        .build();

Crest inspects each class: if annotated with @Editor, it registers the editor automatically.

Dual Role: CLI Parsing and Table Display

An editor serves two purposes:

  • setAsText(String) – called during CLI argument parsing to convert the user’s input string into the target object.
  • getAsText() – called during table rendering to convert the object back into a display string.

Table cells are rendered by first checking for a registered PropertyEditor for the field’s type. If an editor is found, getAsText() is used. Otherwise, the cell falls back to toString().

This means a single @Editor class controls both how a type is parsed from the command line and how it appears in @Table output, keeping the two representations consistent.