More on Java’s DateTimeFormatter for “flexible” parsing

This is a continuation of http://www.clevercaboose.com/2026/07/15/java-datatimeformatter-appendoptional-issue/

No one would recommend a serious parser that just iterated through an exhaustive list of possibilities. So I continued this obsessive, possessed, labor-of-love a little further. This is a hard problem.

Two big issues with DateTimeFormatter:

  1. It is greedy.
  2. There is no way to specify “one of many”

You can work around “greedy”. You cannot work around “one of many”. All you can do is provide “zero or more of many” and hope that greedy catches failures.

Also, this is an unsolvable problem in the sense that the solution will often be ambiguous. You’d never use this for “backend work”, obviously. And in the UI, you are relying on the user to check that the interpreted value is correct. That is unacceptable in some circles.

It might be acceptable to provide 3 results:

  1. Correct value parsed with confidence. (Green)
  2. A value was parsed but it might be wrong. (Orange)
  3. A value could not be parsed. (Error/Red)

So I soldiered on. Creating a sequential list of optional patterns comes very close to working, but not quite. I ran into an obscure error where enough “[-]” patterns in sequence ate up the optional spaces and swallowed the “-” of the time zone offset “-05:30”.

Search google and you’ll see a suggestions to “nest the optionals” rather than just apply them sequentially. But that only works when you have a required constraint at the end of the chain. The problem for us is that our chain is all optional, so nested optionals have the same outcome as flattened optionals.

In the end, I solved the special cases by appending specific patterns to handle these errors. Ugly but necessary.

This is the best I have come up with so far. First a Builder to wrap a Builder:

package com.mri.util.time;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalField;
import java.util.Locale;

/**
 * Criticism of DateTimeFormatterBuilder:
 * 1) No way to declare oneOfMany.
 * 2) Everything is greedy
 */
public class DateTimeFormatterBuilderBuilder {

    final DateTimeFormatterBuilder builder;
    final Locale locale;

    public DateTimeFormatterBuilderBuilder() {
        this(Locale.ENGLISH);
    }
    public DateTimeFormatterBuilderBuilder(Locale locale) {
        this.locale = locale;
        builder = new DateTimeFormatterBuilder().parseCaseInsensitive().parseStrict();
    }

    public DateTimeFormatter toFormatter() {
        return builder.toFormatter(locale);
    }

    // #region separator

    // This is still optional. No way to make this a requirement
    // Make the space the last check. Otherwise will swallow " -", which catches the zone offsets
    static final String separatorPattern = "[:][/][-][,][.][ ]";
    static final DateTimeFormatter separatorOptional = DateTimeFormatter.ofPattern(separatorPattern);

    public DateTimeFormatterBuilderBuilder appendOptionalSeparator() {
        builder.appendOptional(separatorOptional); // could have also done builder.append(separatorOptional);
        return this;
    }

    // #endregion

    // #region optional descending from month to nano

    /**
     * year4 is required. everything following is optional
     */
    public DateTimeFormatterBuilderBuilder year4MonthDayHourMinSecNanoTzOptional() {
        year4();
        appendOptionalSeparator();
        monthDayHourMinSecNanoTzOptional();
        defaultingAll();
        return this;
    }

    /**
     * year2 is required. everything following is optional
     */
    public DateTimeFormatterBuilderBuilder year2SpaceMonthDayHourMinSecNanoTzOptional() {
        year2();
        appendOptionalSeparator();
        monthDayHourMinSecNanoTzOptional();
        defaultingAll();
        return this;
    }

    public DateTimeFormatterBuilderBuilder year4() {
        builder.appendValue(ChronoField.YEAR, 4); // this is exact
        // builder.appendPattern("yyyy"); //this consumes 2 to 19 characters and is more flexible but can cause problems with neighbors
        return this;
    }

    public DateTimeFormatterBuilderBuilder year2() {
        builder.appendValueReduced(ChronoField.YEAR, 2, 2, LocalDate.of(1980, 1, 1));  // valid values from {@code 1980} to {@code 2079}
        // builder.appendPattern("yy");
        return this;
    }

    /**
     * Still have an issue in this method. Turns out that the greedy year has fewer false parsing errors
     * than specific widths. I don't know why and haven't investigated.
     */
    public DateTimeFormatterBuilderBuilder yearAll() {
        if (true) {
            builder.appendPattern("[yyyy][yy]");
        } else {
            // This has a higher number of erroneous parsings
            DateTimeFormatter year2 = new DateTimeFormatterBuilder().appendValue(ChronoField.YEAR, 4).toFormatter();
            DateTimeFormatter year1 = new DateTimeFormatterBuilder().appendValue(ChronoField.YEAR, 2).toFormatter();
            appendOneOf(year2, year1);
        }
        return this;
    }

    public DateTimeFormatterBuilderBuilder month2() {
        builder.appendValue(ChronoField.MONTH_OF_YEAR, 2);
        return this;
    }

    public DateTimeFormatterBuilderBuilder monthAll() {
        appendPattern("[MMMM][MMM]"); // ChronoField not used for text inputs
        DateTimeFormatter month2 = new DateTimeFormatterBuilder().appendValue(ChronoField.MONTH_OF_YEAR, 2).toFormatter();
        DateTimeFormatter month1 = new DateTimeFormatterBuilder().appendValue(ChronoField.MONTH_OF_YEAR, 1).toFormatter();
        return appendOneOf(month2, month1);
    }

    public DateTimeFormatterBuilderBuilder dayAll() {
        DateTimeFormatter day2 = new DateTimeFormatterBuilder().appendValue(ChronoField.DAY_OF_MONTH, 2).toFormatter();
        DateTimeFormatter day1 = new DateTimeFormatterBuilder().appendValue(ChronoField.DAY_OF_MONTH, 1).toFormatter();
        return appendOneOf(day2, day1);
    }

    public DateTimeFormatterBuilderBuilder day2() {
        builder.appendValue(ChronoField.DAY_OF_MONTH, 2);
        return this;
    }

    public DateTimeFormatterBuilderBuilder hour2() {
        builder.appendValue(ChronoField.HOUR_OF_DAY, 2);
        return this;
    }

    public DateTimeFormatterBuilderBuilder minute2() {
        builder.appendValue(ChronoField.MINUTE_OF_HOUR, 2);
        return this;
    }

    public DateTimeFormatterBuilderBuilder second2() {
        builder.appendValue(ChronoField.SECOND_OF_MINUTE, 2);
        return this;
    }

    public DateTimeFormatterBuilderBuilder hourAll() {
        DateTimeFormatter hour2 = new DateTimeFormatterBuilder().appendValue(ChronoField.HOUR_OF_DAY, 2).toFormatter();
        DateTimeFormatter hour1 = new DateTimeFormatterBuilder().appendValue(ChronoField.HOUR_OF_DAY, 1).toFormatter();
        return appendOneOf(hour2, hour1);
    }

    public DateTimeFormatterBuilderBuilder minuteAll() {
        DateTimeFormatter minute2 = new DateTimeFormatterBuilder().appendValue(ChronoField.MINUTE_OF_HOUR, 2).toFormatter();
        DateTimeFormatter minute1 = new DateTimeFormatterBuilder().appendValue(ChronoField.MINUTE_OF_HOUR, 1).toFormatter();
        return appendOneOf(minute2, minute1);
    }

    public DateTimeFormatterBuilderBuilder secondAll() {
        // this fails in the "MM dd yyy hh mm n tz" case ("05 12 1963 01 01 -05:30")
        // problem is that greedy parsing in the optional separator swallows the space here, and the subsequent nano parsing
        // swallows the minus sign of the time zone offset. And then the timezone parser fails because 05:30 is illegal (sign is required)
        optionalStart();
        DateTimeFormatter second2 = new DateTimeFormatterBuilderBuilder().appendValue(ChronoField.SECOND_OF_MINUTE, 2).toFormatter();
        DateTimeFormatter second1 = new DateTimeFormatterBuilderBuilder().appendValue(ChronoField.SECOND_OF_MINUTE, 1).toFormatter();
        appendOneOf(second2, second1);
        optionalEnd();
        return this;
    }

    public DateTimeFormatterBuilderBuilder secondAllX() {
        optionalStart();
        // moving the optional separator inside the required element so the position rolls back if not parsed.
        // This eliminates the parsing errors (Exception because failed to parse) but greatly increases the parsed errors
        // (parsed but incorrect value), because now the timezone offsets are parsed as nanoseconds.
        DateTimeFormatter second2 = new DateTimeFormatterBuilderBuilder().appendOptionalSeparator().appendValue(ChronoField.SECOND_OF_MINUTE, 2).toFormatter();
        DateTimeFormatter second1 = new DateTimeFormatterBuilderBuilder().appendOptionalSeparator().appendValue(ChronoField.SECOND_OF_MINUTE, 1).toFormatter();
        appendOneOf(second2, second1);
        optionalEnd();
        return this;
    }

    /**
     * The month-day-year version, with everything else being optional.
     * If we use MM for the month, then there could be ambiguity with year-month-day.
     * Could solve it by forcing a space after month. If so, consistency suggests spaces between month, day, and year, and hour.
     * Or could just require a space after year. That is still a unique quirk, but is less visible/likely-problem
     */
    public DateTimeFormatterBuilderBuilder monthDayYearHourMinSecNanoTzOptional() {
        monthAll();
        appendOptionalSeparator();
        dayAll();
        appendOptionalSeparator();
        yearAll();
        appendOptionalSeparator(); // to differentiate between year-month-day
        hourMinSecNanoTzOptional();
        defaultingAll();
        return this;
    }

    /**
     * The monthDayYearHourMinSecNanoTzOptional has one failure case:
     * If there are no seconds, then it swallows the separator. The subsequent nano
     * parser swallows the next separator, which includes the "-" of a ZoneOffset (e.g., -05:30).
     * So the ZoneOffset parser fails on 05:30 because there is no sign.
     * No known solution. So just add another backup case that skips the seconds and nanos
     *
     * BY THE WAY. THIS SHOWS ANOTHER ISSUE: IF THE USER SKIPS SECONDS BUT INCLUDES NANOSECONDS, WHO KNOWS WHAT WILL HAPPEN?
     */
    public DateTimeFormatterBuilderBuilder monthDayYearHourMinTzOptional() {
        monthAll();
        appendOptionalSeparator();
        dayAll();
        appendOptionalSeparator();
        yearAll();
        appendOptionalSeparator(); // to differentiate between year-month-day
        hourMinTzOptional();
        defaultingAll();
        return this;
    }
    public DateTimeFormatterBuilderBuilder year4MonthDayHourMinTzOptional() {
        year4();
        appendOptionalSeparator();
        monthDayHourMinTzOptional();
        defaultingAll();
        return this;
    }

    private DateTimeFormatterBuilderBuilder monthDayHourMinSecNanoTzOptional() {
        monthAll();
        appendOptionalSeparator();
        dayHourMinSecNanoTzOptional();
        return this;
    }

    private DateTimeFormatterBuilderBuilder monthDayHourMinTzOptional() {
        monthAll();
        appendOptionalSeparator();
        dayHourMinTzOptional();
        return this;
    }

    private DateTimeFormatterBuilderBuilder dayHourMinSecNanoTzOptional() {
        dayAll();
        appendOptionalSeparator();
        hourMinSecNanoTzOptional();
        return this;
    }

    private DateTimeFormatterBuilderBuilder dayHourMinTzOptional() {
        dayAll();
        appendOptionalSeparator();
        hourMinTzOptional();
        return this;
    }

    private DateTimeFormatterBuilderBuilder hourMinSecNanoTzOptional() {
        hourAll();
        appendOptionalSeparator();
        minuteAll();
        appendOptionalSeparator();
        secondAll();
        appendOptionalSeparator();
        appendPattern("[n]");
        appendAllTimeZones();
        return this;
    }

    private DateTimeFormatterBuilderBuilder hourMinTzOptional() {
        hourAll();
        appendOptionalSeparator();
        minuteAll();
        appendAllTimeZones();
        return this;
    }

    public DateTimeFormatterBuilderBuilder yyyymmddOptional() {
        year4();
        month2();
        day2();
        appendOptionalSeparator();
        hourMinSecNanoTzOptional();
        return this;
    }

    public DateTimeFormatterBuilderBuilder mmddyyyyOptional() {
        month2();
        day2();
        year4();
        appendOptionalSeparator();
        hourMinSecNanoTzOptional();
        return this;
    }

    // #endregion

    // #region timezones

    // Need a separator before the timezone. Cannot include '-' because that swallows the "minus". Just use a space
    // Make it optional because everything in this series is optional
    private static final String allTimeZonesWithSpacePrefixStr = "[ ][VV][xxxx][XXX][z][zzzz]";
    static final DateTimeFormatter allTimeZonesWithSpacePrefixEnglish = DateTimeFormatter.ofPattern(allTimeZonesWithSpacePrefixStr, Locale.ENGLISH);

    // Tried to improve the previous line but could not. This produces worse results. Still investigating...
    // private static final DateTimeFormatter allTimeZonesWithSpacePrefix = allTimeZonesInferior(Locale.ENGLISH);
    // static DateTimeFormatter allTimeZonesInferior(Locale locale) {
    // DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
    // builder.appendPattern("[ ]");
    // builder.optionalStart().appendZoneOrOffsetId().optionalEnd();
    // builder.optionalStart().appendLocalizedOffset(TextStyle.FULL).optionalEnd();
    // builder.optionalStart().appendLocalizedOffset(TextStyle.SHORT).optionalEnd();
    // for (TextStyle textStyle : TextStyle.values()) {
    // builder.optionalStart().appendZoneText(textStyle).optionalEnd();
    // }
    // return builder.toFormatter(locale);
    // }

    /**
     * Include one of all four timezone formats
     *
     * Symbol Meaning Example
     * VV Full Time Zone ID America/Chicago, Asia/Tokyo
     * z Short Zone Name CST, PST, GMT
     * Z Offset without colons +0530, -0800
     * XXX Offset with colons +05:30, -08:00
     *
     * VV (Zone ID): Parses regional IDs like Europe/Paris or America/New_York.
     * x (Zone Offset): Parses formats like +05 or +0530 (outputs +00 for zero).
     * XX (Zone Offset): Parses formats like +0530 or Z (outputs Z for zero).
     * XXX (Zone Offset with Colon): Parses standard formats like +05:30 or Z.
     * z (Short Zone Name): Parses abbreviations like EST, PST, or GMT
     * zzzz (Long Zone Name): Parses full names like Pacific Standard Time
     */
    public DateTimeFormatterBuilderBuilder appendAllTimeZones() {
        // requiredSpace(); //Cannot be a separator because it swallows the "minus"
        // used cache value when possible
        if (locale == Locale.ENGLISH) {
            return append(allTimeZonesWithSpacePrefixEnglish);
        } else {
            return append(DateTimeFormatter.ofPattern(allTimeZonesWithSpacePrefixStr, locale));
        }
    }
    // #endregion

    public DateTimeFormatterBuilderBuilder defaultingAll() {
        builder.parseDefaulting(ChronoField.MONTH_OF_YEAR, 1);
        builder.parseDefaulting(ChronoField.DAY_OF_MONTH, 1);
        builder.parseDefaulting(ChronoField.HOUR_OF_DAY, 0);
        builder.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0);
        builder.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0);
        builder.parseDefaulting(ChronoField.NANO_OF_SECOND, 0);
        return this;
    }

    public DateTimeFormatterBuilderBuilder requiredSpace() {
        builder.appendLiteral(' ');
        return this;
    }

    public DateTimeFormatterBuilderBuilder append(DateTimeFormatter formatter) {
        builder.append(formatter);
        return this;
    }

    private DateTimeFormatterBuilderBuilder optionalStart() {
        builder.optionalStart();
        return this;
    }

    private DateTimeFormatterBuilderBuilder optionalEnd() {
        builder.optionalEnd();
        return this;
    }

    public DateTimeFormatterBuilderBuilder appendOptional(DateTimeFormatter formatter) {
        builder.appendOptional(formatter);
        return this;
    }

    public DateTimeFormatterBuilderBuilder appendPattern(String str) {
        builder.appendPattern(str);
        return this;
    }

    public DateTimeFormatterBuilderBuilder appendOptionalPatterns(String... patterns) {
        optionalStart();
        for (String pattern : patterns) {
            optionalStart();
            appendPattern(pattern);
            optionalEnd();
        }
        optionalEnd();
        return this;
    }

    public DateTimeFormatterBuilderBuilder appendLiteral(String str) {
        builder.appendLiteral(str);
        return this;
    }

    public DateTimeFormatterBuilderBuilder appendValue(TemporalField field, int width) {
        builder.appendValue(field, width);
        return this;
    }

    // warning: there is no way to specify that one of them MUST be required
    // just append them all sequentially as optional
    public DateTimeFormatterBuilderBuilder appendOneOf(DateTimeFormatter... formatters) {
        for (DateTimeFormatter formatter : formatters) {
            appendOptional(formatter);
        }
        return this;
    }
}

And obligatory global utility classes:

public final class MriUtilsLocalDate {
package com.mri.util.time;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;

//package visible
final class MriUtilsLocalDate {

    public static final DateTimeFormatter ISO_DATE_FORMATTER = DateTimeFormatter.ISO_DATE; // ISO-8601
    public static final String ISO_DATE = "yyyy-MM-dd"; // corresponds ISO_DATE_FORMATTER
    static {
        MriUtilsTime.formatterMap.put(ISO_DATE, ISO_DATE_FORMATTER);
    }

    // make sure this comes AFTER the statics
    public static final MriUtilsLocalDate INSTANCE = new MriUtilsLocalDate();

    /**
     * A list of DateTimeFormatters that are used in sequence to attempt to parse a String as a LocalDate
     */
    final List
<DateTimeFormatter> sequencedFormatters;

    private MriUtilsLocalDate() {
        sequencedFormatters = new ArrayList<>();
        sequencedFormatters.add(ISO_DATE_FORMATTER);
        sequencedFormatters.add(new DateTimeFormatterBuilderBuilder().yyyymmddOptional().toFormatter());
        sequencedFormatters.add(new DateTimeFormatterBuilderBuilder().mmddyyyyOptional().toFormatter());
        sequencedFormatters.add(new DateTimeFormatterBuilderBuilder().year4MonthDayHourMinSecNanoTzOptional().toFormatter());
        sequencedFormatters.add(new DateTimeFormatterBuilderBuilder().year2SpaceMonthDayHourMinSecNanoTzOptional().toFormatter());
        sequencedFormatters.add(new DateTimeFormatterBuilderBuilder().monthDayYearHourMinSecNanoTzOptional().toFormatter());

        //all these other cases to catch perverse issues with minus signs in ZoneOffset and the greedy "-" separator
        sequencedFormatters.add(new DateTimeFormatterBuilderBuilder().monthDayYearHourMinTzOptional().toFormatter());
        sequencedFormatters.add(new DateTimeFormatterBuilderBuilder().year4MonthDayHourMinTzOptional().toFormatter());
    }

    /**
     * Expect this to be typed in by a user who will inspect the results.
     * So do our best to find a result, but there is a possibility it will be wrong
     */
    LocalDate parseFlexibleLocalDate(CharSequence text) {
        return MriUtilsTimePriv.parseFormatterSequence(sequencedFormatters, text, LocalDate::from);
    }
}

package com.mri.util.time;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;

//package visible
final class MriUtilsLocalDateTime {

    public static final DateTimeFormatter ISO_LOCAL_DATE_TIME_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
    public static final String ISO_LOCAL_DATE_TIME = "yyyy-MM-dd'T'HH:mm:ss";
    static {
        MriUtilsTime.formatterMap.put(ISO_LOCAL_DATE_TIME, ISO_LOCAL_DATE_TIME_FORMATTER);
    }

    // this no longer works because the yyyy is greedy. Have to use ChronoField4 instead
    // public static final String YYYYMMDDHH = MriUtilsLocalDate.YYYYMMDD+"HH";

    // make sure this comes AFTER the statics
    public static final MriUtilsLocalDateTime INSTANCE = new MriUtilsLocalDateTime();

    /**
     * A large list of DateTimeFormatters that are used in sequence to attempt to parse a String as a LocalDate
     */
    final List
<DateTimeFormatter> sequencedFormatters;

    private MriUtilsLocalDateTime() {
        sequencedFormatters = new ArrayList<>();
        sequencedFormatters.add(ISO_LOCAL_DATE_TIME_FORMATTER);
        sequencedFormatters.addAll(MriUtilsLocalDate.INSTANCE.sequencedFormatters);
    }

    /**
     * Expect this to be typed in by a user who will inspect the results.
     * So do our best to find a result, but there is a possibility it will be wrong
     */
    LocalDateTime parseFlexibleLocalDateTime(CharSequence text) {
        return MriUtilsTimePriv.parseFormatterSequence(sequencedFormatters, text, LocalDateTime::from);
    }
}

package com.mri.util.time;

import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.List;
import java.util.TimeZone;
import org.jspecify.annotations.Nullable;

//package visible
final class MriUtilsInstant {

    // ISO-8601 '2011-12-03T10:15:30Z'
    public static final DateTimeFormatter ISO_INSTANT = DateTimeFormatter.ISO_INSTANT;

    // Standard iso format for zoneddatetime/instant parsing. user includes timezone
    public static final String MM_DD_YYYY_HH_MM_SS_NANO_ZONE = "MM/dd/yyyy HH:mm:ss.n'['VV']'";

    public static final DateTimeFormatter MM_DD_YYYY_HH_MM_SS_NANO_ZONE_FORMATTER = new DateTimeFormatterBuilder()
        .appendPattern("MM/dd/yyyy HH:mm:ss")
        .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
        .appendLiteral(' ')
        .appendZoneId()
        .toFormatter();

    // make sure this goes after the statics
    public static final MriUtilsInstant INSTANCE = new MriUtilsInstant();

    /**
     * A large list of DateTimeFormatters that are used in sequence to attempt to parse a String as a LocalDate
     */
    private final List
<DateTimeFormatter> sequencedFormatters;

    private MriUtilsInstant() {
        sequencedFormatters = new ArrayList<>();
        sequencedFormatters.add(ISO_INSTANT);
        sequencedFormatters.add(MM_DD_YYYY_HH_MM_SS_NANO_ZONE_FORMATTER);
        sequencedFormatters.addAll(MriUtilsLocalDateTime.INSTANCE.sequencedFormatters);
    }

    /**
     * Expect this to be typed in by a user who will inspect the results.
     * So do our best to find a result, but there is a possibility it will be wrong.
     * Not all formats require a TimeZone, as sometimes the TZ will be in the String.
     */
    public Instant parseFlexibleInstant(CharSequence text, @Nullable TimeZone defaultTimeZone) {
        TemporalAccessor temporal = MriUtilsTimePriv.parseFormatterSequenceBest(sequencedFormatters, text);
        return MriUtilsTime.instantFromTemporal(temporal, defaultTimeZone);
    }
}

    public static @Nullable Instant instantFromTemporal(@Nullable TemporalAccessor temporal, @Nullable TimeZone defaultTimeZone) {
        return switch (temporal) {
            case Instant inst -> inst;
            case ZonedDateTime zoned -> zoned.toInstant();
            case LocalDateTime local -> localDateTimeToInstant(local, defaultTimeZone);
            case LocalDate localDate -> localDateTimeToInstant(localDate.atStartOfDay(), defaultTimeZone);
            case LocalTime localTime -> localDateTimeToInstant(localTime.atDate(LocalDate.now()), defaultTimeZone);
            case null -> null;
            default -> throw new IllegalArgumentException("Unexpected class= " + temporal.getClass() + " value: " + temporal);
        };
    }

    public static @Nullable Instant localDateTimeToInstant(@Nullable LocalDateTime localDateTime, @Nullable TimeZone tz) {
        if (localDateTime == null || tz == null) {
            return null;
        }
        return localDateTime.atZone(tz.toZoneId()).toInstant();
    }
}

class MriUtilsTimePriv {

    private static Logger LOG = org.apache.logging.log4j.LogManager.getLogger(MriUtilsTimePriv.class);

    public static TemporalAccessor parseFormatterSequenceBest(List
<DateTimeFormatter> formatters, CharSequence text, TemporalQuery<?>... queries) {
        for (DateTimeFormatter formatter : formatters) {
            try {
                return formatter.parseBest(text, queries);
            } catch (DateTimeParseException ignore) {
                // noop
                LOG.trace("ignore: text={} formatter={}", text, formatter, ignore);
            } catch (Exception e) {
                LOG.warn("Ignoring exception text={} formatter={} ex={}", text, formatter, e);
            }
        }
        throw MriException.from("Could not parse temporal = " + text);
    }

    public static TemporalAccessor parseFormatterSequenceBest(List
<DateTimeFormatter> formatters, CharSequence text) {
        return parseFormatterSequenceBest(formatters, text, Instant::from, LocalDateTime::from, LocalDate::from, LocalTime::from);
    }

    public static 
<T> T parseFormatterSequence(List<DateTimeFormatter> formatters, CharSequence text, TemporalQuery<T> query) {
        for (DateTimeFormatter formatter : formatters) {
            try {
                return formatter.parse(text, query);
            } catch (DateTimeParseException ignore) {
                // noop
                LOG.trace("ignore: text={} formatter={}", text, formatter, ignore);
            } catch (Exception e) {
                LOG.warn("Ignoring exception text={} formatter={}", text, formatter);
            }
        }
        throw MriException.from("Could not parse temporal = " + text);
    }

}

I’m not going to show the JUnit test classes because of the amount of code. (And this doesn’t dseserve its own github repository.) However, I am pleased with the results so far. Currently with a few thousand tests (can easily add many thousands more), no parse failures, and the only errors are caused by greedy/ambiguous parsing, which I don’t think I can fix.

2026_07_19_13_47_42_
2026_07_19_13_51_07_

Java DataTimeFormatter appendOptional issue

I want a date/time/instant parser that “just works” for anything the user is typing in. An AI-solution that analyzed a sentence and did its best would be great, but I am not ready for that amount of work. I could write my own custom antrl parser. But really, the DataTimeFormatter should do the job.

One solution is to create a list of DataTimeFormatters and just try them one at a time. This has some inefficiency.

But DataTimeFormatter has something just for that: The .appendOptional method.

The quickest way for an expert user to type a date is without punctuation. For example, “5 12 26” (using month day year format), Or even shorter: “20260512”. I use the latter a lot because it sorts nicely.

But we also want to handle punctuation between values. A space, a comma, a dash, or a slash. It would be great if DataTimeFormatter had an appendLiteral(“[ ,-/]”) (borrowing regex notation), but it does not. It can support that, but you have to work with .optionalStart and .optionalEnd, which is a little more complicated. Still doable, though.

I have some basic cartesianProduct code, so I wrote it longhand. This is what I came up with:

/**
 * A large list of DateTimeFormatters that are used in sequence to attempt to parse a String
 */
private static final List
<DateTimeFormatter> sequencedLocalDateFormatters;
public static final DateTimeFormatter TRY_ALL_LOCAL_DATE_FORMATTER;
static {
    sequencedLocalDateFormatters = new ArrayList<>();
    sequencedLocalDateFormatters.add(ISO_DATE_FORMATTER);
    sequencedLocalDateFormatters.add(formatter(YYYYMMDD));
    List
<Object> years = List.of("yyyy", "yy");
    List
<Object> months = List.of("MMMM", "MMM", "MM");
    List
<Object> days = List.of("dd");
    List
<Object> separators = List.of(" ", "-", "/", ",");
    List<List<Object>> sets = List.of(years, months, days, separators);
    List<List<Object>> cartesian = cartesianProduct(sets);
    for (List
<Object> list : cartesian) {
        String year = (String) list.get(0);
        String month = (String) list.get(1);
        String day = (String) list.get(2);
        String sep = (String) list.get(3);
        sequencedLocalDateFormatters.add(DateTimeFormatter.ofPattern(year + sep + month + sep + day));
        sequencedLocalDateFormatters.add(DateTimeFormatter.ofPattern(month + sep + day + sep + year));
    }
    DateTimeFormatterBuilder tryAllFormatter = new DateTimeFormatterBuilder();
    for (DateTimeFormatter dateTimeFormatter : sequencedLocalDateFormatters) {
        tryAllFormatter = tryAllFormatter.appendOptional(dateTimeFormatter);
    }
    TRY_ALL_LOCAL_DATE_FORMATTER = tryAllFormatter.toFormatter();
}
public static LocalDate parseLocalDate(CharSequence text) {
    return LocalDate.parse(text, TRY_ALL_LOCAL_DATE_FORMATTER);
}

/**
 * Cartesian product
 * https://www.baeldung.com/java-cartesian-product-sets
 */
static 
<T> List<List<Object>> cartesianProduct(List<List<Object>> sets) {
    return cartesianProduct(sets,0).collect(Collectors.toList());
 }
static Stream<List<Object>> cartesianProduct(List<List<Object>> sets, int index) {
     if (index == sets.size()) {
         List
<Object> emptyList = new ArrayList<>();
         return Stream.of(emptyList);
     }
     List
<Object> currentSet = sets.get(index);
     return currentSet.stream().flatMap(element -> cartesianProduct(sets, index+1)
       .map(list -> {
           List
<Object> newList = new ArrayList<>(list);
           newList.add(0, element);
           return newList;
       }));
}

//JUnit tests
private static Stream
<Arguments> localDateData() {
    return Stream.of(
        Arguments.of("05 12 1963", LocalDate.of(1963, 5, 12)),
        Arguments.of("05-12-1963", LocalDate.of(1963, 5, 12)),
        Arguments.of("05/12/1963", LocalDate.of(1963, 5, 12)),

        Arguments.of("May 12 1963", LocalDate.of(1963, 5, 12)),
        Arguments.of("August 12 1963", LocalDate.of(1963, 8, 12)),

        Arguments.of("05 12 63", LocalDate.of(2063, 5, 12)),
        Arguments.of("05-12-63", LocalDate.of(2063, 5, 12)),
        Arguments.of("05/12/63", LocalDate.of(2063, 5, 12)),

        Arguments.of("May 12 63", LocalDate.of(2063, 5, 12)),
        Arguments.of("August 12 63", LocalDate.of(2063, 8, 12)),

        Arguments.of("19630512", LocalDate.of(1963, 5, 12)),
        Arguments.of("1963-05-12", LocalDate.of(1963, 5, 12)),
        Arguments.of("1963/05/12", LocalDate.of(1963, 5, 12)));
}

@ParameterizedTest
@MethodSource("localDateData")
void test1(String str, LocalDate expectedDate) {
    LocalDate date = LocalDate.parse.(str, TRY_ALL_LOCAL_DATE_FORMATTER);
    assertThat(date).isEqualTo(expectedDate);
}

But when I ran the unit tests, there were errors:

JUnit results

It should have worked. Here is the gemini suggestion for testing the case that errored.

@Test
// gemini suggestion
void geminiSuggestion() {
    String dateString = "05 12 63";

    // Use MM for month, dd for day, and yy for 2-digit year
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM dd yy");
    LocalDate date = LocalDate.parse(dateString, formatter);

    System.out.println(date); // Outputs: 2063-05-12
}

And I am certain that “MM dd yy” is in the list.

Seems like a java bug to me. If I give the DateTimeFormatter a list of DateTimeFormatters to try, it should recover from an individual error and try them all.

So I switched to the “long-hand” version:

public static LocalDate parseLocalDate(CharSequence text) {
    //return LocalDate.parse(text, TRY_ALL_LOCAL_DATE_FORMATTER);
    for (DateTimeFormatter dateTimeFormatter : sequencedLocalDateFormatters) {
        try {
            return LocalDate.parse(text, dateTimeFormatter);
        } catch (Exception ignore) {}
    }
    throw new RuntimeException.from("Could not parse LocalDate = ", text);
}

And this time all the JUnit tests worked!

Bit of a shock. C’est la IT guerre.

Continued at http://www.clevercaboose.com/2026/07/19/more-on-javas-da…flexible-parsing/

On parallel class hierarchies and generics-hell in java

This article discusses the limitations and hassles of java generics when dealing with “parallel class hierarchies”. A parallel class hierarchy is the situation where you have a “main” class hierarchy and a “secondary” class hierarchy that mirrors the “main” hierarchy. The secondary class models some (isolatable/groupable) aspect of the main class that can and should be separated into another class. This frequently shows up in classical object or relational data models.

To get concrete, let’s start with two main classes: Equipment and Structure. Equipment is generally mobile, whereas Structure is not. Both are subclasses of a common abstract class PhysicalItem. The class diagram and java classes are shown below. Fields are omitted.

mermaid-diagram-2025-07-06-155622
public abstract static class PhysicalItem {}
public static class Equipment extends PhysicalItem {}
public static class Structure extends PhysicalItem {}

Now let’s add a “Status” attribute to this hierarchy. A Status is a collection of properties that changes tofether (and frequently), thus we wish to put them into a separate class, rather than the main class.

mermaid-diagram-2025-07-06-160151
public abstract static class PhysicalItem {}
public static class Equipment extends PhysicalItem {}
public static class Structure extends PhysicalItem {}

public abstract static class PhysicalItemStatus {}
public static class EquipmentStatus extends PhysicalItemStatus {}
public static class StructureStatus extends PhysicalItemStatus {}

The data modellers draw an arrow (directional or bidirectional) between the PhysicalItem and PhysicalItemStatus, note the need for the subclasses to handle the appropriate class casting, and consider themselves done.

The programmers have multiple ways to implement that arrow. They can add a field named “status” to the PhysicalItem of type PhysicalItemStatus. And/or they can add a field “item” to the PhysicalItemStatus of type PhysicalItem. Those fields can be in the superclass or in the subclasses. Another option is to create a 3rd class to hold the two fields “status” and “item”, but that is hardly ever done.

For the point of this article, we’re going to choose the first option: Add a field named “status” to the PhysicalItem of type PhysicalItemStatus. So we add getter methods to the PhysicalItem class hierarchy:

public abstract static class PhysicalItem {

    protected PhysicalItemStatus status;

    public PhysicalItemStatus getStatus() {
        return status;
    }
}

public static class Equipment extends PhysicalItem {
    @Override public EquipmentStatus getStatus() {
        return (EquipmentStatus) status;
    }
}

public static class Structure extends PhysicalItem {
    @Override public StructureStatus getStatus() {
        return (StructureStatus) status;
    }
}

Everything is perfect. Notice how our IDE (integration development environment) is aware of the method overrides and automatically knows the correct type.

public static void showCastingIsAutomatic(PhysicalItem item, Equipment equipment, Structure structure) {
    PhysicalItemStatus itemStatus = item.getStatus();
    EquipmentStatus equipmentStatus = equipment.getStatus();
    StructureStatus structureStatus = structure.getStatus();
}

Unfortunately, that was the last of the good news. Now we have to deal with the setters:

It is convenient to add a setter method to the PhysicalItem. But now the IDE (and java) allows runtime errors:

public abstract static class PhysicalItem {
 protected PhysicalItemStatus status;

    public void setStatus(PhysicalItemStatus status) {
        this.status = status;
    }
}

public static void problemsWithSetter(PhysicalItem item, Equipment equipment, Structure structure,
         PhysicalItemStatus itemStatus, EquipmentStatus equipmentStatus, StructureStatus structureStatus) {
    item.setStatus(itemStatus); //fine
    equipment.setStatus(equipmentStatus); //fine
    structure.setStatus(structureStatus); //fine

    equipment.setStatus(structureStatus); //WRONG TYPE. RUNTIME ERROR
    structure.setStatus(equipmentStatus); //WRONG TYPE. RUNTIME ERROR
}

We can add setter methods to the subclass but they cannot OVERRIDE the parent setter method. So client code sees BOTH methods. It is STILL possible to cause a runtime error.

    public abstract static class PhysicalItem {
        protected PhysicalItemStatus status;

        public void setStatus(PhysicalItemStatus status) {
            this.status = status;
        }
    }

    public static class Equipment extends PhysicalItem {
        public void setStatus(EquipmentStatus status) {
            this.status = status;
        }
    }

    public static class Structure extends PhysicalItem {
        public void setStatus(StructureStatus status) {
            this.status = status;
        }
    }

The image below is the eclipse IDE showing the suggestions to complete the setter method. the problem is that we see TWO methods. One is right and the other leads to runtime errors.

2025_07_06_16_45_28_eclipse_workspace_8_standards_src_main_java_generics_example_DualInheritanceTr

Before java gained “generics” we had to live with this. The code had to be written defensively to throw errors when the wrong method was called. And the multiple method problem was so annoying we usually just lived with the single method in the superclass.

When java generics arrived, we could convert our Status class into a generic. Then we could declare our getter and setter in the superclass using the generic variable. Our subclasses now require the correct argument types and can show errors at COMPILE TIME.

public abstract static class PhysicalItem<STATUS extends PhysicalItemStatus> {

    protected STATUS status;

    public STATUS getStatus() {
        return status;
    }
    public void setStatus(STATUS status) {
        this.status = status;
    }
}

public static class Equipment extends PhysicalItem
<EquipmentStatus> {}

public static class Structure extends PhysicalItem
<StructureStatus> {}

public static void getterCastingIsAutomatic(PhysicalItem item, Equipment equipment, Structure structure) {
    PhysicalItemStatus itemStatus = item.getStatus();
    EquipmentStatus equipmentStatus = equipment.getStatus();
    StructureStatus structureStatus = structure.getStatus();
}

public static void noProblemsWithSubclassSetters(Equipment equipment, Structure structure,
       PhysicalItemStatus itemStatus, EquipmentStatus equipmentStatus, StructureStatus structureStatus) {
    equipment.setStatus(equipmentStatus); //fine
    structure.setStatus(structureStatus); //fine

    equipment.setStatus(structureStatus); //WRONG TYPE. COMPILER ERROR
    equipment.setStatus(itemStatus); //WRONG TYPE. COMPILER ERROR
    structure.setStatus(equipmentStatus); //WRONG TYPE. COMPILER ERROR
    structure.setStatus(itemStatus); //WRONG TYPE. COMPILER ERROR
}

public static void potentialErrorsWithSuperclassSetters(PhysicalItem item, PhysicalItemStatus itemStatus, EquipmentStatus equipmentStatus, StructureStatus structureStatus) {
    item.setStatus(itemStatus); //LEGAL BUT POTENTIAL RUNTIME ERROR
    item.setStatus(equipmentStatus); //LEGAL BUT POTENTIAL RUNTIME ERROR
    item.setStatus(structureStatus); //LEGAL BUT POTENTIAL RUNTIME ERROR
}

For those who complain about CSS in HTML pages, the oldtime joke: “CSS is the worst thing ever! With the exception of not having CSS.” Well, the same thing applies to generics. “Generics is the worst thing ever, except for not having generics.”

(Well-read readers will have immediately noted that I am stealing the phrase from Winston Churchill: “Many forms of Government have been tried, and will be tried in this world of sin and woe. No one pretends that democracy is perfect or all-wise. Indeed it has been said that democracy is the worst form of Government except all those other forms that have been tried from time to time”.)

There isn’t a solution to using generics in this manner. They do not provide a 100% solution.

Perhaps even worse than not solving the problem, generics spread to all client code. Now ALL clients of the hierarchy must deal with generics, even if that particular code has nothing to do with status. Either you write your code to ignore generics (and ignore the compiler warnings), or you add <?> everywhere pointlessly. It is a REAL burden.

And it is a burden that multiplies. Because big data models have many aspects that we’d like to extract into secondary classes. So there is a code tug towards many generics, and we wind up referring to PhysicalItem<? extends PhysicalItemStatus, ? extends PhysicalItemHistory, ? extends PhysicalItemType, ? extends CivilOrMilitary> and more. Every time you add a new generic, you break code in potentially hundreds of places.

To be clear, java generics “disappear at the leaf classes”, i.e., those classes that have no subclasses. (If Equipment was a concrete subclass, code dealing with Equipment would have no generics.) However, much of our code will be required to reference intermediate classes (classes which have superclasses and subclasses), and that is where the pain is. Equipment has many subclasses but a lot of code just needs to know it is an Equipment. Hence, generics-hell.

After many years of fighting this, I have come to believe that the price of generics is too high to include them in our “main” classes. Generics are very useful, but they MUST be contained within the code that needs them, while all other code must be able to work without knowing about them.

My solution is a “bridge” method that converts the main class to a particular generic instance. This method takes the client from the main instance (that has no generics) to the generic secondary instance (that DOES have the generics).

In our example, there is no getter and setter for the status field on PhysicalItem. Instead, a new method toPhysicalItemStatus() returns a PhysicalItem “bridge” instance which DOES have generics.

The following is a complete example:


public abstract class PhysicalItem{
    protected PhysicalItemStatus status;

    /**
     * Delegate to a "Bridge" class that "casts" to an instance with the correct generics.
     */
    public HasPhysicalItemStatus<? extends PhysicalItemStatus> toPhysicalItemStatus() {
        return new HasPhysicalItemStatusBridge<>(this, PhysicalItemStatus.class);
    }
}

public class Equipment extends PhysicalItem {
    @Override public HasPhysicalItemStatus
<EquipmentStatus> toPhysicalItemStatus() {
        return new HasPhysicalItemStatusBridge<>(this, EquipmentStatus.class);
    }
}

public class Structure extends PhysicalItem {
    @Override public HasPhysicalItemStatus
<StructureStatus> toPhysicalItemStatus() {
        return new HasPhysicalItemStatusBridge<>(this, StructureStatus.class);
    }
}

public interface HasPhysicalItemStatus<STATUS extends PhysicalItemStatus>{
    STATUS getStatus();
    void setStatus(STATUS status);
}

/**
 * A "bridge" instance that switches to Status with the correct generics.
 * This is a new instance, so theoretically it requires heap space. However, everything is final, so 
 * short-lived instances will not incur instance allocation and garb age collection.
 *
 * Put this in the same package as the PhysicalItem so it can access a private/protected field (status).
 * Alternatively, can live in a different package, and the constructor needs to provide a getter and setter
 */
public final class HasPhysicalItemStatusBridge<STATUS extends PhysicalItemStatus> implements HasPhysicalItemStatus<STATUS>{
    private final PhysicalItem item;
    private final Class
<STATUS> statusClass;

    public HasPhysicalItemStatusBridge(PhysicalItem item, Class
<STATUS> statusClass) {
        this.statusClass = statusClass;
        this.item = item;
    }

    @SuppressWarnings("unchecked") @Override public STATUS getStatus() {
        return (STATUS) item.status;
    }

    @Override public void setStatus(STATUS newStatus) {
        assert statusClass.isInstance(newStatus); //intermediate classes need checking
        item.status = newStatus;
    }
}

public abstract class PhysicalItemStatus {}
public class EquipmentStatus extends PhysicalItemStatus {}
public class StructureStatus extends PhysicalItemStatus {}

The test code below shows the results of all the getters and setters. Subclasses have correct typing. Intermediate classes have “as correct as can be” methods but will always require some amount of runtime type checking. The possibility of runtime errors exist, but no worse than before. We catch them at runtime and throw an Exception rather than corrupting the data model.

public static void testExamples(PhysicalItem item, Equipment equipment, Structure structure, 
      PhysicalItemStatus itemStatus, EquipmentStatus equipmentStatus, StructureStatus structureStatus) {

    //Correctly typed
    HasPhysicalItemStatus<? extends PhysicalItemStatus> physicalItemBridge = item.toPhysicalItemStatus();
    HasPhysicalItemStatus
<EquipmentStatus> equipmentBridge = equipment.toPhysicalItemStatus();
    HasPhysicalItemStatus
<StructureStatus> structureBridge = structure.toPhysicalItemStatus();

    //Correctly typed
    PhysicalItemStatus status1 = physicalItemBridge.getStatus();
    EquipmentStatus status2 = equipmentBridge.getStatus();
    StructureStatus status3 = structureBridge.getStatus();

    //supertype has issues. (But always will)
    physicalItemBridge.setStatus(itemStatus); //COMPILE ERROR because the type is <? extends PhysicalItemStatus> instead of <PhysicalItemStatus>

    //supertype "solution" to the above error also has issues
    HasPhysicalItemStatus
<PhysicalItemStatus> physicalItemStatusBridge2 = (HasPhysicalItemStatus<PhysicalItemStatus>) item.toPhysicalItemStatus(); //have to cast
    physicalItemStatusBridge2.setStatus(itemStatus); //This is now legal but a possible runtime error, so it needs runtime checking

    physicalItemBridge.setStatus(equipmentStatus); //COMPILE ERROR CORRECTLY
    physicalItemBridge.setStatus(structureStatus); //COMPILE ERROR CORRECTLY

    equipmentBridge.setStatus(itemStatus); //COMPILE ERROR CORRECTLY
    equipmentBridge.setStatus(equipmentStatus); //fine
    equipmentBridge.setStatus(structureStatus); //COMPILE ERROR CORRECTLY

    structureBridge.setStatus(itemStatus); //COMPILE ERROR CORRECTLY
    structureBridge.setStatus(equipmentStatus); //COMPILE ERROR CORRECTLY
    structureBridge.setStatus(structureStatus); //fine
}

The important point is that:

  1. The main classes are declared free of generics. So we don’t degrade the overall code base.
  2. Generics ARE provided for code that can benefit from it. The cost is one extra method that delegates/bridges to an intermediate generically-typed interface..

Death to yaml

A while back, spring started supporting yaml files as well as properties files for its configuration. Newer things sounded good so I switched to it.

My experience has been awful. Silent failures that don’t show errors and lead me down wild goose chases.

The problem is that yaml files are white-space sensitive. Indentation matters. And the indentation must be consistent: I you use 2-spaces somewhere and 3-spaces somewhere else, the indentation will not work and the parser will silently ignore the properties.

I must have had 20 of these errors. Sometimes I caught it quickly. Other times I spent an hour looking for the problem in code that wasn’t there. I’ve had enough. Back to boring properties files.

Note that we will often find yaml code on the internet that we want to use. For that, pick one of the yaml-to-properties online web pages.

Here is my last error that was the final straw:

The following is incorrect! Run this code and spring will throw a fatal error that it could not find the Datasource

spring:
 application:
  name: jsonstore-server
  main:
    banner-mode: OFF
  datasource:
    url: jdbc:postgresql://${postgres.url:localhost:5432/jsonstore}

This is correct! Can you spot the difference?

spring:
  application:
    name: jsonstore-server
  main:
    banner-mode: OFF
  datasource:
    url: jdbc:postgresql://${postgres.url:localhost:5432/jsonstore}

I rest my case.

Playwright or Selenium?

Playwright and Selenium are the two big choices when choosing a browser user-interface test programming tool. Selenium has been around a long time. Playwright is the new kid in town.

I’ve used Selenium before. It is a pita but it mostly works. Unfortunately, even 99% “mostly works” is a problem when you are running hundreds of tests. That means something is always failing. So you wind up writing all your code with “wait-until-something-is true” and “try-this-multiple-times-until-it-succeeds” features. And then it still fails once in a while, so you just have to repeat the whole test and then it works. The bigger the project, the worse this problem becomes.

In short, we use Selenium because we have to, not because we like it.

I had the opportunity to start a new project so I tried Playwright. Still a learning curve. Still requires a lot of work. But they took all the “wait-until” stuff and moved it “behind the scenes”. It still has to be done, but you, the programmer, don’t have to handle it yourself. unless you want to. Much better.

After 2 weeks working with Playwright, I was still impressed. Progress was slow but steady.  The hardest part was that I am using Vaadin as the front-end development tool and they make serious use of the shadow DOM, so each new element type took trial and error to get working. This would have been the same amount of work in Playwright and Selenium.

I was also fighting against the “best-practices” of Playwright. I like to use “id” attributes whenever I am selecting something. And I really like to use XPath. Yes, XPath can be brittle, but don’t kid yourself: UI testing is always going to be brittle. Now, Playwright doesn’t support XPath inside the shadow-dom, and I was constantly running into that problem. Eventually, everything I was doing with XPath was easily handled using CSS. For example, select the element with type “vaadin-form-layout” and attributes class=”user=prefs” and id=”userPrefsId”. So I was writing “xpath-ish” and it was easily translated into CSS “vaadin-form-layout[user=”prefs”][id=userPrefsId”]” and so forth. Anyway, personal preferences and nothing to do with the subject of Playwright vs Selenium.

And then I read this guy and he scared me:

https://javascript.plainenglish.io/playwrights-auto-waiting-is-wrong-2f9e24beb3b8

https://zhiminzhan.medium.com/waiting-strategies-for-test-steps-in-web-test-automation-aaae828eb3b3

https://zhiminzhan.medium.com/why-raw-selenium-syntax-is-better-than-cypress-and-playwright-a0f796aafc43

https://zhiminzhan.medium.com/correct-wrong-playwrights-advantage-over-selenium-part-1-playwright-is-modern-and-faster-than-0a652c7e9ee7

https://zhiminzhan.medium.com/why-raw-selenium-syntax-is-better-than-cypress-and-playwright-part-2-the-audience-matters-a8375e6918e4

https://zhiminzhan.medium.com/playwright-vs-selenium-webdriver-syntax-comparison-by-example-4ad74ca59dcc

https://medium.com/geekculture/optimize-selenium-webdriver-automated-test-scripts-speed-12d23f623a6

His arguments were reasonable but not sufficiently detailed to be definitively persuasive. To summarize at a very high level, the best arguments were:

  1. Playwright did waiting wrong.
  2. Selenium is the web-standard and google/facebook will make sure it is up to date. Playwright could get left behind.

Ok, both of these are serious accusations. And I don’t feel qualified to comment on their validity.

Emotionally, the author, Zhimin Zhan seemed a bit cranky. Certainly seems like an expert, but sometimes experts get cranky when their favorite technology gets left behind. Either possibility seemed plausible.

So I decided I would redo the last 2 weeks work in Playwright in Selenium. It only took a few hours.

As soon as I ran the same tests in Selenium, I remembered why it was always so frustrating:

The first problem was with the Save-Menu. On startup it is inactive (disabled=”true”) and my tests assert that. However, the Selenium method isEnabled() returned true. Google “selenium isenabled not working”. My God! How many years now and that is STILL broken?

We shouldn’t all have to write this crappy code:

public static boolean isEnabled(WebElement element) {
   boolean enabled1 = element.isEnabled(); //this can be wrong
   String disabled = element.getAttribute("disabled"); //this is reliable
   boolean disabled2 = disabled != null && Boolean.parseBoolean(disabled);
   if (enabled1 == disabled2) {
      System.err.println("discrepancy in isEnabled");
      enabled1 = !disabled2;
   }
   return enabled1;
}

The next problem was when I clicked on a VaadinSideNavItem. I got this error:

org.openqa.selenium.ElementClickInterceptedException: element click intercepted: Element <vaadin-side-nav-item path="domain/person" 
id="CometPersonInit-peopleNav" role="listitem" has-children="">...</vaadin-side-nav-item> is not clickable at point (127, 92).
Other element would receive the click: <html lang="en" theme="dark">...</html>

The ‘theme=”dark”‘ element is adjacent to the side-nav button. So something was wrong with the point location calculation.

Setting an implicit wait period did not work. An explicit wait period did not work either. One thing that really sucks about wait code is that it swallows the exception so you don’t see the actual problem. (You are ignoring the exception and not logging it.) So when it fails, all you know is that it did not work for X seconds; not why.

The third issue is that the Selenium isVisible() method checks the element values but does not actually check to see if the element has been scrolled into view. Playwright does it correctly (my interpretation of “isVisible” is literal). Playwright also automatically scrolls elements into view when you try and act upon them. Very nice.

So I had 3 immediate frustrations with Selenium that Playwright took care of.

I sat back and googled some more. I found a video I liked where the speaker asserted that there was no comparison between the two.

At this point I was inclined to agree with him. And I’d invested a day to verify I was making the best decision. Out with Selenium. In with Playwright.

Now I am not saying Playwright is without difficulties. The codegen tool is a miss more than a hit when I use it to try and auto-code the Locators. And I believe I have found a bug where it just fails to work correctly with chromium. That was a huge frustration that cost a week of stoppage hell until I ran out of ideas and tried firefox instead of chromium. Firefox worked, and I was able to start moving again.

Spatialite DLL hell

Spatialite is a geospatial-enabled extension to sqlite. I must have it. I intend to use it inside our commercial java product, which has windows and linux users. The development environment is Eclipse. The OS is Windows 10.

The author of spatialite mentions that the windows environment suffers from DLL-Hell. Well yes it does.

Installing sqlite and the xerial-jdbc software is easy and not covered.

You download the spatialite files here. It isn’t well explained, but you need the “mod_spatialite–.7z file.

My preferred eclipse setup is to create a /lib folder in my eclipse project and put the jars and dlls there. Below is a simple test project showing the locations of the files. Note that the sqlite-jdbc.jar is included with a “Native library location” that points to the folder containing the dll files. This is the standard way to do things. It works most of the time.

The author of spatialize has some good installation instructions and sample code. I added the sample file and tried to run it.

The result was the following error: Failed to load extension mod_spatialite. Trying to find file manually Exception in thread "main" java.lang.UnsatisfiedLinkError: Can't load library: C:\apps\eclipse-64-workspace\spatialite-test2\lib\mod_spatialite.dll at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1827) at java.lang.Runtime.load0(Runtime.java:809) at java.lang.System.load(System.java:1086) at SpatialiteSample.main(SpatialiteSample.java:40)

This is the well-known dll hell. The eclipse launcher uses the “root” folder of the project as the root java location. Here are the details. The command line is: "C:\Program Files\Java\jdk1.8.0_121\bin\javaw.exe" -agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:57143 -Djava.library.path=C:\apps\eclipse-64-workspace\spatialite-test2\lib -Dfile.encoding=Cp1252 -classpath C:\apps\eclipse-64-workspace\spatialite-test2\bin;C:\apps\eclipse-64-workspace\spatialite-test2\lib\sqlite-jdbc-3.18.0.jar SpatialiteSample

At this point, someone gets desperate and recommends adding the spatialite path to the windows PATH variable, or drop the dlls in the /system folder. But this is intended as a professional installation, not something for the enthusiatic developer. These are not acceptable recommendations.

To simplify the problem, a good next step is to move the dlls to the root folder. Can’t miss them there, can we?

And that works. The “mod_spatialite.dll” is found in the root folder. The command line is: "C:\Program Files\Java\jdk1.8.0_121\bin\javaw.exe" -agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:57854 -Dfile.encoding=Cp1252 -classpath C:\apps\eclipse-64-workspace\spatialite-test3\bin;C:\apps\eclipse-64-workspace\spatialite-test3\lib\sqlite-jdbc-3.18.0.jar SpatialiteSample.

So we can run spatialite in eclipse if we are willing to place the dlls in the root directory. We are not.

At this point, I am extremely puzzled. To be continued, hopefully…

Java Null Pointer Static Analysis in Eclipse – JDT vs CheckerFramework – False-Positive comparison

We’re talking about this and this. For those of us who program in Eclipse using Java, what is the best tool(s) for checking null pointer exceptions.

There have been several attempts before, but nothing has made its way into the official language. Eclipse does provide good null pointer checking out of the box, but there is only so much it can do. To improve the power of the checkers, we need to add annotations.

Note: In the following, I am using JDK 1.8.

  1. We have Eclipse itself, using the JDT annotations Eclipse help.
  2. We have the standard checker framework.
  3. We have FindBugs, which I believe is used by sonarcube.

There is great power to have something built into the environment, so the standard eclipse system has a strong advantage. Unless it has serious problems, it should be part of the standard development environment.

Unfortunately, it does have serious problems. Take a look at the following program: [sourcecode lang=”java” highlight=”12″] public class TestNullCode {

@org.eclipse.jdt.annotation.Nullable
private String testJdt;

public void testJdt() {
    if (testJdt == null) {
        testJdt = &quot;not null&quot;;
    }
    // this line causes a false-positive warning
    // Potential null pointer access: The field testJdt is specified as @Nullable
    if (testJdt.equals(&quot;not null&quot;)) {
        System.out.println(&quot;will work because testJdt is not null&quot;);
    }
}

} [/sourcecode]

This example shows a false-positive that is provably incorrect. The compiler issues a warning (or error, depending on your settings) on line 12, that is untrue.

Frankly, this is pretty sad. The reasons are justifiable, but the result is sad, nonetheless.

For non-final @Nullable fields the simplest strategy for avoiding any nullness related risks is to pessimistically assume potential null at every read. This means for strict checking no flow analysis should be applied to @Nullable fields..

How does the checkerframework do? Answer: No problems at all: [sourcecode lang=”java” highlight=”11″] public class TestNullCode {

@org.checkerframework.checker.nullness.qual.Nullable
private String testCheckerFramework;

public void testCheckerFramework() {
    if (testCheckerFramework == null) {
        testCheckerFramework = &quot;not null&quot;;
    }
    // this line is ok
    if (testCheckerFramework.equals(&quot;not null&quot;)) {
        System.out.println(&quot;will work because testCheckerFramework is not null&quot;);
    }

    testCheckerFramework = null;
    // following line is flagged by checker framework
    // dereference of possibly-null reference testCheckerFramework
    System.out.println(&quot;will fail because testCheckerFramework is not null&quot; + testCheckerFramework.toString());
}

} [/sourcecode]

How does FindBugs do? Answer: No problems at all. Same as checkerframework , so I won’t post the identical code.

Eclipse has a workaround that costs a line of extra code:

While this appears to be a very drastic restriction, the remedy is quite easy: before dereferencing a @Nullable field it has to be assigned to a local variable. Flow analysis is then safely applied to the local variable with no risk of side effects, aliasing nor concurrency, since local variables are not shared with any code locations that would be outside the scope of the analysis. I.e., the flow analysis can see everything it needs to consider regarding local variables.

[sourcecode lang=”java” highlight=”12″] public class TestNullCode {

@org.eclipse.jdt.annotation.Nullable
private String testJdt;

public void testJdtCopy() {
    String copy = testJdt;
    if (copy == null) {
        copy = &quot;not null&quot;;
    }
    //This is fine
    if (copy.equals(&quot;not null&quot;)) {
        System.out.println(&quot;will work because copyis not null&quot;);
    }
}

} [/sourcecode] I consider this “solution” ugly. I get it, but I don’t buy it. At some point, I would expect the Eclipse engine to get better.

So at this point, we still need the other tools.

You can download my example eclipse project here.

Running Java apps as Windows Services

A tricky little problem, as it turns out. There is little out in google world to help you, and only a few tools. License issues with almost all I found, especially running on Windows boxes in 64 bit mode.

 I finally found Yet Another Java Service Wrapper at yajsw. License is LGPL and it supports 64 bit windows. I was able to get my application running very quickly. Worked perfectly the first time. Getting going was interesting:

 1) You download yajsw and extract the zip file.
 2) You run your app and lookup its PID.
3) You run the yajsw script generator, and it analyzes your running application to create a script file.
4) You then run the yajsw bat files to install-service, uninstall-service, start-service, stop-service, etc.

The only trick is to run the bat files with administrator permissions. (Right-click on the bat file and select “Run as administrator”.)

Right now the yajsw folder needs to exist on the server as well. Doubtless there are more convenient ways to package the install, but this was a good first step.

Debug xquery in eclipse. Configuration

So you want to run xquery in eclipse. You eventually find your way to XQDT. Some concern that we’re working with beta software (version 0.8.0 as of this post), but you bravely forge ahead. You survive the installation and run into your first roadblock at the getting started. Here you discover that they are working with Zorba 0.9.4 (later you suspect this is a misprint, but never mind) and do NOT SUPPORT DEBUGGING! If you can’t debug, this is DOA.

You follow the note to install Zorba from the subversion trunk, but this is a C project and no compiled distributions. Dead end 1.

You download and install Zorba itself (version 1.4.0), and while it runs, you are unable to debug. (Perhaps you can, but you never figured it out and got impatient. Plus, you refuse to depend on other programs installed in Windows anyway, because this is a distribution/installation hell.) Probably dead end 2.

You hear about the 28msec Sausalito project. They apparently have a version of Zorba that supports debugging. Looking better, but you don’t want a separate Eclipse installation. After a bit of exploration, you figure out to how attach the Sausalito project code to your existing Eclipse installation:

1) Install XQDT into Eclipse as normal. Steps described here

2) Download the Sausalito project from here, and extract the zipped contents.

3) Navigate to the plugins/com.28.msec.sausalito.win32/coresdk/bin directory.

Note: In my download, the path is com.28msec.sausalito.win32_1.2.10.201011291638

4) Copy the contents of this directory into your existing Eclipse project. (Put it somewhere convenient, of course, such as /lib/sausalito,)

Note: Obviously, you do not need all the contents of this directory. I do not yet know which files are required, and which are not. I know the zorba-sausastore.exe file is required, along with some others.

5) Now to add the new intepreter to Eclipse. Open Preferences -> XQuery -> Interpreters. Click the “Add…” button, then the “Browse” button. Navigate to the zorba-sausastore.exe file you just added, and select it. Eclipse should be able to inspect the file and set the default Interpreter name, e.g.,




I find it odd that Zorba is up to version 1.5.0, but XQDT is back on 0.9.4.

I also have NOT done anything with a Linux install. Obviously, you won’t have a win32 directory, but you’ll know what you’re doing anyway.