From dffaef0c476fb3aad487207a415d296cbc275d57 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Mon, 5 Oct 2026 19:49:04 +0100 Subject: [PATCH 1/2] honour date facet bounds in xsd2inst samples formatDate compared its random seed, a full dateTime, against the bounds before cutting it down to the facet's type, so for date, time and the g* types every comparison came back as incomparable and the bounds were ignored. Where a reset did fire, it moved the random date by a few hours instead of moving it next to the bound. Cut the seed down to the type and pick its timezone first, then check it against all four facets the way validation does, treating an incomparable result as out of range. If it misses, try the bound itself and then the bound stepped inwards by the smallest unit the type shows. Co-Authored-By: Claude Opus 5.5 --- .../xmlbeans/impl/xsd2inst/SampleXmlUtil.java | 320 ++++++------------ .../tools/xsd2inst/checkin/Xsd2InstTest.java | 38 +++ 2 files changed, 133 insertions(+), 225 deletions(-) diff --git a/src/main/java/org/apache/xmlbeans/impl/xsd2inst/SampleXmlUtil.java b/src/main/java/org/apache/xmlbeans/impl/xsd2inst/SampleXmlUtil.java index fc19e4aaa..b7ccb0be3 100644 --- a/src/main/java/org/apache/xmlbeans/impl/xsd2inst/SampleXmlUtil.java +++ b/src/main/java/org/apache/xmlbeans/impl/xsd2inst/SampleXmlUtil.java @@ -650,249 +650,119 @@ private String formatDuration(SchemaType sType) { } private String formatDate(SchemaType sType) { + int btc = sType.getPrimitiveType().getBuiltinTypeCode(); GDateBuilder gdateb = new GDateBuilder(new Date(1000L * pick(365 * 24 * 60 * 60) + (30L + pick(20)) * 365 * 24 * 60 * 60 * 1000)); - GDate min = null, max = null; - - // Find the min and the max according to the type - switch (sType.getPrimitiveType().getBuiltinTypeCode()) { - case SchemaType.BTC_DATE_TIME: { - XmlDateTime x = (XmlDateTime) sType.getFacet(SchemaType.FACET_MIN_INCLUSIVE); - if (x != null) { - min = x.getGDateValue(); - } - x = (XmlDateTime) sType.getFacet(SchemaType.FACET_MIN_EXCLUSIVE); - if (x != null) { - if (min == null || min.compareToGDate(x.getGDateValue()) <= 0) { - min = x.getGDateValue(); - } - } + gdateb.setBuiltinTypeCode(btc); + if (pick(2) == 0) { + gdateb.clearTimeZone(); + } + if (satisfiesBounds(gdateb, sType)) { + return gdateb.toString(); + } - x = (XmlDateTime) sType.getFacet(SchemaType.FACET_MAX_INCLUSIVE); - if (x != null) { - max = x.getGDateValue(); - } - x = (XmlDateTime) sType.getFacet(SchemaType.FACET_MAX_EXCLUSIVE); - if (x != null) { - if (max == null || max.compareToGDate(x.getGDateValue()) >= 0) { - max = x.getGDateValue(); - } - } + // Step off a bound by the smallest unit the type shows + GDuration[] steps; + switch (btc) { + case SchemaType.BTC_DATE_TIME: + steps = new GDuration[]{new GDuration("P1D"), new GDuration("PT1H"), new GDuration("PT1M"), + new GDuration("PT1S"), new GDuration("PT0.001S")}; break; - } - case SchemaType.BTC_TIME: { - XmlTime x = (XmlTime) sType.getFacet(SchemaType.FACET_MIN_INCLUSIVE); - if (x != null) { - min = x.getGDateValue(); - } - x = (XmlTime) sType.getFacet(SchemaType.FACET_MIN_EXCLUSIVE); - if (x != null) { - if (min == null || min.compareToGDate(x.getGDateValue()) <= 0) { - min = x.getGDateValue(); - } - } - - x = (XmlTime) sType.getFacet(SchemaType.FACET_MAX_INCLUSIVE); - if (x != null) { - max = x.getGDateValue(); - } - x = (XmlTime) sType.getFacet(SchemaType.FACET_MAX_EXCLUSIVE); - if (x != null) { - if (max == null || max.compareToGDate(x.getGDateValue()) >= 0) { - max = x.getGDateValue(); - } - } + case SchemaType.BTC_TIME: + steps = new GDuration[]{new GDuration("PT1H"), new GDuration("PT1M"), + new GDuration("PT1S"), new GDuration("PT0.001S")}; break; - } - case SchemaType.BTC_DATE: { - XmlDate x = (XmlDate) sType.getFacet(SchemaType.FACET_MIN_INCLUSIVE); - if (x != null) { - min = x.getGDateValue(); - } - x = (XmlDate) sType.getFacet(SchemaType.FACET_MIN_EXCLUSIVE); - if (x != null) { - if (min == null || min.compareToGDate(x.getGDateValue()) <= 0) { - min = x.getGDateValue(); - } - } - - x = (XmlDate) sType.getFacet(SchemaType.FACET_MAX_INCLUSIVE); - if (x != null) { - max = x.getGDateValue(); - } - x = (XmlDate) sType.getFacet(SchemaType.FACET_MAX_EXCLUSIVE); - if (x != null) { - if (max == null || max.compareToGDate(x.getGDateValue()) >= 0) { - max = x.getGDateValue(); - } - } + case SchemaType.BTC_G_YEAR: + steps = new GDuration[]{new GDuration("P1Y")}; break; - } - case SchemaType.BTC_G_YEAR_MONTH: { - XmlGYearMonth x = (XmlGYearMonth) sType.getFacet(SchemaType.FACET_MIN_INCLUSIVE); - if (x != null) { - min = x.getGDateValue(); - } - x = (XmlGYearMonth) sType.getFacet(SchemaType.FACET_MIN_EXCLUSIVE); - if (x != null) { - if (min == null || min.compareToGDate(x.getGDateValue()) <= 0) { - min = x.getGDateValue(); - } - } - - x = (XmlGYearMonth) sType.getFacet(SchemaType.FACET_MAX_INCLUSIVE); - if (x != null) { - max = x.getGDateValue(); - } - x = (XmlGYearMonth) sType.getFacet(SchemaType.FACET_MAX_EXCLUSIVE); - if (x != null) { - if (max == null || max.compareToGDate(x.getGDateValue()) >= 0) { - max = x.getGDateValue(); - } - } + case SchemaType.BTC_G_YEAR_MONTH: + case SchemaType.BTC_G_MONTH: + steps = new GDuration[]{new GDuration("P1M")}; break; - } - case SchemaType.BTC_G_YEAR: { - XmlGYear x = (XmlGYear) sType.getFacet(SchemaType.FACET_MIN_INCLUSIVE); - if (x != null) { - min = x.getGDateValue(); - } - x = (XmlGYear) sType.getFacet(SchemaType.FACET_MIN_EXCLUSIVE); - if (x != null) { - if (min == null || min.compareToGDate(x.getGDateValue()) <= 0) { - min = x.getGDateValue(); - } - } - - x = (XmlGYear) sType.getFacet(SchemaType.FACET_MAX_INCLUSIVE); - if (x != null) { - max = x.getGDateValue(); - } - x = (XmlGYear) sType.getFacet(SchemaType.FACET_MAX_EXCLUSIVE); - if (x != null) { - if (max == null || max.compareToGDate(x.getGDateValue()) >= 0) { - max = x.getGDateValue(); - } - } + default: + steps = new GDuration[]{new GDuration("P1D")}; break; - } - case SchemaType.BTC_G_MONTH_DAY: { - XmlGMonthDay x = (XmlGMonthDay) sType.getFacet(SchemaType.FACET_MIN_INCLUSIVE); - if (x != null) { - min = x.getGDateValue(); - } - x = (XmlGMonthDay) sType.getFacet(SchemaType.FACET_MIN_EXCLUSIVE); - if (x != null) { - if (min == null || min.compareToGDate(x.getGDateValue()) <= 0) { - min = x.getGDateValue(); - } - } + } - x = (XmlGMonthDay) sType.getFacet(SchemaType.FACET_MAX_INCLUSIVE); - if (x != null) { - max = x.getGDateValue(); - } - x = (XmlGMonthDay) sType.getFacet(SchemaType.FACET_MAX_EXCLUSIVE); - if (x != null) { - if (max == null || max.compareToGDate(x.getGDateValue()) >= 0) { - max = x.getGDateValue(); - } - } - break; - } - case SchemaType.BTC_G_DAY: { - XmlGDay x = (XmlGDay) sType.getFacet(SchemaType.FACET_MIN_INCLUSIVE); - if (x != null) { - min = x.getGDateValue(); - } - x = (XmlGDay) sType.getFacet(SchemaType.FACET_MIN_EXCLUSIVE); - if (x != null) { - if (min == null || min.compareToGDate(x.getGDateValue()) <= 0) { - min = x.getGDateValue(); - } - } + GDate min = facetDate(sType, SchemaType.FACET_MIN_EXCLUSIVE); + if (min == null) { + min = facetDate(sType, SchemaType.FACET_MIN_INCLUSIVE); + } + GDate max = facetDate(sType, SchemaType.FACET_MAX_EXCLUSIVE); + if (max == null) { + max = facetDate(sType, SchemaType.FACET_MAX_INCLUSIVE); + } - x = (XmlGDay) sType.getFacet(SchemaType.FACET_MAX_INCLUSIVE); - if (x != null) { - max = x.getGDateValue(); - } - x = (XmlGDay) sType.getFacet(SchemaType.FACET_MAX_EXCLUSIVE); - if (x != null) { - if (max == null || max.compareToGDate(x.getGDateValue()) >= 0) { - max = x.getGDateValue(); - } - } - break; + for (GDate bound : new GDate[]{min, max}) { + if (bound == null) { + continue; } - case SchemaType.BTC_G_MONTH: { - XmlGMonth x = (XmlGMonth) sType.getFacet(SchemaType.FACET_MIN_INCLUSIVE); - if (x != null) { - min = x.getGDateValue(); - } - x = (XmlGMonth) sType.getFacet(SchemaType.FACET_MIN_EXCLUSIVE); - if (x != null) { - if (min == null || min.compareToGDate(x.getGDateValue()) <= 0) { - min = x.getGDateValue(); - } - } - - x = (XmlGMonth) sType.getFacet(SchemaType.FACET_MAX_INCLUSIVE); - if (x != null) { - max = x.getGDateValue(); - } - x = (XmlGMonth) sType.getFacet(SchemaType.FACET_MAX_EXCLUSIVE); - if (x != null) { - if (max == null || max.compareToGDate(x.getGDateValue()) >= 0) { - max = x.getGDateValue(); - } + if (satisfiesBounds(bound, sType)) { + return bound.toString(); + } + for (GDuration step : steps) { + GDateBuilder candidate = stepFrom(bound, step, bound == min, btc); + if (candidate != null && satisfiesBounds(candidate, sType)) { + return candidate.toString(); } - break; } } - if (min != null && max == null) { - if (min.compareToGDate(gdateb) >= 0) { - // Reset the date to min + (1-8) hours - Calendar c = gdateb.getCalendar(); - c.add(Calendar.HOUR_OF_DAY, pick(8)); - gdateb = new GDateBuilder(c); + // the bounds leave no value we can find + return gdateb.toString(); + } + + private static GDate facetDate(SchemaType sType, int facet) { + XmlObject x = sType.getFacet(facet); + return x == null ? null : ((SimpleValue) x).getGDateValue(); + } + + // compareToGDate returns 2 for an incomparable pair, which satisfies no bound + private static boolean satisfiesBounds(GDateSpecification d, SchemaType sType) { + GDate g; + int c; + if ((g = facetDate(sType, SchemaType.FACET_MIN_EXCLUSIVE)) != null && d.compareToGDate(g) != 1) { + return false; + } + if ((g = facetDate(sType, SchemaType.FACET_MIN_INCLUSIVE)) != null && + (c = d.compareToGDate(g)) != 0 && c != 1) { + return false; + } + if ((g = facetDate(sType, SchemaType.FACET_MAX_EXCLUSIVE)) != null && d.compareToGDate(g) != -1) { + return false; + } + return (g = facetDate(sType, SchemaType.FACET_MAX_INCLUSIVE)) == null || + (c = d.compareToGDate(g)) == 0 || c == -1; + } + + private static GDateBuilder stepFrom(GDate bound, GDuration step, boolean up, int btc) { + // date arithmetic needs a complete date and time, so fill in what the + // type leaves out and drop it again afterwards; 2000 is a leap year, so + // --02-29 survives + GDateBuilder b = new GDateBuilder(bound); + try { + if (!b.hasYear()) { + b.setYear(2000); } - } else if (min == null && max != null) { - if (max.compareToGDate(gdateb) <= 0) { - // Reset the date to max - (1-8) hours - Calendar c = gdateb.getCalendar(); - c.add(Calendar.HOUR_OF_DAY, -pick(8)); - gdateb = new GDateBuilder(c); + if (!b.hasMonth()) { + b.setMonth(1); } - } else if (min != null && max != null) { - if (min.compareToGDate(gdateb) >= 0 || max.compareToGDate(gdateb) <= 0) { - // Find a date between the two - Calendar c = min.getCalendar(); - Calendar cmax = max.getCalendar(); - c.add(Calendar.HOUR_OF_DAY, 1); - if (c.after(cmax)) { - c.add(Calendar.HOUR_OF_DAY, -1); - c.add(Calendar.MINUTE, 1); - if (c.after(cmax)) { - c.add(Calendar.MINUTE, -1); - c.add(Calendar.SECOND, 1); - if (c.after(cmax)) { - c.add(Calendar.SECOND, -1); - c.add(Calendar.MILLISECOND, 1); - if (c.after(cmax)) { - c.add(Calendar.MILLISECOND, -1); - } - } - } - } - gdateb = new GDateBuilder(c); + if (!b.hasDay()) { + b.setDay(1); } + if (!b.hasTime()) { + b.setTime(0, 0, 0, null); + } + if (up) { + b.addGDuration(step); + } else { + b.subtractGDuration(step); + } + } catch (IllegalArgumentException | IllegalStateException | ArithmeticException e) { + // stepped past the range of years a GDate can hold + return null; } - - gdateb.setBuiltinTypeCode(sType.getPrimitiveType().getBuiltinTypeCode()); - if (pick(2) == 0) { - gdateb.clearTimeZone(); - } - return gdateb.toString(); + b.setBuiltinTypeCode(btc); + return b; } private SchemaType closestBuiltin(SchemaType sType) { diff --git a/src/test/java/tools/xsd2inst/checkin/Xsd2InstTest.java b/src/test/java/tools/xsd2inst/checkin/Xsd2InstTest.java index 3bad771fa..2e0dbe7a2 100644 --- a/src/test/java/tools/xsd2inst/checkin/Xsd2InstTest.java +++ b/src/test/java/tools/xsd2inst/checkin/Xsd2InstTest.java @@ -106,6 +106,44 @@ void testTotalDigitsStillNarrowsTheBounds() throws Exception { assertTrue(unconstrained.contains("1000.00"), unconstrained); } + private static SchemaTypeSystem simpleTypeSchema(String base, String facets) throws Exception { + XmlObject xsd = XmlObject.Factory.parse("" + + "" + + "" + facets + "" + + ""); + return XmlBeans.compileXsd(new XmlObject[]{xsd}, XmlBeans.getBuiltinTypeSystem(), new XmlOptions()); + } + + @Test + void testDateSamplesSatisfyTheirBounds() throws Exception { + // the random seed is a full dateTime, so for the other date types the facet + // checks compared values that were never comparable, and the bounds were + // ignored. The seed and the timezone are random, so try each case repeatedly. + String[][] cases = { + {"dateTime", ""}, + {"dateTime", ""}, + {"dateTime", "" + + ""}, + {"time", ""}, + {"date", ""}, + {"date", ""}, + {"gYearMonth", ""}, + {"gYear", ""}, + {"gYear", ""}, + {"gMonthDay", ""}, + {"gMonth", ""}, + {"gDay", ""}, + }; + for (String[] c : cases) { + SchemaTypeSystem sts = simpleTypeSchema(c[0], c[1]); + for (int i = 0; i < 20; i++) { + String sample = SampleXmlUtil.createSampleForType(sts.globalElements()[0]); + XmlObject doc = sts.parse(sample, null, null); + assertTrue(doc.validate(), c[0] + " " + c[1] + ": " + sample); + } + } + } + @Test void testSampleXmlUtil() throws Exception { XmlObject xobj; From 28fc31908c90ed222d5e2e107104f28f2dbb352a Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Mon, 5 Oct 2026 19:57:00 +0100 Subject: [PATCH 2/2] check facet error codes in IncomparableMinFacetValidateTest Assert the expected cvc-*-valid code rather than only that validation failed, and compile the schema once for the class. Co-Authored-By: Claude Opus 5.5 --- .../IncomparableMinFacetValidateTest.java | 59 +++++++++++++------ 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/src/test/java/misc/checkin/IncomparableMinFacetValidateTest.java b/src/test/java/misc/checkin/IncomparableMinFacetValidateTest.java index 75deb5713..b90b1aef8 100644 --- a/src/test/java/misc/checkin/IncomparableMinFacetValidateTest.java +++ b/src/test/java/misc/checkin/IncomparableMinFacetValidateTest.java @@ -21,13 +21,15 @@ import org.apache.xmlbeans.XmlObject; import org.apache.xmlbeans.XmlOptions; import org.apache.xmlbeans.impl.xb.xsdschema.SchemaDocument; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; public class IncomparableMinFacetValidateTest { @@ -66,50 +68,69 @@ private static String xsd() { return sb.append("").toString(); } - private static boolean validate(String element, String value) throws Exception { - SchemaTypeLoader loader = XmlBeans.loadXsd(new XmlObject[]{SchemaDocument.Factory.parse(xsd())}); + private static SchemaTypeLoader loader; + + @BeforeAll + static void compileSchema() throws Exception { + loader = XmlBeans.loadXsd(new XmlObject[]{SchemaDocument.Factory.parse(xsd())}); + } + + // the error codes reported when validating value against the element's facet + private static List validate(String element, String value) throws Exception { XmlObject doc = loader.parse( "" + value + "", null, null); List errors = new ArrayList<>(); - return doc.validate(new XmlOptions().setErrorListener(errors)); + doc.validate(new XmlOptions().setErrorListener(errors)); + return errors.stream().map(XmlError::getErrorCode).collect(Collectors.toList()); + } + + private static void assertValid(String element, String value) throws Exception { + assertEquals(Collections.emptyList(), validate(element, value), element + " " + value); + } + + // element names end in the facet, and each facet has its own error code + private static void assertInvalid(String element, String value) throws Exception { + String facet = element.substring(element.indexOf('-') + 1); + assertEquals(Collections.singletonList("cvc-" + facet + "-valid"), validate(element, value), + element + " " + value); } @Test void incomparableDateTimeFailsEveryBound() throws Exception { // not greater than, less than, or equal to the bound, so no facet is satisfied for (String facet : FACETS) { - assertFalse(validate("dt-" + facet, DT_INCOMPARABLE), facet); + assertInvalid("dt-" + facet, DT_INCOMPARABLE); } } @Test void incomparableDurationFailsEveryBound() throws Exception { for (String facet : FACETS) { - assertFalse(validate("dur-" + facet, DUR_INCOMPARABLE), facet); + assertInvalid("dur-" + facet, DUR_INCOMPARABLE); } } @Test void comparableDateTimeStillChecked() throws Exception { // equal to the bound - assertTrue(validate("dt-minInclusive", "2000-01-01T12:00:00Z")); - assertFalse(validate("dt-minExclusive", "2000-01-01T12:00:00Z")); + assertValid("dt-minInclusive", "2000-01-01T12:00:00Z"); + assertInvalid("dt-minExclusive", "2000-01-01T12:00:00Z"); // more than 14 hours past the bound, so the missing timezone does not matter - assertTrue(validate("dt-minInclusive", "2000-01-03T12:00:00")); - assertTrue(validate("dt-minExclusive", "2000-01-03T12:00:00")); + assertValid("dt-minInclusive", "2000-01-03T12:00:00"); + assertValid("dt-minExclusive", "2000-01-03T12:00:00"); // more than 14 hours short of it - assertFalse(validate("dt-minInclusive", "1999-12-30T12:00:00")); - assertFalse(validate("dt-minExclusive", "1999-12-30T12:00:00")); + assertInvalid("dt-minInclusive", "1999-12-30T12:00:00"); + assertInvalid("dt-minExclusive", "1999-12-30T12:00:00"); } @Test void comparableDurationStillChecked() throws Exception { - assertTrue(validate("dur-minInclusive", "P30D")); - assertFalse(validate("dur-minExclusive", "P30D")); + assertValid("dur-minInclusive", "P30D"); + assertInvalid("dur-minExclusive", "P30D"); // two months are at least 59 days - assertTrue(validate("dur-minInclusive", "P2M")); - assertTrue(validate("dur-minExclusive", "P2M")); - assertFalse(validate("dur-minInclusive", "P27D")); - assertFalse(validate("dur-minExclusive", "P27D")); + assertValid("dur-minInclusive", "P2M"); + assertValid("dur-minExclusive", "P2M"); + assertInvalid("dur-minInclusive", "P27D"); + assertInvalid("dur-minExclusive", "P27D"); } }