Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion src/main/java/org/apache/xmlbeans/XmlOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,8 @@ public enum XmlOptionsKeys {
XPATH_USE_SAXON,
XPATH_USE_XMLBEANS,
ATTRIBUTE_VALIDATION_COMPAT_MODE,
LOAD_STRICT_FLOATING_POINT
LOAD_STRICT_FLOATING_POINT,
LOAD_ALLOW_DECIMAL_EXPONENT
}


Expand Down Expand Up @@ -1207,6 +1208,45 @@ public boolean isLoadStrictFloatingPoint() {
return hasOption(XmlOptionsKeys.LOAD_STRICT_FLOATING_POINT);
}

/**
* If this option is set, xsd:decimal values are allowed to use scientific/exponent
* notation (e.g. {@code 1E5}) when parsing. That form is outside the xsd:decimal
* lexical space - it belongs to xsd:double/xsd:float - and {@link java.math.BigDecimal}
* would otherwise parse it to a wrong value ({@code 1E5 -> 100000}). The default is to
* disallow it: an exponent in a decimal is reported as invalid. Such values can also be
* expensive to parse when the exponent is very large. Set this only to restore the
* long-standing lenient behaviour.
*
* @return this
* @since 5.4.0
*/
public XmlOptions setLoadAllowDecimalExponent() {
return setLoadAllowDecimalExponent(true);
}

/**
* Sets whether xsd:decimal values may use scientific/exponent notation when parsing.
* See {@link #setLoadAllowDecimalExponent()}.
*
* @param b {@code true} to accept an exponent in a decimal lexical value
* @return this
* @since 5.4.0
*/
public XmlOptions setLoadAllowDecimalExponent(boolean b) {
return set(XmlOptionsKeys.LOAD_ALLOW_DECIMAL_EXPONENT, b);
}

/**
* Returns whether xsd:decimal values may use scientific/exponent notation when parsing.
* See {@link #setLoadAllowDecimalExponent()}.
*
* @return {@code true} if an exponent is accepted in a decimal lexical value
* @since 5.4.0
*/
public boolean isLoadAllowDecimalExponent() {
return hasOption(XmlOptionsKeys.LOAD_ALLOW_DECIMAL_EXPONENT);
}

/**
* Instructs the validator to skip elements matching an {@code <any>}
* particle with contentModel="lax". This is useful because,
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/apache/xmlbeans/impl/common/XmlLocale.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,9 @@ public interface XmlLocale
// the xsd:float/xsd:double space (hex floats, the java "Infinity" token and
// the f/F/d/D suffix). Driven by XmlOptions.setLoadStrictFloatingPoint.
default boolean isLoadStrictFloatingPoint ( ) { return false; }

// whether xsd:decimal lexing should accept scientific/exponent notation
// (e.g. "1E5"), which is outside the xsd:decimal lexical space. Defaults to
// false (reject). Driven by XmlOptions.setLoadAllowDecimalExponent.
default boolean isLoadAllowDecimalExponent ( ) { return false; }
}
8 changes: 8 additions & 0 deletions src/main/java/org/apache/xmlbeans/impl/store/Locale.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ private Locale(SchemaTypeLoader stl, XmlOptions options) {

_loadStrictFloatingPoint = options.isLoadStrictFloatingPoint();

_loadAllowDecimalExponent = options.isLoadAllowDecimalExponent();

//
// Check for Saaj implementation request
//
Expand Down Expand Up @@ -2077,6 +2079,10 @@ public boolean isLoadStrictFloatingPoint() {
return _loadStrictFloatingPoint;
}

public boolean isLoadAllowDecimalExponent() {
return _loadAllowDecimalExponent;
}

static boolean isWhiteSpace(String s) {
int l = s.length();

Expand Down Expand Up @@ -2797,6 +2803,8 @@ public QName getQName(char[] uriSrc, int uriPos, int uriCch,

boolean _loadStrictFloatingPoint;

boolean _loadAllowDecimalExponent;

int _posTemp;

nthCache _nthCache_A = new nthCache();
Expand Down
37 changes: 37 additions & 0 deletions src/main/java/org/apache/xmlbeans/impl/util/XsTypeConverter.java
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,31 @@ public static String printDouble(double value) {

// ======================== decimal ========================
public static BigDecimal lexDecimal(CharSequence cs)
throws NumberFormatException {
return lexDecimal(cs, false);
}

/**
* Parses an xsd:decimal lexical value.
*
* @param cs the lexical value
* @param allowExponent when {@code false} (the default) scientific/exponent notation
* such as {@code 1E5} is rejected: it is outside the xsd:decimal
* lexical space (that form belongs to xsd:double/xsd:float) and
* {@link BigDecimal} would otherwise parse it to a wrong value
* ({@code 1E5 -> 100000}). When {@code true} the long-standing
* lenient behaviour applies and an exponent is accepted. Driven by
* {@link org.apache.xmlbeans.XmlOptions#setLoadAllowDecimalExponent()}.
* @return the parsed decimal
* @throws NumberFormatException if the value is not a valid xsd:decimal
* @since 5.4.0
*/
public static BigDecimal lexDecimal(CharSequence cs, boolean allowExponent)
throws NumberFormatException {
rejectInvalidNumber(cs);
if (!allowExponent) {
rejectExponent(cs);
}
final String v = cs.toString();

//TODO: review this
Expand Down Expand Up @@ -754,4 +777,18 @@ private static void rejectInvalidNumber(CharSequence cs) {
throw new NumberFormatException("For input string: \"" + cs + "\"");
}
}

// BigDecimal accepts scientific notation such as "1E5", but the xsd:decimal
// lexical space does not allow an exponent - that form belongs to xsd:double
// and xsd:float. Without this check an exponent value reaching lexDecimal via
// the rich parser parses to a wrong value (e.g. "1E5" -> 100000) instead of
// being reported as invalid.
private static void rejectExponent(CharSequence cs) {
for (int i = 0, len = cs.length(); i < len; i++) {
final char c = cs.charAt(i);
if (c == 'e' || c == 'E') {
throw new NumberFormatException("invalid char '" + c + "' in decimal value");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ protected String compute_text(NamespaceManager nsm) {
}

protected void set_text(String s) {
boolean allowExponent = has_store() && get_store().get_locale().isLoadAllowDecimalExponent();
if (_validateOnSet()) {
validateLexical(s, _voorVc);
validateLexical(s, _voorVc, allowExponent);
}

try {
Expand All @@ -61,6 +62,21 @@ protected void set_nil() {
*/

public static void validateLexical(String v, ValidationContext context) {
validateLexical(v, context, false);
}

public static void validateLexical(String v, ValidationContext context, boolean allowExponent) {
if (allowExponent) {
// long-standing lenient behaviour: accept whatever BigDecimal accepts,
// which includes scientific/exponent notation such as "1E5".
try {
new BigDecimal(v);
} catch (NumberFormatException e) {
context.invalid(XmlErrorCodes.DECIMAL, new Object[]{v});
}
return;
}

// TODO - will want to validate Chars with built in white space handling
// However, this fcn sometimes takes a value with wsr applied
// already
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ public JavaDecimalHolderEx(SchemaType type, boolean complex) {

protected void set_text(String s) {
if (_validateOnSet()) {
validateLexical(s, _schemaType, _voorVc);
boolean allowExponent = has_store() && get_store().get_locale().isLoadAllowDecimalExponent();
validateLexical(s, _schemaType, _voorVc, allowExponent);
}

BigDecimal v = null;
Expand All @@ -63,7 +64,11 @@ protected void set_BigDecimal(BigDecimal v) {
}

public static void validateLexical(String v, SchemaType sType, ValidationContext context) {
JavaDecimalHolder.validateLexical(v, context);
validateLexical(v, sType, context, false);
}

public static void validateLexical(String v, SchemaType sType, ValidationContext context, boolean allowExponent) {
JavaDecimalHolder.validateLexical(v, context, allowExponent);

// check pattern
if (sType.hasPatternFacet()) {
Expand Down Expand Up @@ -189,7 +194,8 @@ public static void validateValue(BigDecimal v, SchemaType sType, ValidationConte
}

protected void validate_simpleval(String lexical, ValidationContext ctx) {
validateLexical(lexical, schemaType(), ctx);
boolean allowExponent = has_store() && get_store().get_locale().isLoadAllowDecimalExponent();
validateLexical(lexical, schemaType(), ctx, allowExponent);
validateValue(getBigDecimalValue(), schemaType(), ctx);
}

Expand Down
10 changes: 10 additions & 0 deletions src/test/java/misc/checkin/XmlOptionsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ void testLoadStrictFloatingPointFlag() {
assertFalse(xmlOptions.isLoadStrictFloatingPoint());
}

@Test
void testLoadAllowDecimalExponentFlag() {
XmlOptions xmlOptions = new XmlOptions();
assertFalse(xmlOptions.isLoadAllowDecimalExponent());
xmlOptions.setLoadAllowDecimalExponent();
assertTrue(xmlOptions.isLoadAllowDecimalExponent());
xmlOptions.setLoadAllowDecimalExponent(false);
assertFalse(xmlOptions.isLoadAllowDecimalExponent());
}

@Test
void testSaveNoAttributeWhitespaceEscapeFlag() {
XmlOptions xmlOptions = new XmlOptions();
Expand Down
46 changes: 46 additions & 0 deletions src/test/java/misc/checkin/XsTypeConverterTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/
package misc.checkin;

import org.apache.xmlbeans.XmlDecimal;
import org.apache.xmlbeans.XmlDouble;
import org.apache.xmlbeans.XmlFloat;
import org.apache.xmlbeans.XmlOptions;
Expand Down Expand Up @@ -197,6 +198,20 @@ void loadStrictFloatingPointOptionGatesDoubleParsing() throws Exception {
XmlDouble.Factory.parse("<xml-fragment>0x1p4</xml-fragment>", strict).getDoubleValue());
}

@Test
void loadAllowDecimalExponentOptionGatesDecimalParsing() throws Exception {
// an exponent is outside the xsd:decimal lexical space, so validation
// rejects "1E5" by default
XmlOptions validate = new XmlOptions().setValidateOnSet();
assertThrows(XmlValueOutOfRangeException.class, () ->
XmlDecimal.Factory.parse("<xml-fragment>1E5</xml-fragment>", validate).getBigDecimalValue());

// setLoadAllowDecimalExponent restores the lenient behaviour
XmlOptions lenient = new XmlOptions().setValidateOnSet().setLoadAllowDecimalExponent();
assertEquals(0, new java.math.BigDecimal("100000").compareTo(
XmlDecimal.Factory.parse("<xml-fragment>1E5</xml-fragment>", lenient).getBigDecimalValue()));
}

@Test
void lexLongRejectsDoubleSign() {
// trimInitialPlus drops the leading '+', then Long.parseLong accepts its
Expand Down Expand Up @@ -235,4 +250,35 @@ void lexDecimalTrimsTrailingZeros() {
assertEquals(0, new java.math.BigDecimal("1.5").compareTo(XsTypeConverter.lexDecimal("1.500")));
assertEquals(0, new java.math.BigDecimal("12").compareTo(XsTypeConverter.lexDecimal("12.000")));
}

@Test
void lexDecimalRejectsExponent() {
// BigDecimal accepts scientific notation, but the xsd:decimal lexical
// space has no exponent - "1E5" used to parse to 100000 instead of
// being rejected as invalid. This is the default (allowExponent = false).
assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDecimal("1E5"));
assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDecimal("1.5e3"));
assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDecimal("-2E-3"));
assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDecimal("1E5", false));
// plain decimals stay valid
assertEquals(0, new java.math.BigDecimal("1.5").compareTo(XsTypeConverter.lexDecimal("1.5")));
}

@Test
void lexDecimalAllowsExponentWhenRequested() {
// XmlOptions.setLoadAllowDecimalExponent restores the lenient behaviour:
// the exponent form is accepted again (e.g. "1E5" -> 100000).
assertEquals(0, new java.math.BigDecimal("100000").compareTo(XsTypeConverter.lexDecimal("1E5", true)));
assertEquals(0, new java.math.BigDecimal("1500").compareTo(XsTypeConverter.lexDecimal("1.5e3", true)));
// plain decimals are unaffected by the flag
assertEquals(0, new java.math.BigDecimal("1.5").compareTo(XsTypeConverter.lexDecimal("1.5", true)));
}

@Test
void lexIntegerRejectsExponent() {
// xs:integer is parsed with BigInteger, which never accepts an exponent,
// so the exponent form is already rejected for integer types.
assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexInteger("1E5"));
assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexInteger("1e5"));
}
}