Skip to content

Releases: jeyben/fixedformat4j

1.9.1

Choose a tag to compare

@jeyben jeyben released this 17 Jun 21:35
7e2fa1e

Maintenance release on the 1.9.x line.

Bug fix: custom formatter= on enum fields no longer triggers the enum length check (#161) — fixed in lock-step in both the runtime (FieldValidator) and the compile-time annotation processor (FieldChecker). Forward-ported from the 1.7.4 / 1.8.2 maintenance releases.

See changelog for details.

1.8.2

Choose a tag to compare

@jeyben jeyben released this 17 Jun 20:59
fefe28d

Maintenance release on the 1.8.x line.

Bug fix: custom formatter= on enum fields no longer triggers the built-in enum length validation (#161) — forward-ported from the 1.7.x line.

See changelog for details.

1.7.4

Choose a tag to compare

@jeyben jeyben released this 17 Jun 20:47
bba5df9

Maintenance release on the 1.7.x line.

Corrected republish of 1.7.3 — the 1.7.3 artifact on Maven Central was built from the wrong sources (release-tooling bug, fixed in #164); 1.7.4 ships the intended 1.7.x build carrying the #161 fix: custom formatter= on enum fields no longer triggers the built-in enum length validation.

See changelog for details.

1.9.0

Choose a tag to compare

@jeyben jeyben released this 12 Jun 17:51

A feature release introducing two new optional artifacts (Micrometer instrumentation and a compile-time annotation processor), Java record support, a schema introspection API, and substantial performance work — while staying fully compatible with Java 11 and existing 1.8.x record classes.

New features

  • Micrometer instrumentation — new optional fixedformat4j-micrometer artifact (#120) — decorator-based metrics for any Micrometer registry (Spring Boot Actuator, Quarkus, Micronaut, plain Java). FixedFormatMetrics.of(registry).instrument(manager) publishes fixedformat.load/fixedformat.export timers, a parse-error counter, and a metadata-cache gauge; wrapper factories add reader line counters. The core artifact is unchanged and gains no dependencies; steady-state overhead is tens of nanoseconds per operation, only on instrumented managers (#140). See Metrics.
  • Compile-time annotation validation — new optional fixedformat4j-processor artifact (#118) — validates @Field/@Record configuration during javac, turning misconfigurations into compile errors instead of runtime FixedFormatExceptions. Checks patterns, enum widths, null sentinels, rest-of-line rules, record-length overflow, and overlapping offsets. Strictly opt-in; zero runtime cost. Note: the two layout checks are stricter than the runtime and can surface latent bugs in existing code. See Compile-time validation.
  • Java record support (#119) — @Record classes can now be Java record types (JDK 16+). Annotate the components directly; load() binds through the canonical constructor, export() reads the accessors. All annotations work exactly as on getters, including nested records, repeating fields, and null sentinels. The artifact still runs on Java 11 — record binding activates only when a record class is encountered.
  • Schema introspection API — FixedFormatIntrospector (#117) — introspect(Class<?>) returns one immutable FieldInfo per @Field (ordered by offset): name, offset, length, type, alignment, padding, null sentinels, formatter, repeat count, nested-record flag. Delivered as a separate interface so third-party FixedFormatManager implementations stay compatible. See Schema introspection.
  • @Field.nullValue — literal null sentinel string (#130) — complement to nullChar for feeds where the null marker is a mixed-character string (e.g. "9998"). Strictly opt-in; mutually exclusive with nullChar.
  • Pluggable type registry (#116) — FixedFormatManagerImpl.builder().registerType(UUID.class, UUIDFormatter.class).build() registers a custom type-to-formatter mapping once at startup instead of repeating formatter= on every field.
  • FixedFormatWriter — write-side IO symmetry (#114) — fluent builder API for writing fixed-format files to Writer, OutputStream, or Path, from Iterable or lazy Stream sources, with charset and line-separator control. Heterogeneous record lists supported; instances are thread-safe.
  • FixedFormatReader.openStream() — lazy stream processing (#115) — returns a lazy Stream (untyped or filtered to a record type) so arbitrarily large files can be processed with bounded memory. Caller owns the stream lifecycle via try-with-resources.

Performance improvements

Repeating fields (@Field(count > 1)) are significantly faster: per-class metadata caching makes loading ~1.9× and exporting ~1.25× faster in the bundled JMH benchmark. ByTypeFormatter resolves its delegate once instead of reflectively per call, enum constants are cached, and decimal export skips DecimalFormat entirely (~15% faster small-record export, and output is now guaranteed locale-independent — previously a JVM defaulting to a non-Latin-digit locale could export digits the parser couldn't round-trip).

Bug fixes

  • LEFT-aligned signed numbers no longer lose their leading digitSign.PREPEND/APPEND freed the sign slot from the wrong side for LEFT alignment, corrupting the value (e.g. 5 exported as "+ " and round-tripped as 0). RIGHT-aligned output is byte-for-byte unchanged.
  • NUMERIC enum ordinal 0 survives a round trip with paddingChar='0' — an all-zeros field now loads as the ordinal-0 constant instead of null.
  • Parse failures keep their root cause — date/time formatters now chain the underlying ParseException/DateTimeParseException instead of discarding it.

Dependency changes

  • commons-lang3 removed — replaced with Java 11 natives. fixedformat4j now has a single compile-scope dependency: slf4j-api. Action required only if your project relied on the transitive commons-lang3 — declare it directly in that case.

Build

  • All modules compile with --release 11, validating builds on newer JDKs against the Java 11 API signature.

Full details (including documented known limitations) in the changelog.

Full diff: 1_8_1...1_9_0

1.8.1

Choose a tag to compare

@jeyben jeyben released this 05 May 20:54

Performance improvements

load() and export() are measurably faster, especially for workloads that process many records or hit the same record class repeatedly:

  • Less reflection per call — formatter instances are resolved once at cache-warm time instead of being created fresh on every field operation.
  • Date and time fields are cheaperDateTimeFormatter and SimpleDateFormat instances are cached and reused, eliminating the most expensive allocation in date parsing and formatting.
  • Tighter memory use — internal maps are pre-sized and constant string values are pre-computed at startup rather than rebuilt on every operation.

No API or behaviour change. Existing annotated record classes, custom formatters, and serialized fixed-width data are unaffected. Upgrade by bumping the version number.

Full changelog: https://jeyben.github.io/fixedformat4j/changelog

1.8.0

Choose a tag to compare

@jeyben jeyben released this 01 May 20:17

1.8.0 (2026-05-01)

Breaking changes

  • Removed deprecated protected FixedFormatManagerImpl#readDataAccordingFieldAnnotation (#109) —
    The method was deprecated since #77 (1.7.0) and was never on the live load() path, which has used
    ClassMetadataCache exclusively since that release. Only consumers that extend FixedFormatManagerImpl
    and override or call this method are affected; consumers using the FixedFormatManager interface are
    unaffected.

    Migration: subclassers should drive parsing through FixedFormatManager#load(Class, String) instead.

  • @Record(align) now uses RecordAlign instead of Align (#81) —
    A new two-value enum RecordAlign { LEFT, RIGHT } replaces Align as the type of @Record#align().
    Because Align includes the INHERIT sentinel, which has no meaning at the record level, the old
    type admitted a combination that was only detectable at runtime (and was rejected with a
    FixedFormatException since 1.7.1). RecordAlign makes that mistake impossible at compile time
    and removes the runtime check.

    Migration: replace Align.LEFT / Align.RIGHT with RecordAlign.LEFT / RecordAlign.RIGHT
    on every @Record annotation that specifies the align attribute:

    // Before (1.7.x)
    @Record(length = 20, align = Align.RIGHT)
    public class MyRecord { … }
    
    // After (1.8.0+)
    @Record(length = 20, align = RecordAlign.RIGHT)
    public class MyRecord { … }

    Records that do not specify align are unaffected — the default (RecordAlign.LEFT)
    preserves the existing behaviour. The Align enum itself is unchanged and continues to be
    used for @Field(align = …).

New features

  • FixedFormatReader — file and stream processing (#82,
    #95) —
    Reads fixed-format records from files, streams, or Readers line-by-line, routing each line
    to one or more @Record-annotated classes via LinePattern discriminators. Three factories cover
    the common cases: LinePattern.prefix("HDR"), LinePattern.positional(int[], String) for
    multi-position checks (e.g. type code at offset 0..2 plus a sub-type at offset 7..8), and
    LinePattern.matchAll() for catch-all routing. Patterns are bucketed into hash tables at build
    time, so per-line routing is near O(1) regardless of how many record types are registered.
    FixedFormatReader is unparameterized.

    Two output shapes:

    • read() — returns ReadResult, a type-safe class-keyed container; get(Class<R>) returns List<R> with no cast required. Also provides getAll(), contains(Class<?>), and classes().
    • process(source, HandlerRegistry) — push-style; dispatches each parsed record to the typed Consumer<R> registered in a per-call HandlerRegistry. Classes absent from the registry are silently ignored. Because the registry is supplied at call time, the same reader is safe to use from multiple threads.

    Every shape accepts Reader, InputStream, or Path; stream overloads default to UTF-8.

    Three configurable strategies: MultiMatchStrategy (firstMatch / throwOnAmbiguity /
    allMatches), UnmatchStrategy (skip / throwException), and ParseErrorStrategy
    (throwException / skipAndLog). An excludeLines(Predicate<String>) pre-filter runs
    before pattern matching and bypasses UnmatchStrategy.

    RecordMapping<T> is the public value type carrying the class and pattern for each registered
    mapping; it is surfaced as the parameter and return type of MultiMatchStrategy.resolve().
    Consumers implementing a custom MultiMatchStrategy must reference it directly.

    FixedFormatIOException (extends FixedFormatException) is thrown on underlying IOException.

    import com.ancientprogramming.fixedformat4j.io.read.LinePattern;
    
    FixedFormatReader reader = FixedFormatReader.builder()
        .addMapping(HeaderRecord.class, LinePattern.prefix("HDR"))
        .addMapping(DetailRecord.class, LinePattern.prefix("DTL"))
        .build();
    
    ReadResult result = reader.read(Path.of("data.txt"));
    List<HeaderRecord> headers = result.get(HeaderRecord.class); // no cast
    List<DetailRecord> details = result.get(DetailRecord.class); // no cast

    See File processing for a complete guide.

Bug fixes

  • Classloader leak prevention via ClassValue (#89) —
    The three JVM-level caches (ClassMetadataCache, FixedFormatManagerImpl.VALIDATED_CLASSES, and
    AbstractPatternFormatter.PATTERN_LENGTH_CACHE) were backed by static
    ConcurrentHashMap<Class<?>, …> instances. A ConcurrentHashMap holds strong references to
    its keys, so a Class used as a key can never be garbage-collected — even after all application
    references to it are gone. In multi-classloader environments (OSGi, servlet containers, Spring
    Boot DevTools, Jakarta EE) this causes the child ClassLoader that defined the record class to
    be retained indefinitely, leaking all classes it loaded.

    All three caches are now backed by ClassValue<T>. Computed values are stored inside the
    Class object itself; when the record class's defining ClassLoader becomes unreachable the
    cached metadata is collected with it — no external map, no leak.

    No API or behaviour change. Existing annotated record classes, custom formatters, and
    serialized fixed-width data are unaffected.


1.7.2

Choose a tag to compare

@jeyben jeyben released this 20 Apr 20:55

See changelog for details.

1.7.1

Choose a tag to compare

@jeyben jeyben released this 18 Apr 18:37

New features

  • nullChar on @Field — opt-in sentinel to distinguish a genuinely-absent field from zero/empty. Null-aware handling is active only when nullChar differs from paddingChar. On load, an all-nullChar slice yields null; on export, a null value is emitted as length × nullChar. Works per-element for repeating fields (count > 1). (#29)

  • Record-level default alignment via @Record(align = …) — sets a default alignment for all fields in the record; individual fields may still override with an explicit @Field(align = …). (#30)

Validation improvements

  • Align.INHERIT is now rejected on @Record(align) with a clear FixedFormatException (it is a field-only sentinel).
  • nullChar is now rejected on @Field with a primitive return type (int, long, etc.) — primitives can never be null.

1.7.0

Choose a tag to compare

@jeyben jeyben released this 18 Apr 14:01

Breaking changes

  • AbstractFixedFormatter.getRemovePadding removed — deprecated in 1.6.1 and now deleted.
    Rename any override to stripPadding; the signature is identical. The call chain is now
    parse()stripPadding() directly.

    // Before (1.6.x)
    @Override
    protected String getRemovePadding(String value, FormatInstructions instructions) { … }
    
    // After (1.7.0+)
    @Override
    protected String stripPadding(String value, FormatInstructions instructions) { … }

New features

  • Enum support via @FixedFormatEnum (#67) —
    Annotate any getter that returns an enum type with @FixedFormatEnum to control how the value
    is serialised in the fixed-width record. Two modes are available through the EnumFormat enum:

    • LITERAL (default) — stores and reads the enum constant name (Enum.name() / valueOf()).
    • NUMERIC — stores and reads the ordinal as a zero-padded integer (Enum.ordinal() / index lookup).
    public enum Status { ACTIVE, INACTIVE }
    
    // LITERAL (default): stores "ACTIVE" / "INACTIVE"
    @Field(offset = 1, length = 8)
    @FixedFormatEnum
    public Status getStatus() { … }
    
    // NUMERIC: stores "0" / "1"
    @Field(offset = 1, length = 1)
    @FixedFormatEnum(EnumFormat.NUMERIC)
    public Status getStatus() { … }

Performance improvements

  • Field metadata caching (#77) —
    ClassMetadataCache precomputes and caches all field descriptors per annotated class on first
    use, eliminating repeated annotation scanning on every load() / export() call. The cache is
    process-wide and thread-safe.

  • MethodHandle dispatch (#75) —
    Getter and setter invocation now uses MethodHandle instead of Method.invoke(), reducing
    per-call overhead after JIT warmup.

  • Reduced string allocations (#76) —
    Padding and sign handling rewritten to minimise intermediate String object creation per field.

1.6.1

Choose a tag to compare

@jeyben jeyben released this 10 Apr 08:38

Bug fixes

  • DateFormatter (and LocalDateFormatter / LocalDateTimeFormatter) no longer over-strips padding characters (#33) — When the configured paddingChar happened to be a character that also appears in the formatted date string (e.g. paddingChar = '0' with a time value whose seconds component is 00), the previous stripPadding implementation removed those characters from the parsed string, leaving it too short and causing a ParseException. The fix introduces AbstractPatternFormatter, which overrides stripPadding to remove only leading/trailing padding characters rather than all occurrences of the character.

Deprecations

  • AbstractFixedFormatter.getRemovePadding deprecated — The method has been renamed to stripPadding, which better reflects its behaviour. The old name carried a misleading get prefix that implied a zero-argument accessor.

    getRemovePadding remains callable and fully functional in 1.6.1; it now delegates to stripPadding. It will be removed in 1.7.0.

    Migration: rename any override of getRemovePadding to stripPadding — the signature is identical:

    // Before (1.6.0 and earlier)
    @Override
    protected String getRemovePadding(String value, FormatInstructions instructions) { … }
    
    // After (1.6.1+)
    @Override
    protected String stripPadding(String value, FormatInstructions instructions) { … }