Skip to content

Releases: jeyben/fixedformat4j

1.8.1

05 May 20:54

Choose a tag to compare

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

01 May 20:17

Choose a tag to compare

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

20 Apr 20:55

Choose a tag to compare

See changelog for details.

1.7.1

18 Apr 18:37

Choose a tag to compare

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

18 Apr 14:01

Choose a tag to compare

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

10 Apr 08:38

Choose a tag to compare

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) { … }

1.6.0

09 Apr 20:38

Choose a tag to compare

What's New

Repeating Fields

  • Added count and strictCount attributes to @Field for repeating field support — a single annotated getter can now map to multiple consecutive fixed-width slots
  • Clarified @Fields documentation: prefer @Field(count=…) for repeating fields

LocalDateTime Support

  • Added built-in formatter for java.time.LocalDateTime with type-specific default patterns
  • Eager pattern validation catches misconfigured @FixedFormatPattern at load time rather than at parse time

Mutation Testing (CI/CD)

  • Added PIT mutation testing with automated GitHub Pages report publishing
  • Nightly PIT report published to GitHub Pages with links from README and docs navigation

Documentation & Internals

  • Added fixed left-sidebar navigation to docs
  • Refactored FixedFormatManagerImpl into focused collaborators for improved maintainability
  • Added public method Javadoc across all production classes
  • Switched to SLF4J parameterized logging and String.format throughout

Maintenance

  • Issue tracker updated from Google Code to GitHub Issues
  • Removed maven-changes-plugin (changelog now maintained in CHANGELOG.md)

1.5.0

07 Apr 21:10
e0a0bec

Choose a tag to compare

1.5.0

New features

  • Field-level @Field and @Fields annotations@Field and @Fields can now be placed directly on Java fields in addition to getter methods. The manager discovers them at runtime and derives the getter/setter by the get/is naming convention. This enables clean usage with Lombok (@Getter/@Setter) and reduces boilerplate in plain POJOs.

    If both the field and its getter carry @Field, an error is logged (configuration mismatch) and the field annotation is used.

1.4.0

05 Apr 18:24

Choose a tag to compare

1.4.0 (2026-04-05)

New features

  • LocalDate supportjava.time.LocalDate is now a first-class field type handled automatically by ByTypeFormatter. No custom formatter needed. Configure the date pattern with @FixedFormatPattern (default: yyyyMMdd).

    @Field(offset = 1, length = 8)
    @FixedFormatPattern("yyyyMMdd")
    public LocalDate getEventDate() { return eventDate; }

    String "20260405" parses to LocalDate.of(2026, 4, 5); exporting writes "20260405" back.

Breaking changes

  • Java 11 minimum — Java 8 is no longer supported. The minimum required runtime is Java 11.
  • Logging: SLF4J replaces Commons Logging — The library no longer depends on Apache Commons Logging. Logging is now done via SLF4J. If your project relied on the transitive commons-logging dependency, you will need to add an SLF4J binding instead (e.g. logback-classic or slf4j-simple). See Get It for details.

Documentation

  • Added Quick Start guide, Examples page, and an enriched Annotations reference.

Fixedformat4j v1.3.7

24 Jun 07:21

Choose a tag to compare

Fixed bug in how negative decimal numbers between 0 and -1 is parsed big the decimal parser