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/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"); } } 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;