diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/core/dsom/ElementBase.scala b/daffodil-core/src/main/scala/org/apache/daffodil/core/dsom/ElementBase.scala index 29bf03b311..606f0b3f0f 100644 --- a/daffodil-core/src/main/scala/org/apache/daffodil/core/dsom/ElementBase.scala +++ b/daffodil-core/src/main/scala/org/apache/daffodil/core/dsom/ElementBase.scala @@ -970,8 +970,6 @@ trait ElementBase (simpleType.primType =:= PrimType.String || simpleType.primType =:= PrimType.HexBinary) - import org.apache.daffodil.runtime1.dsom.FacetTypes.* - private lazy val hasPattern: Boolean = typeDef.optRestriction.exists(_.hasPattern) private lazy val hasEnumeration: Boolean = typeDef.optRestriction.exists(_.hasEnumeration) protected lazy val hasLength: Boolean = typeDef.optRestriction.exists(_.hasLength) @@ -984,7 +982,7 @@ trait ElementBase private lazy val hasTotalDigits = typeDef.optRestriction.exists(_.hasTotalDigits) private lazy val hasFractionDigits = typeDef.optRestriction.exists(_.hasFractionDigits) - final lazy val patternValues: Seq[FacetValueR] = { + final lazy val patternValues: Seq[FacetPattern] = { Assert.invariant(hasPattern) typeDef.optRestriction.map { _.patternValues }.getOrElse(Nil) } diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/core/dsom/Facets.scala b/daffodil-core/src/main/scala/org/apache/daffodil/core/dsom/Facets.scala index a3ca0c4455..bb1057d86e 100644 --- a/daffodil-core/src/main/scala/org/apache/daffodil/core/dsom/Facets.scala +++ b/daffodil-core/src/main/scala/org/apache/daffodil/core/dsom/Facets.scala @@ -17,17 +17,14 @@ package org.apache.daffodil.core.dsom -import java.math.BigInteger import scala.xml.Node import org.apache.daffodil.lib.exceptions.Assert import org.apache.daffodil.lib.iapi.InvalidRestrictionPolicy -import org.apache.daffodil.lib.xml.XMLUtils import org.apache.daffodil.runtime1.dpath.NodeInfo.PrimType import org.apache.daffodil.runtime1.dsom.* trait Facets { self: Restriction => - import org.apache.daffodil.runtime1.dsom.FacetTypes.* private def retrieveFacetValueFromRestrictionBase( xml: Node, @@ -159,75 +156,49 @@ trait Facets { self: Restriction => } final lazy val hasEnumeration: Boolean = - (localEnumerationValue.length > 0) || (getRemoteFacetValues(Facet.enumeration).size > 0) + localEnumerationValue.nonEmpty || remoteBaseFacets.collectFirst { + case _: FacetEnumeration => + }.isDefined final lazy val hasPattern: Boolean = - (localPatternValue.length > 0) || (getRemoteFacetValues(Facet.pattern).size > 0) + localPatternValue.nonEmpty || remoteBaseFacets.collectFirst { case _: FacetPattern => + }.isDefined final lazy val hasLength: Boolean = - (localLengthValue != "") || (getRemoteFacetValues(Facet.length).size > 0) + localLengthValue.nonEmpty || remoteBaseFacets.collectFirst { case _: FacetLength => + }.isDefined final lazy val hasMinLength: Boolean = - (localMinLengthValue != "") || (getRemoteFacetValues(Facet.minLength).size > 0) + localMinLengthValue.nonEmpty || remoteBaseFacets.collectFirst { case _: FacetMinLength => + }.isDefined final lazy val hasMaxLength: Boolean = - (localMaxLengthValue != "") || (getRemoteFacetValues(Facet.maxLength).size > 0) + localMaxLengthValue.nonEmpty || remoteBaseFacets.collectFirst { case _: FacetMaxLength => + }.isDefined final lazy val hasMinInclusive: Boolean = - (localMinInclusiveValue != "") || (getRemoteFacetValues(Facet.minInclusive).size > 0) + localMinInclusiveValue.nonEmpty || remoteBaseFacets.collectFirst { + case _: FacetMinInclusive => + }.isDefined final lazy val hasMaxInclusive: Boolean = - (localMaxInclusiveValue != "") || (getRemoteFacetValues(Facet.maxInclusive).size > 0) + localMaxInclusiveValue.nonEmpty || remoteBaseFacets.collectFirst { + case _: FacetMaxInclusive => + }.isDefined final lazy val hasMinExclusive: Boolean = - (localMinExclusiveValue != "") || (getRemoteFacetValues(Facet.minExclusive).size > 0) + localMinExclusiveValue.nonEmpty || remoteBaseFacets.collectFirst { + case _: FacetMinExclusive => + }.isDefined final lazy val hasMaxExclusive: Boolean = - (localMaxExclusiveValue != "") || (getRemoteFacetValues(Facet.maxExclusive).size > 0) + localMaxExclusiveValue.nonEmpty || remoteBaseFacets.collectFirst { + case _: FacetMaxExclusive => + }.isDefined final lazy val hasTotalDigits: Boolean = - (localTotalDigitsValue != "") || (getRemoteFacetValues(Facet.totalDigits).size > 0) + localTotalDigitsValue.nonEmpty || remoteBaseFacets.collectFirst { + case _: FacetTotalDigits => + }.isDefined final lazy val hasFractionDigits: Boolean = - (localFractionDigitsValue != "") || (getRemoteFacetValues(Facet.fractionDigits).size > 0) + localFractionDigitsValue.nonEmpty || remoteBaseFacets.collectFirst { + case _: FacetFractionDigits => + }.isDefined - final lazy val patternValues: Seq[FacetValueR] = { - val values: Seq[(Facet.Type, Values)] = combinedBaseFacets.filter { case (f, _) => - f == Facet.pattern - } - val valuesRemapped: Seq[FacetValueR] = values.map { - case (f, v: String) => { - // - // The DFDL Infoset can contain strings which hold characters - // that are not allowed in XML at all. - // - // In order to talk about these characters in a XSD pattern facet - // we use a remapping of such characters into the Unicode - // Private Use Area, so as to have XML-legal characters. - // - // See the section titled "XML Illegal Characters" on this web page: - // https://daffodil.apache.org/infoset/ - // - // Before processing a regex of these characters in Daffodil's pattern facet - // validation, we must remap these PUA characters back to the originally - // intended code points, since that's what the Infoset strings will contain. - // - // Consider the character code 0xB. This is illegal in XML v1.0 documents. - // A DFDL Schema is an XML Schema, which is an XML document; hence, we cannot - // use the character with code 0xB directly, nor can we use an XML numeric - // character entity like for it. The character is simply disallowed in - // XML, including DFDL schemas. Hence, we mention 0xB by using a remapping of it - // to the PUA area character 0xE00B, which we express by  - // Hence a pattern regex like "[ 0-9a-zA-Z]" will match - // the character with char code 0xB (remapped from E00B), as well as spaces, - // and alphanumeric characters. - // - // The XSD numeric character entity  can be used to match ASCII NUL - // (char code 0). - // - // This remapping is for pattern facets, which are inside a DFDL schema, - // and so will not contain CR characters, since XML reading will convert those - // to LF. To discuss CR in this pattern we can't use ` ` syntax because that - // turns into a CR which gets turned into a LF. Plus the pattern value is - // an XML attribute, the value of which gets its whitespace collapsed, all - // line-ending chars converted to spaces, and adjacent spaces collapsed to one. - // - // So a pattern facet must use `\r` and '\n' to describe line-endings within the pattern. - // And in general one must be careful about whitespace. - // - val remapped: String = XMLUtils.remapPUAToXMLIllegalCharacters(v) - (f, remapped.r) - } + final lazy val patternValues: Seq[FacetPattern] = { + val valuesRemapped: Seq[FacetPattern] = combinedBaseFacets.collect { case f: FacetPattern => + f } (primType, tunable.invalidRestrictionPolicy) match { case (PrimType.String, _) => @@ -243,515 +214,51 @@ trait Facets { self: Restriction => } } - final lazy val enumerationValues: Option[String] = { - // Should only ever have one set per SimpleType - val values = combinedBaseFacets.filter { case (f, _) => f == Facet.enumeration } - if (values.size > 0) { - val (_, value) = values(0) - Some(value) - } else None - } - // TODO: Tidy up. Can likely replace getFacetValue with a similar call to combinedBaseFacets - // as combinedBaseFacets should contain the 'narrowed' values. - // + final lazy val enumerationValues: Option[String] = + combinedBaseFacets.collectFirst { case f: FacetEnumeration => f }.map(_.xmlValue) final lazy val lengthValue: java.math.BigDecimal = - getFacetValue(localLengthValue, Facet.length, hasLength) + combinedBaseFacets + .collectFirst { case f: FacetLength => f } + .map(f => new java.math.BigDecimal(f.bigIntValue)) + .getOrElse(SDE("The length facet was not found.")) final lazy val minLengthValue: java.math.BigDecimal = - getFacetValue(localMinLengthValue, Facet.minLength, hasMinLength) + combinedBaseFacets + .collectFirst { case f: FacetMinLength => f } + .map(f => new java.math.BigDecimal(f.bigIntValue)) + .getOrElse(SDE("The minLength facet was not found.")) final lazy val maxLengthValue: java.math.BigDecimal = - getFacetValue(localMaxLengthValue, Facet.maxLength, hasMaxLength) + combinedBaseFacets + .collectFirst { case f: FacetMaxLength => f } + .map(f => new java.math.BigDecimal(f.bigIntValue)) + .getOrElse(SDE("The maxLength facet was not found.")) final lazy val minInclusiveValue: java.math.BigDecimal = - getFacetValue(localMinInclusiveValue, Facet.minInclusive, hasMinInclusive) + combinedBaseFacets + .collectFirst { case f: FacetMinInclusive => f } + .map(_.bigDecimalValue) + .getOrElse(SDE("The minInclusive facet was not found.")) final lazy val maxInclusiveValue: java.math.BigDecimal = - getFacetValue(localMaxInclusiveValue, Facet.maxInclusive, hasMaxInclusive) + combinedBaseFacets + .collectFirst { case f: FacetMaxInclusive => f } + .map(_.bigDecimalValue) + .getOrElse(SDE("The maxInclusive facet was not found.")) final lazy val minExclusiveValue: java.math.BigDecimal = - getFacetValue(localMinExclusiveValue, Facet.minExclusive, hasMinExclusive) + combinedBaseFacets + .collectFirst { case f: FacetMinExclusive => f } + .map(_.bigDecimalValue) + .getOrElse(SDE("The minExclusive facet was not found.")) final lazy val maxExclusiveValue: java.math.BigDecimal = - getFacetValue(localMaxExclusiveValue, Facet.maxExclusive, hasMaxExclusive) + combinedBaseFacets + .collectFirst { case f: FacetMaxExclusive => f } + .map(_.bigDecimalValue) + .getOrElse(SDE("The maxExclusive facet was not found.")) final lazy val totalDigitsValue: java.math.BigDecimal = - getFacetValue(localTotalDigitsValue, Facet.totalDigits, hasTotalDigits) + combinedBaseFacets + .collectFirst { case f: FacetTotalDigits => f } + .map(f => new java.math.BigDecimal(f.bigIntValue)) + .getOrElse(SDE("The totalDigits facet was not found.")) final lazy val fractionDigitsValue: java.math.BigDecimal = - getFacetValue(localFractionDigitsValue, Facet.fractionDigits, hasFractionDigits) - - private def errorOnLocalLessThanBaseFacet( - local: BigInteger, - base: BigInteger, - theFacetType: Facet.Type - ) = { - val res = local.compareTo(base) - if (res < 0) - SDE( - "SimpleTypes: The local %s (%s) was less than the base %s (%s) ", - theFacetType, - local, - theFacetType, - base - ) - } - private def errorOnLocalGreaterThanBaseFacet( - local: BigInteger, - base: BigInteger, - theFacetType: Facet.Type - ) = { - val res = local.compareTo(base) - if (res > 0) - SDE( - "SimpleTypes: The local %s (%s) was greater than the base %s (%s) ", - theFacetType, - local, - theFacetType, - base - ) - } - private def errorOnLocalLessThanBaseFacet( - local: java.math.BigDecimal, - base: java.math.BigDecimal, - theFacetType: Facet.Type - ) = { - val res = local.compareTo(base) - if (res < 0) - SDE( - "SimpleTypes: The local %s (%s) was less than the base %s (%s) ", - theFacetType, - local, - theFacetType, - base - ) - } - private def errorOnLocalGreaterThanBaseFacet( - local: java.math.BigDecimal, - base: java.math.BigDecimal, - theFacetType: Facet.Type - ) = { - val res = local.compareTo(base) - if (res > 0) - SDE( - "SimpleTypes: The local %s (%s) was greater than the base %s (%s) ", - theFacetType, - local, - theFacetType, - base - ) - } - private def errorOnLocalNotEqualToBaseFacet( - local: BigInteger, - base: BigInteger, - theFacetType: Facet.Type - ) = { - val res = local.compareTo(base) - if (res != 0) - SDE( - "SimpleTypes: The local %s (%s) was not equal to the base %s (%s) ", - theFacetType, - local, - theFacetType, - base - ) - } - - private def getRemoteFacetValues(theFacetType: Facet.Type): Seq[FacetValue] = { - val res = remoteBaseFacets.filter { case (f, _) => f == theFacetType } - res - } - - private def getRemoteFacetValue(theFacetType: Facet.Type): String = { - // Filtering works more appropriately here - val res = remoteBaseFacets.filter { case (f, v) => f == theFacetType } - if (res.size > 0) { - val (_, theFacetValue) = res(0) - return theFacetValue - } - "" // Indicates the facet doesn't exist - } - - private def getFacetValue( - theLocalValue: String, - theRemoteValue: String, - theType: Facet.Type, - exists: Boolean - ): java.math.BigDecimal = { - if (!exists) SDE("The facet %s was not found.", theType) - else if (theLocalValue != "" && theRemoteValue != "") { - val resFacet = doNumericFacetNarrowing(theLocalValue, theRemoteValue, theType) - new java.math.BigDecimal(resFacet) - } else if (theLocalValue != "") { - checkValueSpaceFacetRange(theLocalValue, theType) - } else { - checkValueSpaceFacetRange(theRemoteValue, theType) - } - } - - private def getFacetValue( - theLocalValue: String, - theType: Facet.Type, - exists: Boolean - ): java.math.BigDecimal = { - val remoteFacets = getRemoteFacetValues(theType) - if (!exists) SDE("The facet %s was not found.", theType) - else if (theLocalValue != "" && remoteFacets.size > 0) { - val (_, remoteValue) = getRemoteFacetValues(theType)(0) - val resFacet = doNumericFacetNarrowing(theLocalValue, remoteValue, theType) - new java.math.BigDecimal(resFacet) - } else if (theLocalValue != "") { - checkValueSpaceFacetRange(theLocalValue, theType) - } else { - val (_, remoteValue) = remoteFacets(0) - checkValueSpaceFacetRange(remoteValue, theType) - } - } - - private def narrowNonNegativeFacets( - localFacet: String, - remoteFacet: String, - facetType: Facet.Type - ): String = { - val theLocalFacet = new BigInteger(localFacet) - val theRemoteFacet = new BigInteger(remoteFacet) - if (theLocalFacet.signum() != 1) - SDE("The %s facet must be a non-negative integer.", facetType) - facetType match { - case Facet.minLength => { - errorOnLocalLessThanBaseFacet(theLocalFacet, theRemoteFacet, facetType) - localFacet - } - case Facet.length => { - errorOnLocalNotEqualToBaseFacet(theLocalFacet, theRemoteFacet, facetType) - localFacet - } - case Facet.maxLength | Facet.fractionDigits => { - errorOnLocalGreaterThanBaseFacet(theLocalFacet, theRemoteFacet, facetType) - localFacet - } - case _ => { - val errMsg = "narrowNonNegativeFacets is not valid for %s facet".format(facetType) - Assert.usageError(errMsg) - } - } - } - - private def narrowPositiveIntegerFacets( - localFacet: String, - remoteFacet: String, - facetType: Facet.Type - ): String = { - val theLocalFacet = new BigInteger(localFacet) - val theRemoteFacet = new BigInteger(remoteFacet) - if ((theLocalFacet.signum() != 1) || (theLocalFacet.compareTo(BigInteger.ZERO) == 0)) - SDE("The %s facet must be a positive integer.", facetType) - facetType match { - case Facet.totalDigits => { - errorOnLocalGreaterThanBaseFacet(theLocalFacet, theRemoteFacet, facetType) - localFacet - } - case _ => { - val errMsg = "narrowPositiveIntegerFacets is not valid for %s facet".format(facetType) - Assert.usageError(errMsg) - } - } - } - - private def narrowValueSpaceFacets( - localFacet: String, - remoteFacet: String, - facetType: Facet.Type - ) = { - val (theLocalFacet, theRemoteFacet) = - checkValueSpaceFacetRange(localFacet, remoteFacet, facetType) - - // Made it here so range checks were successful - // Now just validate/compare local and base/remote facet - facetType match { - case Facet.minInclusive => { - errorOnLocalLessThanBaseFacet(theLocalFacet, theRemoteFacet, facetType) - } - case Facet.maxInclusive => { - errorOnLocalGreaterThanBaseFacet(theLocalFacet, theRemoteFacet, facetType) - } - case Facet.minExclusive => { - errorOnLocalLessThanBaseFacet(theLocalFacet, theRemoteFacet, facetType) - } - case Facet.maxExclusive => { - errorOnLocalGreaterThanBaseFacet(theLocalFacet, theRemoteFacet, facetType) - } - case _ => { - val errMsg = - "Unrecognized facet type (%s) for narrowing of value-space facets.".format(facetType) - Assert.usageError(errMsg) - } - } - localFacet - } - - private def convertFacetToBigDecimal(facet: String): java.math.BigDecimal = { - self.primType match { - case PrimType.DateTime => - dateToBigDecimal( - facet, - "uuuu-MM-dd'T'HH:mm:ss.SSSSSSxxx", - PrimType.DateTime.toString(), - this - ) - case PrimType.Date => - dateToBigDecimal(facet, "uuuu-MM-ddxxx", PrimType.Date.toString(), this) - case PrimType.Time => - dateToBigDecimal(facet, "HH:mm:ss.SSSSSSxxx", PrimType.Time.toString(), this) - case _ => new java.math.BigDecimal(facet) - } - } - - private def checkValueSpaceFacetRange( - localFacet: String, - facetType: Facet.Type - ): java.math.BigDecimal = { - // Necessary for min/max Inclusive/Exclusive Facets - - // Perform conversions once - val theLocalFacet = convertFacetToBigDecimal(localFacet) - - facetType match { - case Facet.maxExclusive | Facet.maxInclusive | Facet.minExclusive | Facet.minInclusive | - Facet.enumeration => { - // Here we're just doing range checking for the - // specified primitive type - primType match { - case PrimType.Int => { - if (!isFacetInIntRange(theLocalFacet)) { - SDE( - "%s facet value (%s) was found to be outside of Int range.", - facetType, - localFacet - ) - } - } - case PrimType.Byte => { - if (!isFacetInByteRange(theLocalFacet)) { - SDE( - "%s facet value (%s) was found to be outside of Byte range.", - facetType, - localFacet - ) - } - } - case PrimType.Short => { - if (!isFacetInShortRange(theLocalFacet)) { - SDE( - "%s facet value (%s) was found to be outside of Short range.", - facetType, - localFacet - ) - } - } - case PrimType.Long => { - if (!isFacetInLongRange(theLocalFacet)) { - SDE( - "%s facet value (%s) was found to be outside of Long range.", - facetType, - localFacet - ) - } - } - case PrimType.Integer => { - // Unbounded integer - if (!isFacetInIntegerRange(theLocalFacet)) { - SDE( - "%s facet value (%s) was found to be outside of Integer range.", - facetType, - localFacet - ) - } - } - case PrimType.UnsignedInt => { - if (!isFacetInUnsignedIntRange(theLocalFacet)) { - SDE( - "%s facet value (%s) was found to be outside of unsigned int range.", - facetType, - localFacet - ) - } - } - case PrimType.UnsignedByte => { - if (!isFacetInUnsignedByteRange(theLocalFacet)) { - SDE( - "%s facet value (%s) was found to be outside of unsigned byte range.", - facetType, - localFacet - ) - } - } - case PrimType.UnsignedShort => { - if (!isFacetInUnsignedShortRange(theLocalFacet)) { - SDE( - "%s facet value (%s) was found to be outside of unsigned short range.", - facetType, - localFacet - ) - } - } - case PrimType.UnsignedLong => { - if (!isFacetInUnsignedLongRange(theLocalFacet)) { - SDE( - "%s facet value (%s) was found to be outside of unsigned long range.", - facetType, - localFacet - ) - } - } - case PrimType.Double => { - if (!isFacetInDoubleRange(theLocalFacet)) { - SDE( - "%s facet value (%s) was found to be outside of Double range.", - facetType, - localFacet - ) - } - } - case PrimType.Float => { - if (!isFacetInFloatRange(theLocalFacet)) { - SDE( - "%s facet value (%s) was found to be outside of Float range.", - facetType, - localFacet - ) - } - } - case PrimType.NonNegativeInteger => { - // Unsigned Unbounded Integer - if (!isFacetInNonNegativeIntegerRange(theLocalFacet)) { - SDE( - "%s facet value (%s) was found to be outside of NonNegativeInteger range.", - facetType, - localFacet - ) - } - } - case PrimType.Decimal => { /* Nothing to do here */ } - case PrimType.DateTime => { /* Nothing to do here */ } - case PrimType.Date => { /* Nothing to do here */ } - case PrimType.Time => { /* Nothing to do here */ } - case PrimType.Boolean => notYetImplemented("checkValueSpaceFacetRange - Boolean") - case PrimType.HexBinary => { /* Nothing to do here */ } - case PrimType.String => { /* Nothing to do here */ } - case _ => - Assert.usageError( - "checkValueSpaceFacetRange - Unrecognized primitive type: " + primType.name - ) - } - } - case _ => { /* Nothing to do */ } - } - theLocalFacet - } - - private def checkValueSpaceFacetRange( - localFacet: String, - remoteFacet: String, - facetType: Facet.Type - ): (java.math.BigDecimal, java.math.BigDecimal) = { - // Neccessary for min/max Inclusive/Exclusive Facets - - // TODO: I think the performance here can be improved. - // - // Consider storing the remoteBaseFacets as their actual evaluated values - // rather than just as String. This would prevent us from having to perform - // the checkValueSpaceFacetRange on the remoteFacet here as it would've already - // been done in the base. --TRW - - // Perform conversions once - val theRemoteFacet = checkValueSpaceFacetRange(remoteFacet, facetType) - val theLocalFacet = checkValueSpaceFacetRange(localFacet, facetType) - - (theLocalFacet, theRemoteFacet) - } - - private def isFacetInByteRange(facet: java.math.BigDecimal): Boolean = - self.isInByteRange(facet) - - private def isFacetInShortRange(facet: java.math.BigDecimal): Boolean = - self.isInShortRange(facet) - private def isFacetInIntRange(facet: java.math.BigDecimal): Boolean = self.isInIntRange(facet) - private def isFacetInIntegerRange(facet: java.math.BigDecimal): Boolean = - self.isInIntegerRange(facet) - private def isFacetInLongRange(facet: java.math.BigDecimal): Boolean = - self.isInLongRange(facet) - private def isFacetInDoubleRange(facet: java.math.BigDecimal): Boolean = - self.isInDoubleRange(facet) - private def isFacetInFloatRange(facet: java.math.BigDecimal): Boolean = - self.isInFloatRange(facet) - // private def isFacetInDecimalRange(facet: java.math.BigDecimal): Boolean = { - // BigDecimal is unbounded? So nothing outside of its range? - // true - // } - // private def isFacetInNegativeIntegerRange(facet: java.math.BigDecimal): Boolean = self.isInNonNegativeIntegerRange(facet) - private def isFacetInNonNegativeIntegerRange(facet: java.math.BigDecimal): Boolean = - self.isInNonNegativeIntegerRange(facet) - private def isFacetInUnsignedLongRange(facet: java.math.BigDecimal): Boolean = - isInUnsignedLongRange(facet) - - private def isFacetInUnsignedIntRange(facet: java.math.BigDecimal): Boolean = - isInUnsignedIntRange(facet) - - private def isFacetInUnsignedShortRange(facet: java.math.BigDecimal): Boolean = - isInUnsignedShortRange(facet) - - private def isFacetInUnsignedByteRange(facet: java.math.BigDecimal): Boolean = - isInUnsignedByteRange(facet) - - protected def doNumericFacetNarrowing( - localFacet: String, - remoteFacet: String, - facetType: Facet.Type - ) = { - // Assumes both local and remote facets exist - // Only for Numeric facets - // - // Can likely do narrowing checks here - // - // BigInt use compareTo - // a negative number, zero, or a positive number as this BigInteger is numerically less than, - // equal to, or greater than o, which must be a BigInteger. - facetType match { - case Facet.length | Facet.minLength | Facet.maxLength | Facet.fractionDigits => { - // Non-negative Integers. BigInt - narrowNonNegativeFacets(localFacet, remoteFacet, facetType) - } - case Facet.minInclusive | Facet.maxInclusive | Facet.minExclusive | - Facet.maxExclusive => { - // In value-space of base type. BigDecimal? - narrowValueSpaceFacets(localFacet, remoteFacet, facetType) - } - case Facet.totalDigits => { - // Positive Integer (value greater than 0). BigInt - narrowPositiveIntegerFacets(localFacet, remoteFacet, facetType) - } - case _ => - Assert.usageError("Call to 'doNumericFacetNarrowing' only valid for Numeric Facets.") - } - } - - private def getLocalValue(theType: Facet.Type) = { - val res = localBaseFacets.filter { case (f, v) => f == theType } - if (res.length > 0) { - val (_, theFacetValue) = res(0) - theFacetValue - } else "" - } - - protected def getCombinedValue(theType: Facet.Type) = { - val lValue = getLocalValue(theType) - val rValue = getRemoteFacetValue(theType) - val cValue = getFacetValue(lValue, rValue, theType, true) - cValue - } - - protected lazy val getCombinedValueEnum = { - val lValue = getLocalValue(Facet.enumeration) - val rValue = getRemoteFacetValue(Facet.enumeration) - lValue.foreach(e => { - if (rValue.length() > 0 && !rValue.contains(e)) - SDE("Local enumerations must be a subset of base enumerations.") - }) - if (lValue.length() > 0) { lValue } - else { rValue } - } - + combinedBaseFacets + .collectFirst { case f: FacetFractionDigits => f } + .map(f => new java.math.BigDecimal(f.bigIntValue)) + .getOrElse(SDE("The fractionDigits facet was not found.")) } diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/core/dsom/RestrictionUnion.scala b/daffodil-core/src/main/scala/org/apache/daffodil/core/dsom/RestrictionUnion.scala index 14f0025c58..7af4c1f694 100644 --- a/daffodil-core/src/main/scala/org/apache/daffodil/core/dsom/RestrictionUnion.scala +++ b/daffodil-core/src/main/scala/org/apache/daffodil/core/dsom/RestrictionUnion.scala @@ -24,18 +24,11 @@ import scala.xml.Node import org.apache.daffodil.lib.exceptions.Assert import org.apache.daffodil.lib.exceptions.ThrowsSDE -import org.apache.daffodil.lib.exceptions.UnsuppressableException import org.apache.daffodil.lib.xml.QName import org.apache.daffodil.lib.xml.RefQName import org.apache.daffodil.runtime1.dpath.NodeInfo import org.apache.daffodil.runtime1.dpath.NodeInfo.PrimType import org.apache.daffodil.runtime1.dsom.* -import org.apache.daffodil.runtime1.dsom.FacetTypes.ElemFacets -import org.apache.daffodil.runtime1.dsom.FacetTypes.FacetValue - -import com.ibm.icu.text.SimpleDateFormat -import com.ibm.icu.util.GregorianCalendar -import com.ibm.icu.util.TimeZone object Restriction { def apply(xmlArg: Node, simpleTypeDef: SimpleTypeDefBase) = { @@ -48,12 +41,165 @@ object Restriction { /** * A schema component for simple type restrictions */ + final class Restriction private (xmlArg: Node, val simpleTypeDef: SimpleTypeDefBase) extends SchemaComponentImpl(xmlArg, simpleTypeDef) with Facets with NestingLexicalMixin with TypeChecks { + private def checkValueSpaceFacetRange( + localFacet: java.math.BigDecimal, + facetType: Facet.Type + ): Unit = { + // Necessary for min/max Inclusive/Exclusive Facets + facetType match { + case Facet.maxExclusive | Facet.maxInclusive | Facet.minExclusive | Facet.minInclusive | + Facet.enumeration => { + // Here we're just doing range checking for the + // specified primitive type + primType match { + case PrimType.Int => { + if (!isInIntRange(localFacet)) { + SDE( + "%s facet value (%s) was found to be outside of Int range.", + facetType, + localFacet + ) + } + } + case PrimType.Byte => { + if (!isInByteRange(localFacet)) { + SDE( + "%s facet value (%s) was found to be outside of Byte range.", + facetType, + localFacet + ) + } + } + case PrimType.Short => { + if (!isInShortRange(localFacet)) { + SDE( + "%s facet value (%s) was found to be outside of Short range.", + facetType, + localFacet + ) + } + } + case PrimType.Long => { + if (!isInLongRange(localFacet)) { + SDE( + "%s facet value (%s) was found to be outside of Long range.", + facetType, + localFacet + ) + } + } + case PrimType.Integer => { + // Unbounded integer + if (!isInIntegerRange(localFacet)) { + SDE( + "%s facet value (%s) was found to be outside of Integer range.", + facetType, + localFacet + ) + } + } + case PrimType.UnsignedInt => { + if (!isInUnsignedIntRange(localFacet)) { + SDE( + "%s facet value (%s) was found to be outside of unsigned int range.", + facetType, + localFacet + ) + } + } + case PrimType.UnsignedByte => { + if (!isInUnsignedByteRange(localFacet)) { + SDE( + "%s facet value (%s) was found to be outside of unsigned byte range.", + facetType, + localFacet + ) + } + } + case PrimType.UnsignedShort => { + if (!isInUnsignedShortRange(localFacet)) { + SDE( + "%s facet value (%s) was found to be outside of unsigned short range.", + facetType, + localFacet + ) + } + } + case PrimType.UnsignedLong => { + if (!isInUnsignedLongRange(localFacet)) { + SDE( + "%s facet value (%s) was found to be outside of unsigned long range.", + facetType, + localFacet + ) + } + } + case PrimType.Double => { + if (!isInDoubleRange(localFacet)) { + SDE( + "%s facet value (%s) was found to be outside of Double range.", + facetType, + localFacet + ) + } + } + case PrimType.Float => { + if (!isInFloatRange(localFacet)) { + SDE( + "%s facet value (%s) was found to be outside of Float range.", + facetType, + localFacet + ) + } + } + case PrimType.NonNegativeInteger => { + // Unsigned Unbounded Integer + if (!isInNonNegativeIntegerRange(localFacet)) { + SDE( + "%s facet value (%s) was found to be outside of NonNegativeInteger range.", + facetType, + localFacet + ) + } + } + case PrimType.Decimal => { + /* Nothing to do here */ + } + case PrimType.DateTime => { + /* Nothing to do here */ + } + case PrimType.Date => { + /* Nothing to do here */ + } + case PrimType.Time => { + /* Nothing to do here */ + } + case PrimType.Boolean => notYetImplemented("checkValueSpaceFacetRange - Boolean") + case PrimType.HexBinary => { + /* Nothing to do here */ + } + case PrimType.String => { + /* Nothing to do here */ + } + case _ => + Assert.usageError( + "checkValueSpaceFacetRange - Unrecognized primitive type: " + primType.name + ) + } + } + case _ => { + /* Nothing to do */ + } + } + } + protected[dsom] override def initialize() = { super.initialize() optUnion @@ -121,92 +267,139 @@ final class Restriction private (xmlArg: Node, val simpleTypeDef: SimpleTypeDefB } lazy val localBaseFacets: ElemFacets = { - val myFacets: Queue[FacetValue] = Queue.empty // val not var - it's a mutable collection - if (localPatternValue.length > 0) { myFacets.enqueue((Facet.pattern, localPatternValue)) } - if (localLengthValue.length > 0) { - myFacets.enqueue((Facet.length, localLengthValue)) - } - if (localMinLengthValue.length > 0) { - myFacets.enqueue((Facet.minLength, localMinLengthValue)) - } - if (localMaxLengthValue.length > 0) { - myFacets.enqueue((Facet.maxLength, localMaxLengthValue)) + // val not var - it's a mutable collection + val myFacets: Queue[FacetOrdered] = Queue.empty[FacetOrdered] + + if (localPatternValue.nonEmpty) + myFacets.enqueue(new FacetPattern(localPatternValue)) + if (localLengthValue.nonEmpty) + myFacets.enqueue(new FacetLength(localLengthValue, this)) + if (localMinLengthValue.nonEmpty) + myFacets.enqueue(new FacetMinLength(localMinLengthValue, this)) + if (localMaxLengthValue.nonEmpty) + myFacets.enqueue(new FacetMaxLength(localMaxLengthValue, this)) + if (localMinInclusiveValue.nonEmpty) { + val facet = new FacetMinInclusive(localMinInclusiveValue, primType, this) + checkValueSpaceFacetRange(facet.bigDecimalValue, Facet.minInclusive) + myFacets.enqueue(facet) } - if (localMinInclusiveValue.length > 0) { - myFacets.enqueue((Facet.minInclusive, localMinInclusiveValue)) + if (localMaxInclusiveValue.nonEmpty) { + val facet = new FacetMaxInclusive(localMaxInclusiveValue, primType, this) + checkValueSpaceFacetRange(facet.bigDecimalValue, Facet.maxInclusive) + myFacets.enqueue(facet) } - if (localMaxInclusiveValue.length > 0) { - myFacets.enqueue((Facet.maxInclusive, localMaxInclusiveValue)) + if (localMinExclusiveValue.nonEmpty) { + val facet = new FacetMinExclusive(localMinExclusiveValue, primType, this) + checkValueSpaceFacetRange(facet.bigDecimalValue, Facet.minExclusive) + myFacets.enqueue(facet) } - if (localMinExclusiveValue.length > 0) { - myFacets.enqueue((Facet.minExclusive, localMinExclusiveValue)) - } - if (localMaxExclusiveValue.length > 0) { - myFacets.enqueue((Facet.maxExclusive, localMaxExclusiveValue)) - } - if (localTotalDigitsValue.length > 0) { - myFacets.enqueue((Facet.totalDigits, localTotalDigitsValue)) - } - if (localFractionDigitsValue.length > 0) { - myFacets.enqueue((Facet.fractionDigits, localFractionDigitsValue)) - } - if (localEnumerationValue.length > 0) { - myFacets.enqueue((Facet.enumeration, localEnumerationValue)) + if (localMaxExclusiveValue.nonEmpty) { + val facet = new FacetMaxExclusive(localMaxExclusiveValue, primType, this) + checkValueSpaceFacetRange(facet.bigDecimalValue, Facet.maxExclusive) + myFacets.enqueue(facet) } + if (localTotalDigitsValue.nonEmpty) + myFacets.enqueue(new FacetTotalDigits(localTotalDigitsValue, this)) + if (localFractionDigitsValue.nonEmpty) + myFacets.enqueue(new FacetFractionDigits(localFractionDigitsValue, this)) + if (localEnumerationValue.nonEmpty) + myFacets.enqueue(new FacetEnumeration(localEnumerationValue)) - val res: ElemFacets = myFacets.toSeq - res + myFacets.toSeq } - final lazy val combinedBaseFacets: Seq[FacetValue] = { - val combined: Queue[FacetValue] = Queue.empty + final lazy val combinedBaseFacets: ElemFacets = { + val combined: Queue[FacetOrdered] = Queue.empty + // enumeration - string based, combine local and remote if (hasEnumeration) { - val enumVal = getCombinedValueEnum - combined.enqueue((Facet.enumeration, enumVal)) + val lValue = localBaseFacets.collectFirst { case f: FacetEnumeration => f } + val rValue = remoteBaseFacets.collectFirst { case f: FacetEnumeration => f } + val result = (lValue, rValue) match { + case (Some(l), Some(r)) => + // local enumerations must be subset of base enumerations + l.xmlValue.foreach(e => { + if (r.xmlValue.nonEmpty && !r.xmlValue.contains(e)) + SDE("Local enumerations must be a subset of base enumerations.") + }) + l // local wins if it exists + case (Some(l), None) => l + case (None, Some(r)) => r + case (None, None) => Assert.impossibleCase + } + combined.enqueue(result) } + // Patterns within a type are OR'd, collect all + // per http://www.xfront.com/XML-Schema-library/papers/Algorithm-for-Merging-a-simpleType-Dependency-Chain.pdf if (hasPattern) { - val lPattern = localBaseFacets.filter { case (f, v) => f == Facet.pattern } - val rPattern = remoteBaseFacets.filter { case (f, v) => f == Facet.pattern } - val cPattern = lPattern ++: rPattern - cPattern.foreach(x => combined.enqueue(x)) + val lPattern = localBaseFacets.collect { case f: FacetPattern => f } + val rPattern = remoteBaseFacets.collect { case f: FacetPattern => f } + (lPattern ++: rPattern).foreach(combined.enqueue) } if (hasLength) { - val cValue = getCombinedValue(Facet.length) - combined.enqueue((Facet.length, cValue.toString())) + val facets = + localBaseFacets.collect { case f: FacetLength => f } ++ + remoteBaseFacets.collect { case f: FacetLength => f } + val narrowed = facets.reduce[FacetOrdered]((l, r) => l.narrow(r)) + combined.enqueue(narrowed) } if (hasMinLength) { - val cValue = getCombinedValue(Facet.minLength) - combined.enqueue((Facet.minLength, cValue.toString())) + val facets = + localBaseFacets.collect { case f: FacetMinLength => f } ++ + remoteBaseFacets.collect { case f: FacetMinLength => f } + val narrowed = facets.reduce[FacetOrdered]((l, r) => l.narrow(r)) + combined.enqueue(narrowed) } if (hasMaxLength) { - val cValue = getCombinedValue(Facet.maxLength) - combined.enqueue((Facet.maxLength, cValue.toString())) - } - if (hasMaxInclusive) { - val cValue = getCombinedValue(Facet.maxInclusive) - combined.enqueue((Facet.maxInclusive, cValue.toString())) - } - if (hasMaxExclusive) { - val cValue = getCombinedValue(Facet.maxExclusive) - combined.enqueue((Facet.maxExclusive, cValue.toString())) + val facets = + localBaseFacets.collect { case f: FacetMaxLength => f } ++ + remoteBaseFacets.collect { case f: FacetMaxLength => f } + val narrowed = facets.reduce[FacetOrdered]((l, r) => l.narrow(r)) + combined.enqueue(narrowed) } if (hasMinInclusive) { - val cValue = getCombinedValue(Facet.minInclusive) - combined.enqueue((Facet.minInclusive, cValue.toString())) + val facets = + localBaseFacets.collect { case f: FacetMinInclusive => f } ++ + remoteBaseFacets.collect { case f: FacetMinInclusive => f } + val narrowed = facets.reduce[FacetOrdered]((l, r) => l.narrow(r)) + combined.enqueue(narrowed) + } + if (hasMaxInclusive) { + val facets = + localBaseFacets.collect { case f: FacetMaxInclusive => f } ++ + remoteBaseFacets.collect { case f: FacetMaxInclusive => f } + val narrowed = facets.reduce[FacetOrdered]((l, r) => l.narrow(r)) + combined.enqueue(narrowed) } if (hasMinExclusive) { - val cValue = getCombinedValue(Facet.minExclusive) - combined.enqueue((Facet.minExclusive, cValue.toString())) + val facets = + localBaseFacets.collect { case f: FacetMinExclusive => f } ++ + remoteBaseFacets.collect { case f: FacetMinExclusive => f } + val narrowed = facets.reduce[FacetOrdered]((l, r) => l.narrow(r)) + combined.enqueue(narrowed) + } + if (hasMaxExclusive) { + val facets = + localBaseFacets.collect { case f: FacetMaxExclusive => f } ++ + remoteBaseFacets.collect { case f: FacetMaxExclusive => f } + val narrowed = facets.reduce[FacetOrdered]((l, r) => l.narrow(r)) + combined.enqueue(narrowed) } if (hasTotalDigits) { - val cValue = getCombinedValue(Facet.totalDigits) - combined.enqueue((Facet.totalDigits, cValue.toString())) + val facets = + localBaseFacets.collect { case f: FacetTotalDigits => f } ++ + remoteBaseFacets.collect { case f: FacetTotalDigits => f } + val narrowed = facets.reduce[FacetOrdered]((l, r) => l.narrow(r)) + combined.enqueue(narrowed) } if (hasFractionDigits) { - val cValue = getCombinedValue(Facet.fractionDigits) - combined.enqueue((Facet.fractionDigits, cValue.toString())) + val facets = + localBaseFacets.collect { case f: FacetFractionDigits => f } ++ + remoteBaseFacets.collect { case f: FacetFractionDigits => f } + val narrowed = facets.reduce[FacetOrdered]((l, r) => l.narrow(r)) + combined.enqueue(narrowed) } + combined.toSeq } @@ -306,145 +499,6 @@ final class Union private (val xmlArg: Node, simpleTypeDef: SimpleTypeDefBase) } sealed trait TypeChecks { self: Restriction => - protected def dateToBigDecimal( - date: String, - format: String, - dateType: String, - context: ThrowsSDE - ): JBigDecimal = { - val df = new SimpleDateFormat(format) - df.setCalendar(new GregorianCalendar()) - df.setTimeZone(TimeZone.GMT_ZONE) - val bd = - try { - val dt = df.parse(date) - new JBigDecimal(dt.getTime()) - } catch { - case s: scala.util.control.ControlThrowable => throw s - case u: UnsuppressableException => throw u - case e1: Exception => { - try { - // Could already be a BigDecimal - new JBigDecimal(date) - } catch { - case s: scala.util.control.ControlThrowable => throw s - case u: UnsuppressableException => throw u - case e2: Exception => - context.SDE( - "Failed to parse (%s) to %s (%s) due to %s (after %s).", - date, - dateType, - format, - e2.getMessage(), - e1.getMessage() - ) - } - } - } - bd - } - - private def convertStringToBigDecimal( - value: String, - primType: PrimType, - context: ThrowsSDE - ): JBigDecimal = { - primType match { - case PrimType.DateTime => - dateToBigDecimal( - value, - "uuuu-MM-dd'T'HH:mm:ss.SSSSSSxxx", - PrimType.DateTime.toString(), - context - ) - case PrimType.Date => - dateToBigDecimal(value, "uuuu-MM-ddxxx", PrimType.Date.toString(), context) - case PrimType.Time => - dateToBigDecimal(value, "HH:mm:ss.SSSSSSxxx", PrimType.Time.toString(), context) - case _ => new JBigDecimal(value) - } - } - - def checkRangeReturnsValue( - value: String, - primType: PrimType, - theContext: ThrowsSDE - ): (Boolean, Option[JBigDecimal]) = { - // EmptyString is only valid for hexBinary and String - if ((value == null | value.length() == 0)) { - return primType match { - case PrimType.HexBinary | PrimType.String => (true, None) - case _ => (false, None) - } - } - - // Don't need to range check String or HexBinary or blobs. - // no point attempting a conversion to BigDecimal so - // return early here. - primType match { - case PrimType.String | PrimType.HexBinary | PrimType.AnyURI => return (true, None) - case _ => /* Continue on below */ - } - - // Check Boolean, and the Numeric types. - (value.toLowerCase(), primType) match { - case ("true", PrimType.Boolean) => (true, Some(JBigDecimal.ONE)) - case ("false", PrimType.Boolean) => (true, Some(JBigDecimal.ZERO)) - case (x, PrimType.Boolean) => - theContext.SDE("%s is not a valid Boolean value. Expected 'true' or 'false'.", x) - case (_, _) => { - // Perform conversions once - val theValue = convertStringToBigDecimal(value, primType, theContext) - - // Here we're just doing range checking for the - // specified primitive type - val res: Boolean = primType match { - case PrimType.Int => isInIntRange(theValue) - case PrimType.Byte => isInByteRange(theValue) - case PrimType.Short => isInShortRange(theValue) - case PrimType.Long => isInLongRange(theValue) - case PrimType.Integer => true // Unbounded Integer - case PrimType.UnsignedInt => isInUnsignedIntRange(theValue) - case PrimType.UnsignedByte => isInUnsignedByteRange(theValue) - case PrimType.UnsignedShort => isInUnsignedShortRange(theValue) - case PrimType.UnsignedLong => isInUnsignedLongRange(theValue) - case PrimType.Double => isInDoubleRange(theValue) - case PrimType.Float => isInFloatRange(theValue) - case PrimType.DateTime => true - case PrimType.Date => true - case PrimType.Time => true - case PrimType.Boolean => Assert.impossibleCase // Handled earlier, shouldn't get here - case PrimType.Decimal => true // Unbounded Decimal - case PrimType.HexBinary => - Assert.impossibleCase // Handled earlier, shouldn't get here - case PrimType.String => Assert.impossibleCase // Handled earlier, shouldn't get here - case PrimType.AnyURI => Assert.impossibleCase // Handled earlier, shouldn't get here - case PrimType.NonNegativeInteger => isInNonNegativeIntegerRange(theValue) - } - val isValueWhole = { - val IsWholeRegex = """^[^.]*(\.0*)?$""".r - value match { - case IsWholeRegex(_) => true - case _ => false - } - } - primType match { - case PrimType.Int | PrimType.Byte | PrimType.Short | PrimType.Long | - PrimType.Integer | PrimType.UnsignedInt | PrimType.UnsignedByte | - PrimType.UnsignedShort | PrimType.UnsignedLong => - if (!isValueWhole) - theContext.SDE("checkRange - Value (%s) must be a whole number.", value) - case _ => // OK - } - (res, Some(theValue)) - } - } - } - - def checkRange(value: String, primType: PrimType, theContext: ThrowsSDE): Boolean = { - val (boolResult, _) = checkRangeReturnsValue(value, primType, theContext) - boolResult - } protected def isNumInRange(num: JBigDecimal, min: JBigDecimal, max: JBigDecimal): Boolean = { val checkMin = num.compareTo(min) diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/core/grammar/primitives/PrimitivesDateTime.scala b/daffodil-core/src/main/scala/org/apache/daffodil/core/grammar/primitives/PrimitivesDateTime.scala index af70bdd3c0..46d2d6885f 100644 --- a/daffodil-core/src/main/scala/org/apache/daffodil/core/grammar/primitives/PrimitivesDateTime.scala +++ b/daffodil-core/src/main/scala/org/apache/daffodil/core/grammar/primitives/PrimitivesDateTime.scala @@ -17,7 +17,6 @@ package org.apache.daffodil.core.grammar.primitives -import java.text.ParsePosition import scala.Boolean import org.apache.daffodil.core.dsom.ElementBase @@ -29,6 +28,8 @@ import org.apache.daffodil.lib.schema.annotation.props.gen.CalendarCheckPolicy import org.apache.daffodil.lib.schema.annotation.props.gen.CalendarFirstDayOfWeek import org.apache.daffodil.lib.schema.annotation.props.gen.CalendarPatternKind import org.apache.daffodil.lib.schema.annotation.props.gen.Representation +import org.apache.daffodil.runtime1.dpath.InvalidPrimitiveDataException +import org.apache.daffodil.runtime1.dpath.NodeInfo import org.apache.daffodil.runtime1.processors.CalendarEv import org.apache.daffodil.runtime1.processors.CalendarLanguageEv import org.apache.daffodil.runtime1.processors.DateTimeFormatterEv @@ -37,10 +38,8 @@ import org.apache.daffodil.runtime1.processors.parsers.ConvertTextCalendarParser import org.apache.daffodil.unparsers.runtime1.ConvertBinaryCalendarSecMilliUnparser import org.apache.daffodil.unparsers.runtime1.ConvertTextCalendarUnparser -import com.ibm.icu.text.SimpleDateFormat import com.ibm.icu.util.Calendar import com.ibm.icu.util.TimeZone -import com.ibm.icu.util.ULocale abstract class ConvertCalendarPrimBase(e: ElementBase, guard: Boolean) extends Terminal(e, guard) { @@ -54,7 +53,6 @@ abstract class ConvertCalendarPrimBase(e: ElementBase, guard: Boolean) abstract class ConvertTextCalendarPrimBase(e: ElementBase, guard: Boolean) extends ConvertCalendarPrimBase(e, guard) { - protected def infosetPattern: String protected def implicitPattern: String protected def validFormatCharacters: Seq[Char] @@ -241,8 +239,7 @@ abstract class ConvertTextCalendarPrimBase(e: ElementBase, guard: Boolean) case class ConvertTextDatePrim(e: ElementBase) extends ConvertTextCalendarPrimBase(e, true) { protected override val xsdType = "date" protected override val prettyType = "Date" - protected override val infosetPattern = "uuuu-MM-ddxxx" - protected override val implicitPattern = "uuuu-MM-dd" + protected override val implicitPattern = "yyyy-MM-dd" protected override val validFormatCharacters = if (e.representation == Representation.Binary) { "dDeFMuwWyY".toSeq @@ -254,7 +251,6 @@ case class ConvertTextDatePrim(e: ElementBase) extends ConvertTextCalendarPrimBa case class ConvertTextTimePrim(e: ElementBase) extends ConvertTextCalendarPrimBase(e, true) { protected override val xsdType = "time" protected override val prettyType = "Time" - protected override val infosetPattern = "HH:mm:ss.SSSSSSxxx" protected override val implicitPattern = "HH:mm:ssZ" protected override val validFormatCharacters = if (e.representation == Representation.Binary) { @@ -268,8 +264,7 @@ case class ConvertTextDateTimePrim(e: ElementBase) extends ConvertTextCalendarPrimBase(e, true) { protected override val xsdType = "dateTime" protected override val prettyType = "DateTime" - protected override val infosetPattern = "uuuu-MM-dd'T'HH:mm:ss.SSSSSSxxx" - protected override val implicitPattern = "uuuu-MM-dd'T'HH:mm:ss" + protected override val implicitPattern = "yyyy-MM-dd'T'HH:mm:ss" protected override val validFormatCharacters = if (e.representation == Representation.Binary) { "dDeFhHkKmMsSuwWyY".toSeq @@ -284,36 +279,18 @@ case class ConvertBinaryDateTimeSecMilliPrim(e: ElementBase, lengthInBits: Long) protected override val prettyType = "DateTime" lazy val epochCalendar: Calendar = { - val cal = Calendar.getInstance - cal.clear() - cal.setLenient(false) - - val sdfWithTZ = new SimpleDateFormat("uuuu-MM-dd'T'HH:mm:ssZZZZ", ULocale.ENGLISH) - var pos = new ParsePosition(0) - sdfWithTZ.parse(e.binaryCalendarEpoch, cal, pos) - - if (pos.getIndex != e.binaryCalendarEpoch.length || pos.getErrorIndex >= 0) { - // binaryCalendarEpoch didn't match the first format with timezone, so try without - cal.clear() - pos = new ParsePosition(0) - val sdf = new SimpleDateFormat("uuuu-MM-dd'T'HH:mm:ss", ULocale.ENGLISH) - cal.setTimeZone(TimeZone.UNKNOWN_ZONE) - sdf.parse(e.binaryCalendarEpoch, cal, pos) - - if (pos.getIndex != e.binaryCalendarEpoch.length || pos.getErrorIndex >= 0) { - SDE( - "Failed to parse binaryCalendarEpoch - Format must match the pattern 'uuuu-MM-dd'T'HH:mm:ss' or 'uuuu-MM-dd'T'HH:mm:ssZZZZ'" - ) - } - } - - try { - cal.getTime - } catch { - case e: IllegalArgumentException => { - SDE("Failed to parse binaryCalendarEpoch: %s.", e.getMessage()) + val dfdlDateTime = + try { + NodeInfo.DateTime.fromXMLString(e.binaryCalendarEpoch).getCalendar + } catch { + case exception: InvalidPrimitiveDataException => + SDE( + "Failed to parse binaryCalendarEpoch '%s' as an xs:dateTime.", + e.binaryCalendarEpoch, + exception.getMessage + ) } - } + val cal = dfdlDateTime.calendar cal } diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/lib/calendar/DFDLCalendar.scala b/daffodil-core/src/main/scala/org/apache/daffodil/lib/calendar/DFDLCalendar.scala index 2dd24555e6..1f7493da71 100644 --- a/daffodil-core/src/main/scala/org/apache/daffodil/lib/calendar/DFDLCalendar.scala +++ b/daffodil-core/src/main/scala/org/apache/daffodil/lib/calendar/DFDLCalendar.scala @@ -22,6 +22,7 @@ import java.math.BigDecimal as JBigDecimal import org.apache.daffodil.lib.exceptions.Assert import com.ibm.icu.util.Calendar +import com.ibm.icu.util.GregorianCalendar import com.ibm.icu.util.TimeZone object DFDLCalendarOrder extends Enumeration { @@ -29,7 +30,8 @@ object DFDLCalendarOrder extends Enumeration { val P_LESS_THAN_Q, P_GREATER_THAN_Q, P_EQUAL_Q, P_NOT_EQUAL_Q = Value val fieldsForComparison = Array( - Calendar.EXTENDED_YEAR, + Calendar.ERA, + Calendar.YEAR, Calendar.MONTH, Calendar.DAY_OF_MONTH, Calendar.HOUR_OF_DAY, @@ -242,7 +244,7 @@ case class DFDLTime(calendar: Calendar, override val hasTimeZone: Boolean) // values and achieve the same effect, but optimized // timeCal.clear(Calendar.YEAR) - timeCal.clear(Calendar.EXTENDED_YEAR) + timeCal.clear(Calendar.ERA) timeCal.clear(Calendar.MONTH) timeCal.clear(Calendar.DAY_OF_MONTH) DFDLDateTime(timeCal, hasTimeZone) @@ -400,5 +402,16 @@ abstract class DFDLCalendar extends OrderedCalendar with Serializable { def hasTimeZone: Boolean + /** + * Returns the XSD 1.0 signed year of a calendar. YEAR is the positive + * year-of-era; a BCE year is returned negated. + * + * @return signed year (e.g. 1 BCE -> -1, 1 CE -> 1) + */ + final def signedYear: Int = { + val y = calendar.get(Calendar.YEAR) + if (calendar.get(Calendar.ERA) == GregorianCalendar.BC) -y else y + } + final def toJBigDecimal: JBigDecimal = new JBigDecimal(calendar.getTimeInMillis()) } diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/lib/calendar/DFDLCalendarConversion.scala b/daffodil-core/src/main/scala/org/apache/daffodil/lib/calendar/DFDLCalendarConversion.scala index d03de5bdd7..e5181084b4 100644 --- a/daffodil-core/src/main/scala/org/apache/daffodil/lib/calendar/DFDLCalendarConversion.scala +++ b/daffodil-core/src/main/scala/org/apache/daffodil/lib/calendar/DFDLCalendarConversion.scala @@ -20,6 +20,7 @@ package org.apache.daffodil.lib.calendar import java.lang.Integer import com.ibm.icu.util.Calendar +import com.ibm.icu.util.GregorianCalendar import com.ibm.icu.util.SimpleTimeZone import com.ibm.icu.util.TimeZone @@ -51,7 +52,7 @@ object DFDLCalendarConversion { def datePartToXMLString(dfdlcal: DFDLCalendar): String = { val calendar = dfdlcal.calendar - val y = calendar.get(Calendar.EXTENDED_YEAR) + val y = dfdlcal.signedYear val m = calendar.get(Calendar.MONTH) + 1 val d = calendar.get(Calendar.DAY_OF_MONTH) @@ -61,14 +62,16 @@ object DFDLCalendarConversion { } /** - * Parses a string that begins with the pattern "uuuu-MM-dd" and sets the + * Parses a string that begins with the pattern "yyyy-MM-dd" and sets the * appropriate values in the calendar. The year part may be 1 or more digits - * (including an optional sign) and must be a valid postive or negative - * integer. The month and day parts must be zero padded digits. + * (including an optional leading minus sign for BCE) and must be a valid + * integer in XSD 1.0 numbering (no year zero; -0001 is 1 BCE). A negative + * year is stored as ERA=BC with a positive YEAR. The month and day parts must + * be zero padded digits. * * If the pattern is not followed, an IllegalArgumentException is thrown. * - * @return if the date part was succesfully parsed, returns a substring of + * @return if the date part was successfully parsed, returns a substring of * the remaining characters */ def datePartFromXMLString(string: String, calendar: Calendar): String = { @@ -95,7 +98,22 @@ object DFDLCalendarConversion { val d = string.substring(endYear + 4, endYear + 6) try { - calendar.set(Calendar.EXTENDED_YEAR, Integer.parseInt(y)) + // Use YEAR + ERA rather than EXTENDED_YEAR to match XSD 1.0 numbering: + // a negative lexical year is BCE, stored as ERA=BC with positive YEAR. + val yInt = Integer.parseInt(y) + + // calendar.getTimeInMillis() in fromXMLString would eventually catch year 0 + // and call invalidCalendar anyway — explicitly rejecting it here is a bit more + // efficient as it skips all the calendar building steps and fails immediately. + // In addition, we do not have to rely on downstream validation. + if (yInt == 0) invalidValue + else if (yInt < 0) { + calendar.set(Calendar.ERA, GregorianCalendar.BC) + calendar.set(Calendar.YEAR, -yInt) + } else { + calendar.set(Calendar.ERA, GregorianCalendar.AD) + calendar.set(Calendar.YEAR, yInt) + } calendar.set(Calendar.MONTH, Integer.parseInt(m) - 1) calendar.set(Calendar.DAY_OF_MONTH, Integer.parseInt(d)) } catch { @@ -298,12 +316,17 @@ object DFDLDateTimeConversion extends DFDLCalendarConversion { /** * Supported patterns: - * uuuu-MM-dd'T'HH:mm:ss.SSSSSSxxxxx - * uuuu-MM-dd'T'HH:mm:ss.SSSSSS - * uuuu-MM-dd'T'HH:mm:ssxxxxx - * uuuu-MM-dd'T'HH:mm:ss - * uuuu-MM-ddxxxxx - * uuuu-MM-dd + * [-]yyyy-MM-dd'T'HH:mm:ss.SSSSSSxxxxx + * [-]yyyy-MM-dd'T'HH:mm:ss.SSSSSS + * [-]yyyy-MM-dd'T'HH:mm:ssxxxxx + * [-]yyyy-MM-dd'T'HH:mm:ss + * [-]yyyy-MM-ddxxxxx + * [-]yyyy-MM-dd + * + * Negative (BCE) years are also supported: the year may carry a leading minus + * (e.g. -0001 = 1 BCE). These are XSD 1.0 lexical strings parsed directly, not + * dfdl:calendarPattern tokens, so the minus is part of the lexical year rather + * than what the yyyy-MM-dd calendar pattern would generally imply. */ def fromXMLString(string: String): DFDLDateTime = { val calendar = emptyCalendar.clone().asInstanceOf[Calendar] @@ -344,8 +367,8 @@ object DFDLDateConversion extends DFDLCalendarConversion { /** * Supported patterns: - * uuuu-MM-ddxxxxx - * uuuu-MM-dd + * yyyy-MM-ddxxxxx + * yyyy-MM-dd */ def fromXMLString(string: String): DFDLDate = { val calendar = emptyCalendar.clone().asInstanceOf[Calendar] diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/dpath/FNFunctions.scala b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/dpath/FNFunctions.scala index a040c2677d..8088718f54 100644 --- a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/dpath/FNFunctions.scala +++ b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/dpath/FNFunctions.scala @@ -983,7 +983,18 @@ abstract class FNFromTime(recipe: CompiledDPath, argType: NodeInfo.Kind) case class FNYearFromDateTime(recipe: CompiledDPath, argType: NodeInfo.Kind) extends FNFromDateTime(recipe, argType) { val fieldName = "year" - val field = Calendar.EXTENDED_YEAR + val field = Calendar.YEAR + + override def computeValue(a: DataValuePrimitive, dstate: DState): DataValueNumber = { + a.getAnyRef match { + case dt: DFDLDateTime => + JBigInt.valueOf(dt.signedYear) + case _ => + throw new NumberFormatException( + "fn:" + fieldName + "-from-dateTime only accepts xs:dateTime." + ) + } + } } case class FNMonthFromDateTime(recipe: CompiledDPath, argType: NodeInfo.Kind) extends FNFromDateTime(recipe, argType) { @@ -1052,7 +1063,16 @@ case class FNSecondsFromDateTime(recipe: CompiledDPath, argType: NodeInfo.Kind) case class FNYearFromDate(recipe: CompiledDPath, argType: NodeInfo.Kind) extends FNFromDate(recipe, argType) { val fieldName = "year" - val field = Calendar.EXTENDED_YEAR + val field = Calendar.YEAR + + override def computeValue(a: DataValuePrimitive, dstate: DState): DataValueNumber = { + a.getAnyRef match { + case d: DFDLDate => + JBigInt.valueOf(d.signedYear) + case _ => + throw new NumberFormatException("fn:" + fieldName + "-from-date only accepts xs:date.") + } + } } case class FNMonthFromDate(recipe: CompiledDPath, argType: NodeInfo.Kind) extends FNFromDate(recipe, argType) { diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/dsom/Facets1.scala b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/dsom/Facets1.scala index 15bf73f7dd..819b829809 100644 --- a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/dsom/Facets1.scala +++ b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/dsom/Facets1.scala @@ -16,12 +16,19 @@ */ package org.apache.daffodil.runtime1.dsom + +import java.math.BigDecimal as JBigDecimal +import java.math.BigInteger as JBigInt import scala.util.matching.Regex +import org.apache.daffodil.lib.exceptions.ThrowsSDE import org.apache.daffodil.lib.util.Enum +import org.apache.daffodil.lib.xml.XMLUtils +import org.apache.daffodil.runtime1.dpath.InvalidPrimitiveDataException +import org.apache.daffodil.runtime1.dpath.NodeInfo.PrimType object Facet extends Enum { - sealed abstract trait Type extends EnumValueType + sealed trait Type extends EnumValueType case object enumeration extends Type case object fractionDigits extends Type case object length extends Type @@ -36,11 +43,278 @@ object Facet extends Enum { case object whiteSpace extends Type } -object FacetTypes { - type Values = String - type ValuesR = Regex - type FacetValue = (Facet.Type, Values) - type FacetValueR = (Facet.Type, ValuesR) - type ElemFacets = Seq[FacetValue] - type ElemFacetsR = Seq[FacetValueR] +/** + * Base sealed trait for compiled XSD facet constraints. + * + * Each subclass represents a specific XSD facet, carrying both the original + * XML string value and, where applicable, a converted representation used for + * runtime validation. + * + * Previously facets were stored as `(Facet.Type, String)` tuples, which + * are serializable by default. Typed facet objects now require an explicit + * Serializable declaration otherwise a java.io.NotSerializableException + * will be thrown during serialization. + */ +sealed trait FacetOrdered extends Serializable { + def xmlValue: String + + /** + * Validates this local facet against the remote base facet. + * Throws SDE if this facet does not correctly narrow the base. + * Returns this facet if valid. + * + * The default implementation is a no-op for facets where narrowing does not apply + * (for example, pattern and enumeration). + */ + def narrow(remote: FacetOrdered): FacetOrdered = this +} + +type ElemFacets = Seq[FacetOrdered] + +/** + * Base abstract class for integer facets. Parses [[xmlValue]] to [[JBigInt]] + * once, validating sign via [[isNonNegative]] — non-negative facets reject + * negative values, positive facets reject zero and negative values. + */ +abstract class FacetInteger(val xmlValue: String, val context: ThrowsSDE) extends FacetOrdered { + def facetName: String + def isNonNegative: Boolean + + lazy val bigIntValue: JBigInt = { + val localFacet = + try { new JBigInt(xmlValue) } + catch { + case e: IllegalArgumentException => + context.SDE("invalid %s facet restriction: %s", facetName, e.getMessage) + } + if (isNonNegative && localFacet.signum() == -1) + context.SDE("The %s facet must be a non-negative integer.", facetName) + else if (!isNonNegative && localFacet.signum() != 1) + context.SDE("The %s facet must be a positive integer.", facetName) + localFacet + } +} + +/** + * Base abstract class for non-negative integer facets (length, minLength, + * maxLength, fractionDigits). Uses [[BigInteger]] for comparison. + */ +abstract class FacetNonNegativeInteger(xmlValue: String, context: ThrowsSDE) + extends FacetInteger(xmlValue, context) { + final val isNonNegative = true + + protected def isInvalidNarrowing(remoteValue: JBigInt): Boolean + protected def narrowingRelation: String + + override final def narrow(remote: FacetOrdered): FacetOrdered = { + val remoteFacet = remote.asInstanceOf[FacetNonNegativeInteger].bigIntValue + if (isInvalidNarrowing(remoteFacet)) + context.SDE( + "SimpleTypes: The local %s (%s) was %s the base %s (%s)", + facetName, + bigIntValue, + narrowingRelation, + facetName, + remoteFacet + ) + this + } +} + +/** + * Base abstract class for value-space facets (minInclusive, maxInclusive, + * minExclusive, maxExclusive). Uses [[JBigDecimal]] for comparison. + * + * Range validation against the primitive type's value space is + * performed separately prior to storage in localBaseFacets. + */ +abstract class FacetValueSpace( + val xmlValue: String, + val primType: PrimType, + val context: ThrowsSDE +) extends FacetOrdered { + + def facetName: String + protected def isInvalidNarrowing(remoteValue: JBigDecimal): Boolean + protected def narrowingRelation: String + + lazy val bigDecimalValue: JBigDecimal = { + try { + primType match { + case PrimType.DateTime | PrimType.Date | PrimType.Time => + try { + primType.fromXMLString(xmlValue).getCalendar.toJBigDecimal + } catch { + case e: InvalidPrimitiveDataException => + context.SDE( + "Failed to parse %s facet value (%s) to %s.", + facetName, + xmlValue, + primType.toString() + ) + } + case _ => new JBigDecimal(xmlValue) + } + } catch { + case e: IllegalArgumentException => + context.SDE("invalid %s facet restriction: %s", facetName, e.getMessage) + } + } + + override final def narrow(remote: FacetOrdered): FacetOrdered = { + val remoteFacet = remote.asInstanceOf[FacetValueSpace].bigDecimalValue + if (isInvalidNarrowing(remoteFacet)) + context.SDE( + "SimpleTypes: The local %s (%s) was %s the base %s (%s)", + facetName, + bigDecimalValue, + narrowingRelation, + facetName, + remoteFacet + ) + this + } +} + +/** + * Facet for xs:totalDigits. Narrowing requires local <= base. + * Inlines positive integer validation — value must be strictly + * greater than zero. + */ +final class FacetTotalDigits(xmlValue: String, context: ThrowsSDE) + extends FacetInteger(xmlValue, context) { + val facetName = "totalDigits" + val isNonNegative = false + + override def narrow(remote: FacetOrdered): FacetOrdered = { + val remoteFacet = remote.asInstanceOf[FacetTotalDigits].bigIntValue + if (bigIntValue.compareTo(remoteFacet) > 0) + context.SDE( + "SimpleTypes: The local %s (%s) was greater than the base %s (%s)", + facetName, + bigIntValue, + facetName, + remoteFacet + ) + this + } +} + +/** Facet for xs:enumeration. */ +final class FacetEnumeration(val xmlValue: String) extends FacetOrdered + +/** + * Facet for xs:pattern. + * + * The DFDL Infoset can contain strings which hold characters + * that are not allowed in XML at all. + * + * In order to talk about these characters in a XSD pattern facet + * we use a remapping of such characters into the Unicode + * Private Use Area, so as to have XML-legal characters. + * + * See the section titled "XML Illegal Characters" on this web page: + * https://daffodil.apache.org/infoset/ + * + * Before processing a regex of these characters in Daffodil's pattern facet + * validation, we must remap these PUA characters back to the originally + * intended code points, since that's what the Infoset strings will contain. + * + * Consider the character code 0xB. This is illegal in XML v1.0 documents. + * A DFDL Schema is an XML Schema, which is an XML document; hence, we cannot + * use the character with code 0xB directly, nor can we use an XML numeric + * character entity like for it. The character is simply disallowed in + * XML, including DFDL schemas. Hence, we mention 0xB by using a remapping of it + * to the PUA area character 0xE00B, which we express by  + * Hence a pattern regex like "[ 0-9a-zA-Z]" will match + * the character with char code 0xB (remapped from E00B), as well as spaces, + * and alphanumeric characters. + * + * The XSD numeric character entity  can be used to match ASCII NUL + * (char code 0). + * + * This remapping is for pattern facets, which are inside a DFDL schema, + * and so will not contain CR characters, since XML reading will convert those + * to LF. To discuss CR in this pattern we can't use ` ` syntax because that + * turns into a CR which gets turned into a LF. Plus the pattern value is + * an XML attribute, the value of which gets its whitespace collapsed, all + * line-ending chars converted to spaces, and adjacent spaces collapsed to one. + * + * So a pattern facet must use `\r` and '\n' to describe line-endings within the pattern. + * And in general one must be careful about whitespace. + */ +final class FacetPattern(val xmlValue: String) extends FacetOrdered { + lazy val regex: Regex = XMLUtils.remapPUAToXMLIllegalCharacters(xmlValue).r +} + +/** Facet for xs:length. Narrowing requires local == base exactly. */ +final class FacetLength(xmlValue: String, context: ThrowsSDE) + extends FacetNonNegativeInteger(xmlValue, context) { + val facetName = "length" + protected def isInvalidNarrowing(remote: JBigInt): Boolean = + bigIntValue.compareTo(remote) != 0 + protected def narrowingRelation = "not equal to" +} + +/** Facet for xs:minLength. Narrowing requires local >= base. */ +final class FacetMinLength(xmlValue: String, context: ThrowsSDE) + extends FacetNonNegativeInteger(xmlValue, context) { + val facetName = "minLength" + protected def isInvalidNarrowing(remote: JBigInt): Boolean = + bigIntValue.compareTo(remote) < 0 + protected def narrowingRelation = "less than" +} + +/** Facet for xs:maxLength. Narrowing requires local <= base. */ +final class FacetMaxLength(xmlValue: String, context: ThrowsSDE) + extends FacetNonNegativeInteger(xmlValue, context) { + val facetName = "maxLength" + protected def isInvalidNarrowing(remote: JBigInt): Boolean = + bigIntValue.compareTo(remote) > 0 + protected def narrowingRelation = "greater than" +} + +/** Facet for xs:fractionDigits. Narrowing requires local <= base. */ +final class FacetFractionDigits(xmlValue: String, context: ThrowsSDE) + extends FacetNonNegativeInteger(xmlValue, context) { + val facetName = "fractionDigits" + protected def isInvalidNarrowing(remote: JBigInt): Boolean = + bigIntValue.compareTo(remote) > 0 + protected def narrowingRelation = "greater than" +} + +/** Facet for xs:minInclusive. Narrowing requires local >= base. */ +final class FacetMinInclusive(xmlValue: String, primType: PrimType, context: ThrowsSDE) + extends FacetValueSpace(xmlValue, primType, context) { + val facetName = "minInclusive" + protected def isInvalidNarrowing(remote: JBigDecimal): Boolean = + bigDecimalValue.compareTo(remote) < 0 + protected def narrowingRelation = "less than" +} + +/** Facet for xs:maxInclusive. Narrowing requires local <= base. */ +final class FacetMaxInclusive(xmlValue: String, primType: PrimType, context: ThrowsSDE) + extends FacetValueSpace(xmlValue, primType, context) { + val facetName = "maxInclusive" + protected def isInvalidNarrowing(remote: JBigDecimal): Boolean = + bigDecimalValue.compareTo(remote) > 0 + protected def narrowingRelation = "greater than" +} + +/** Facet for xs:minExclusive. Narrowing requires local >= base. */ +final class FacetMinExclusive(xmlValue: String, primType: PrimType, context: ThrowsSDE) + extends FacetValueSpace(xmlValue, primType, context) { + val facetName = "minExclusive" + protected def isInvalidNarrowing(remote: JBigDecimal): Boolean = + bigDecimalValue.compareTo(remote) < 0 + protected def narrowingRelation = "less than" +} + +/** Facet for xs:maxExclusive. Narrowing requires local <= base. */ +final class FacetMaxExclusive(xmlValue: String, primType: PrimType, context: ThrowsSDE) + extends FacetValueSpace(xmlValue, primType, context) { + val facetName = "maxExclusive" + protected def isInvalidNarrowing(remote: JBigDecimal): Boolean = + bigDecimalValue.compareTo(remote) > 0 + protected def narrowingRelation = "greater than" } diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/RuntimeData.scala b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/RuntimeData.scala index 49b0d0b1a6..d130a84fe7 100644 --- a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/RuntimeData.scala +++ b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/processors/RuntimeData.scala @@ -21,7 +21,6 @@ import java.lang.Double as JDouble import java.lang.Float as JFloat import java.lang.Long as JLong import java.util.regex.Matcher -import scala.util.matching.Regex import scala.xml.NamespaceBinding import org.apache.daffodil.api.DFDLPrimType @@ -64,7 +63,7 @@ import org.apache.daffodil.runtime1.dpath.NodeInfo.PrimType import org.apache.daffodil.runtime1.dsom.CompiledExpression import org.apache.daffodil.runtime1.dsom.DPathCompileInfo import org.apache.daffodil.runtime1.dsom.DPathElementCompileInfo -import org.apache.daffodil.runtime1.dsom.FacetTypes +import org.apache.daffodil.runtime1.dsom.FacetPattern import org.apache.daffodil.runtime1.dsom.ImplementsThrowsSDE import org.apache.daffodil.runtime1.infoset.DISimple import org.apache.daffodil.runtime1.infoset.DataValue @@ -233,7 +232,7 @@ final class SimpleTypeRuntimeData( noPrefixNamespaceArg: NS, val primType: NodeInfo.PrimType, val noFacetChecks: Boolean, - val patternValues: Seq[FacetTypes.FacetValueR], + val patternValues: Seq[FacetPattern], val enumerationValues: Option[String], val length: Option[java.math.BigDecimal], val minLength: Option[java.math.BigDecimal], @@ -266,8 +265,10 @@ final class SimpleTypeRuntimeData( */ private lazy val matcherPool = new ThreadSafePool[(Seq[Matcher], Option[Matcher])] { protected final override def allocate() = { - val patternMatchers = patternValues.map { case (_, r) => r.pattern.matcher("") } - val optEnumMatcher = enumerationValues.map { en => en.r.pattern.matcher("") } + val patternMatchers = patternValues.map { p => p.regex.pattern.matcher("") } + val optEnumMatcher = enumerationValues.map { enumeration => + enumeration.r.pattern.matcher("") + } (patternMatchers, optEnumMatcher) } } @@ -342,7 +343,7 @@ final class SimpleTypeRuntimeData( if (!check) { // The escaping is important here as error messages were impossible to figure out when control chars were involved. val patternStrings = e.patternValues - .map { case (_, r: Regex) => XMLUtils.escape(r.pattern.pattern()) } + .map { pattern => XMLUtils.escape(pattern.regex.pattern.pattern()) } .mkString(",") return Error("facet pattern(s): %s".format(patternStrings)) } diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/unparsers/runtime1/ConvertTextCalendarUnparser.scala b/daffodil-core/src/main/scala/org/apache/daffodil/unparsers/runtime1/ConvertTextCalendarUnparser.scala index b570a6eb28..64664173b7 100644 --- a/daffodil-core/src/main/scala/org/apache/daffodil/unparsers/runtime1/ConvertTextCalendarUnparser.scala +++ b/daffodil-core/src/main/scala/org/apache/daffodil/unparsers/runtime1/ConvertTextCalendarUnparser.scala @@ -93,13 +93,14 @@ case class ConvertTextCalendarUnparser( // that. This ensures there are no locale specific issues related to // unparsing calendars. calendar.set( - infosetCalendar.get(Calendar.EXTENDED_YEAR), + infosetCalendar.get(Calendar.YEAR), infosetCalendar.get(Calendar.MONTH), infosetCalendar.get(Calendar.DAY_OF_MONTH), infosetCalendar.get(Calendar.HOUR_OF_DAY), infosetCalendar.get(Calendar.MINUTE), infosetCalendar.get(Calendar.SECOND) ) + calendar.set(Calendar.ERA, infosetCalendar.get(Calendar.ERA)) calendar.set(Calendar.MILLISECOND, infosetCalendar.get(Calendar.MILLISECOND)) calendar.setTimeZone(infosetCalendar.getTimeZone) diff --git a/daffodil-core/src/test/scala/org/apache/daffodil/core/dsom/TestDsomCompiler.scala b/daffodil-core/src/test/scala/org/apache/daffodil/core/dsom/TestDsomCompiler.scala index fb139ebfd7..ffc162958c 100644 --- a/daffodil-core/src/test/scala/org/apache/daffodil/core/dsom/TestDsomCompiler.scala +++ b/daffodil-core/src/test/scala/org/apache/daffodil/core/dsom/TestDsomCompiler.scala @@ -1170,7 +1170,7 @@ class TestDsomCompiler { val decl = declf.asRoot assertEquals(1, decl.patternValues.length) - val (_, pattern) = decl.patternValues(0) + val pattern = decl.patternValues.head.regex assertEquals("1|2|3", pattern.toString()) } @@ -1210,9 +1210,9 @@ class TestDsomCompiler { val decl = declf.asRoot assertEquals(3, decl.patternValues.length) - val (_, st1) = decl.patternValues(0) - val (_, st2) = decl.patternValues(1) - val (_, st3) = decl.patternValues(2) + val st1 = decl.patternValues.head.regex + val st2 = decl.patternValues(1).regex + val st3 = decl.patternValues(2).regex assertEquals("1|2|3", st1.toString()) assertEquals("4|5|6", st2.toString()) diff --git a/daffodil-tdml-processor/src/test/scala/org/apache/daffodil/processor/tdml/TestTDMLRunner.scala b/daffodil-tdml-processor/src/test/scala/org/apache/daffodil/processor/tdml/TestTDMLRunner.scala index 59f9af9abc..796bbdb7e9 100644 --- a/daffodil-tdml-processor/src/test/scala/org/apache/daffodil/processor/tdml/TestTDMLRunner.scala +++ b/daffodil-tdml-processor/src/test/scala/org/apache/daffodil/processor/tdml/TestTDMLRunner.scala @@ -1077,4 +1077,49 @@ f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff assertTrue(msg.contains("/this/does/not/exist.xml")) } + // Verifies that an expected infoset containing year 0000 (which is invalid + // in XSD 1.0 — no year zero exists) causes a TDMLException when the TDML + // runner tries to compare it against the actual parsed result using + // XMLGregorianCalendar. This is expected behavior — an invalid expected + // infoset value is a TDML test error, not a parse error. + @Test def testYearZeroThrows(): Unit = { + val inputValueCalc = "{ fn:year-from-dateTime(../ex:date) }" + val testSuite = + + + + + + + + + + + + + + + 0001-01-01T23:00:00 BC + + + + 0000-01-01T23:00:00 + -1 + + + + + + val runner = new Runner(testSuite) + val e = intercept[TDMLException] { + runner.runOneTest("yearZeroThrows") + } + runner.reset() + assertTrue(e.getMessage().contains("0000-01-01T23:00:00")) + assertTrue(e.getMessage().contains("not a valid representation")) + assertTrue(e.getMessage().contains("XML Gregorian Calendar")) + } } diff --git a/daffodil-test/src/test/resources/org/apache/daffodil/section05/simple_types/SimpleTypes.tdml b/daffodil-test/src/test/resources/org/apache/daffodil/section05/simple_types/SimpleTypes.tdml index 61808e7ece..984139a8ae 100644 --- a/daffodil-test/src/test/resources/org/apache/daffodil/section05/simple_types/SimpleTypes.tdml +++ b/daffodil-test/src/test/resources/org/apache/daffodil/section05/simple_types/SimpleTypes.tdml @@ -2652,7 +2652,7 @@ 00000000 00000000 00000000 00111101 - Schema Definition Error: Failed to parse binaryCalendarEpoch - Format must match the pattern 'uuuu-MM-dd'T'HH:mm:ss' or 'uuuu-MM-dd'T'HH:mm:ssZZZZ' + Schema Definition Error: Failed to parse binaryCalendarEpoch '2000-1-1T00:00' as an xs:dateTime. @@ -2663,7 +2663,7 @@ 00000000 00000000 00000000 00111101 - Schema Definition Error: Failed to parse binaryCalendarEpoch: DAY_OF_MONTH=2000, valid range=1..31 + Schema Definition Error: Failed to parse binaryCalendarEpoch '01-01-2000T00:00:00' as an xs:dateTime. @@ -5673,7 +5673,7 @@ - -2012-02-13 + -2013-02-13 diff --git a/daffodil-test/src/test/resources/org/apache/daffodil/section23/dfdl_functions/Functions.tdml b/daffodil-test/src/test/resources/org/apache/daffodil/section23/dfdl_functions/Functions.tdml index 77db13fb1e..1bc329e027 100644 --- a/daffodil-test/src/test/resources/org/apache/daffodil/section23/dfdl_functions/Functions.tdml +++ b/daffodil-test/src/test/resources/org/apache/daffodil/section23/dfdl_functions/Functions.tdml @@ -707,6 +707,24 @@ + + + + + + + + + + + + + + + + @@ -802,8 +820,30 @@ - + + + + + + + + + + + + + + + + @@ -824,6 +864,15 @@ + + + + + + + + @@ -857,7 +906,6 @@ - @@ -870,6 +918,18 @@ + + + + + + + + + + + @@ -2257,24 +2317,76 @@ - + 0001-01-01T23:00:00 BC + + + + -0001-01-01T23:00:00 + -1 + + + + + + + + + 0000-01-01T23:00:00 - - 0000-01-01T23:00:00 - 0 - + + -0001-01-01T23:00:00 + -1 + + + + + + + + 0000-01-01T23:00:00 + 0 + + + + + Unparse error + 0000-01-01T23:00:00 + not a valid xs:dateTime + + + + + + + -1233-01-30T10:01:02-08:00 + + + + -1234-01-30T10:01:02-08:00 + -1234 + 1 + 30 + 10 + 1 + 2 + -08:00 + + + + + - - 0000-01-01 + 0001-01-01 BC - - 0000-01-01 - 0 - + + -0001-01-01 + -1 + @@ -2793,24 +2935,24 @@ - - -0020-01-99 + 0020-01-99 BC - - -0020-04-08 + + -0020-04-09 -20 4 - 8 + 9 - + diff --git a/daffodil-test/src/test/scala/org/apache/daffodil/section23/dfdl_expressions/TestDFDLExpressions.scala b/daffodil-test/src/test/scala/org/apache/daffodil/section23/dfdl_expressions/TestDFDLExpressions.scala index dead3d8de0..820ece07a5 100644 --- a/daffodil-test/src/test/scala/org/apache/daffodil/section23/dfdl_expressions/TestDFDLExpressions.scala +++ b/daffodil-test/src/test/scala/org/apache/daffodil/section23/dfdl_expressions/TestDFDLExpressions.scala @@ -766,8 +766,9 @@ class TestDFDLFunctions extends TdmlTests { @Test def yearfromdatetime_01 = test @Test def yearfromdatetime_02 = test - // DAFFODIL-3084 - @Ignore @Test def yearfromdatetime_03 = test + @Test def yearfromdatetime_03 = test + @Test def yearfromdatetime_04 = test + @Test def yearfromdatetime_05 = test @Test def monthfromdatetime_01 = test @Test def monthfromdatetime_02 = test @Test def dayfromdatetime_01 = test @@ -789,11 +790,11 @@ class TestDFDLFunctions extends TdmlTests { @Test def xfromdatetime_02 = test @Test def xfromdatetime_03 = test @Test def xfromdatetime_04 = test + @Test def xfromdatetime_05 = test @Test def yearfromdate_01 = test @Test def yearfromdate_02 = test - // DAFFODIL-3084 - @Ignore @Test def yearfromdate_03 = test + @Test def yearfromdate_03 = test @Test def monthfromdate_01 = test @Test def monthfromdate_02 = test @Test def dayfromdate_01 = test