/* * Copyright Aduna (http://www.aduna-software.com/) (c) 1997-2007. * * Licensed under the Aduna BSD-style license. */ package org.openrdf.model.datatypes; import java.math.BigDecimal; import java.math.BigInteger; import java.util.StringTokenizer; import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeConstants; import javax.xml.datatype.DatatypeFactory; import javax.xml.datatype.XMLGregorianCalendar; import javax.xml.namespace.QName; import org.openrdf.model.URI; import org.openrdf.model.vocabulary.XMLSchema; /** * Provides methods for handling the standard XML Schema datatypes. * * @author Arjohn Kampman */ public class XMLDatatypeUtil { private static DatatypeFactory dtFactory; static { try { dtFactory = DatatypeFactory.newInstance(); } catch (DatatypeConfigurationException e) { throw new RuntimeException(e); } } /*-------------------* * Datatype checking * *-------------------*/ /** * Checks whether the supplied datatype is a primitive XML Schema datatype. */ public static boolean isPrimitiveDatatype(URI datatype) { return datatype.equals(XMLSchema.DURATION) || datatype.equals(XMLSchema.DATETIME) || datatype.equals(XMLSchema.TIME) || datatype.equals(XMLSchema.DATE) || datatype.equals(XMLSchema.GYEARMONTH) || datatype.equals(XMLSchema.GYEAR) || datatype.equals(XMLSchema.GMONTHDAY) || datatype.equals(XMLSchema.GDAY) || datatype.equals(XMLSchema.GMONTH) || datatype.equals(XMLSchema.STRING) || datatype.equals(XMLSchema.BOOLEAN) || datatype.equals(XMLSchema.BASE64BINARY) || datatype.equals(XMLSchema.HEXBINARY) || datatype.equals(XMLSchema.FLOAT) || datatype.equals(XMLSchema.DECIMAL) || datatype.equals(XMLSchema.DOUBLE) || datatype.equals(XMLSchema.ANYURI) || datatype.equals(XMLSchema.QNAME) || datatype.equals(XMLSchema.NOTATION); } /** * Checks whether the supplied datatype is a derived XML Schema datatype. */ public static boolean isDerivedDatatype(URI datatype) { return datatype.equals(XMLSchema.NORMALIZEDSTRING) || datatype.equals(XMLSchema.TOKEN) || datatype.equals(XMLSchema.LANGUAGE) || datatype.equals(XMLSchema.NMTOKEN) || datatype.equals(XMLSchema.NMTOKENS) || datatype.equals(XMLSchema.NAME) || datatype.equals(XMLSchema.NCNAME) || datatype.equals(XMLSchema.ID) || datatype.equals(XMLSchema.IDREF) || datatype.equals(XMLSchema.IDREFS) || datatype.equals(XMLSchema.ENTITY) || datatype.equals(XMLSchema.ENTITIES) || datatype.equals(XMLSchema.INTEGER) || datatype.equals(XMLSchema.LONG) || datatype.equals(XMLSchema.INT) || datatype.equals(XMLSchema.SHORT) || datatype.equals(XMLSchema.BYTE) || datatype.equals(XMLSchema.NON_POSITIVE_INTEGER) || datatype.equals(XMLSchema.NEGATIVE_INTEGER) || datatype.equals(XMLSchema.NON_NEGATIVE_INTEGER) || datatype.equals(XMLSchema.POSITIVE_INTEGER) || datatype.equals(XMLSchema.UNSIGNED_LONG) || datatype.equals(XMLSchema.UNSIGNED_INT) || datatype.equals(XMLSchema.UNSIGNED_SHORT) || datatype.equals(XMLSchema.UNSIGNED_BYTE); } /** * Checks whether the supplied datatype is a built-in XML Schema datatype. */ public static boolean isBuiltInDatatype(URI datatype) { return isPrimitiveDatatype(datatype) || isDerivedDatatype(datatype); } /** * Checks whether the supplied datatype is a numeric datatype, i.e. if it is * equal to xsd:float, xsd:double, xsd:decimal or one of the datatypes * derived from xsd:decimal. */ public static boolean isNumericDatatype(URI datatype) { return isDecimalDatatype(datatype) || isFloatingPointDatatype(datatype); } /** * Checks whether the supplied datatype is equal to xsd:decimal or one of the * built-in datatypes that is derived from xsd:decimal. */ public static boolean isDecimalDatatype(URI datatype) { return datatype.equals(XMLSchema.DECIMAL) || isIntegerDatatype(datatype); } /** * Checks whether the supplied datatype is equal to xsd:integer or one of the * built-in datatypes that is derived from xsd:integer. */ public static boolean isIntegerDatatype(URI datatype) { return datatype.equals(XMLSchema.INTEGER) || datatype.equals(XMLSchema.LONG) || datatype.equals(XMLSchema.INT) || datatype.equals(XMLSchema.SHORT) || datatype.equals(XMLSchema.BYTE) || datatype.equals(XMLSchema.NON_POSITIVE_INTEGER) || datatype.equals(XMLSchema.NEGATIVE_INTEGER) || datatype.equals(XMLSchema.NON_NEGATIVE_INTEGER) || datatype.equals(XMLSchema.POSITIVE_INTEGER) || datatype.equals(XMLSchema.UNSIGNED_LONG) || datatype.equals(XMLSchema.UNSIGNED_INT) || datatype.equals(XMLSchema.UNSIGNED_SHORT) || datatype.equals(XMLSchema.UNSIGNED_BYTE); } /** * Checks whether the supplied datatype is equal to xsd:float or xsd:double. */ public static boolean isFloatingPointDatatype(URI datatype) { return datatype.equals(XMLSchema.FLOAT) || datatype.equals(XMLSchema.DOUBLE); } /** * Checks whether the supplied datatype is equal to xsd:dateTime, xsd:date, * xsd:time, xsd:gYearMonth, xsd:gMonthDay, xsd:gYear, xsd:gMonth or * xsd:gDay. These are the primitive datatypes that represent dates and/or * times. * * @see XMLGregorianCalendar */ public static boolean isCalendarDatatype(URI datatype) { return datatype.equals(XMLSchema.DATETIME) || datatype.equals(XMLSchema.DATE)|| datatype.equals(XMLSchema.TIME) || datatype.equals(XMLSchema.GYEARMONTH) || datatype.equals(XMLSchema.GMONTHDAY) || datatype.equals(XMLSchema.GYEAR) || datatype.equals(XMLSchema.GMONTH) || datatype.equals(XMLSchema.GDAY); } /** * Checks whether the supplied datatype is ordered. The values of an ordered * datatype can be compared to eachother using operators like <tt><</tt> * and <tt>></tt>. */ public static boolean isOrderedDatatype(URI datatype) { return isNumericDatatype(datatype) || isCalendarDatatype(datatype); } /*----------------* * Value checking * *----------------*/ public static boolean isValidValue(String value, URI datatype) { boolean result = true; if (datatype.equals(XMLSchema.DECIMAL)) { result = isValidDecimal(value); } else if (datatype.equals(XMLSchema.INTEGER)) { result = isValidInteger(value); } else if (datatype.equals(XMLSchema.NEGATIVE_INTEGER)) { result = isValidNegativeInteger(value); } else if (datatype.equals(XMLSchema.NON_POSITIVE_INTEGER)) { result = isValidNonPositiveInteger(value); } else if (datatype.equals(XMLSchema.NON_NEGATIVE_INTEGER)) { result = isValidNonNegativeInteger(value); } else if (datatype.equals(XMLSchema.POSITIVE_INTEGER)) { result = isValidPositiveInteger(value); } else if (datatype.equals(XMLSchema.LONG)) { result = isValidLong(value); } else if (datatype.equals(XMLSchema.INT)) { result = isValidInt(value); } else if (datatype.equals(XMLSchema.SHORT)) { result = isValidShort(value); } else if (datatype.equals(XMLSchema.BYTE)) { result = isValidByte(value); } else if (datatype.equals(XMLSchema.UNSIGNED_LONG)) { result = isValidUnsignedLong(value); } else if (datatype.equals(XMLSchema.UNSIGNED_INT)) { result = isValidUnsignedInt(value); } else if (datatype.equals(XMLSchema.UNSIGNED_SHORT)) { result = isValidUnsignedShort(value); } else if (datatype.equals(XMLSchema.UNSIGNED_BYTE)) { result = isValidUnsignedByte(value); } else if (datatype.equals(XMLSchema.FLOAT)) { result = isValidFloat(value); } else if (datatype.equals(XMLSchema.DOUBLE)) { result = isValidDouble(value); } else if (datatype.equals(XMLSchema.BOOLEAN)) { result = isValidBoolean(value); } else if (datatype.equals(XMLSchema.DATETIME)) { result = isValidDateTime(value); } return result; } public static boolean isValidDecimal(String value) { try { normalizeDecimal(value); return true; } catch (IllegalArgumentException e) { return false; } } public static boolean isValidInteger(String value) { try { normalizeInteger(value); return true; } catch (IllegalArgumentException e) { return false; } } public static boolean isValidNegativeInteger(String value) { try { normalizeNegativeInteger(value); return true; } catch (IllegalArgumentException e) { return false; } } public static boolean isValidNonPositiveInteger(String value) { try { normalizeNonPositiveInteger(value); return true; } catch (IllegalArgumentException e) { return false; } } public static boolean isValidNonNegativeInteger(String value) { try { normalizeNonNegativeInteger(value); return true; } catch (IllegalArgumentException e) { return false; } } public static boolean isValidPositiveInteger(String value) { try { normalizePositiveInteger(value); return true; } catch (IllegalArgumentException e) { return false; } } public static boolean isValidLong(String value) { try { normalizeLong(value); return true; } catch (IllegalArgumentException e) { return false; } } public static boolean isValidInt(String value) { try { normalizeInt(value); return true; } catch (IllegalArgumentException e) { return false; } } public static boolean isValidShort(String value) { try { normalizeShort(value); return true; } catch (IllegalArgumentException e) { return false; } } public static boolean isValidByte(String value) { try { normalizeByte(value); return true; } catch (IllegalArgumentException e) { return false; } } public static boolean isValidUnsignedLong(String value) { try { normalizeUnsignedLong(value); return true; } catch (IllegalArgumentException e) { return false; } } public static boolean isValidUnsignedInt(String value) { try { normalizeUnsignedInt(value); return true; } catch (IllegalArgumentException e) { return false; } } public static boolean isValidUnsignedShort(String value) { try { normalizeUnsignedShort(value); return true; } catch (IllegalArgumentException e) { return false; } } public static boolean isValidUnsignedByte(String value) { try { normalizeUnsignedByte(value); return true; } catch (IllegalArgumentException e) { return false; } } public static boolean isValidFloat(String value) { try { normalizeFloat(value); return true; } catch (IllegalArgumentException e) { return false; } } public static boolean isValidDouble(String value) { try { normalizeDouble(value); return true; } catch (IllegalArgumentException e) { return false; } } public static boolean isValidBoolean(String value) { try { normalizeBoolean(value); return true; } catch (IllegalArgumentException e) { return false; } } public static boolean isValidDateTime(String value) { try { @SuppressWarnings("unused") XMLDateTime dt = new XMLDateTime(value); return true; } catch (IllegalArgumentException e) { return false; } } /*---------------------* * Value normalization * *---------------------*/ /** * Normalizes the supplied value according to the normalization rules for the * supplied datatype. * * @param value * The value to normalize. * @param datatype * The value's datatype. * @return The normalized value if there are any (supported) normalization * rules for the supplied datatype, or the original supplied value * otherwise. * @throws IllegalArgumentException * If the supplied value is illegal considering the supplied * datatype. */ public static String normalize(String value, URI datatype) { String result = value; if (datatype.equals(XMLSchema.DECIMAL)) { result = normalizeDecimal(value); } else if (datatype.equals(XMLSchema.INTEGER)) { result = normalizeInteger(value); } else if (datatype.equals(XMLSchema.NEGATIVE_INTEGER)) { result = normalizeNegativeInteger(value); } else if (datatype.equals(XMLSchema.NON_POSITIVE_INTEGER)) { result = normalizeNonPositiveInteger(value); } else if (datatype.equals(XMLSchema.NON_NEGATIVE_INTEGER)) { result = normalizeNonNegativeInteger(value); } else if (datatype.equals(XMLSchema.POSITIVE_INTEGER)) { result = normalizePositiveInteger(value); } else if (datatype.equals(XMLSchema.LONG)) { result = normalizeLong(value); } else if (datatype.equals(XMLSchema.INT)) { result = normalizeInt(value); } else if (datatype.equals(XMLSchema.SHORT)) { result = normalizeShort(value); } else if (datatype.equals(XMLSchema.BYTE)) { result = normalizeByte(value); } else if (datatype.equals(XMLSchema.UNSIGNED_LONG)) { result = normalizeUnsignedLong(value); } else if (datatype.equals(XMLSchema.UNSIGNED_INT)) { result = normalizeUnsignedInt(value); } else if (datatype.equals(XMLSchema.UNSIGNED_SHORT)) { result = normalizeUnsignedShort(value); } else if (datatype.equals(XMLSchema.UNSIGNED_BYTE)) { result = normalizeUnsignedByte(value); } else if (datatype.equals(XMLSchema.FLOAT)) { result = normalizeFloat(value); } else if (datatype.equals(XMLSchema.DOUBLE)) { result = normalizeDouble(value); } else if (datatype.equals(XMLSchema.BOOLEAN)) { result = normalizeBoolean(value); } else if (datatype.equals(XMLSchema.DATETIME)) { result = normalizeDateTime(value); } return result; } /** * Normalizes a boolean value to its canonical representation. More * specifically, the values <tt>1</tt> and <tt>0</tt> will be normalized * to the canonical values <tt>true</tt> and <tt>false</tt>, * respectively. Supplied canonical values will remain as is. * * @param value * The boolean value to normalize. * @return The normalized value. * @throws IllegalArgumentException * If the supplied value is not a legal boolean. */ public static String normalizeBoolean(String value) { value = collapseWhiteSpace(value); if (value.equals("1")) { return "true"; } else if (value.equals("0")) { return "false"; } else if (value.equals("true") || value.equals("false")) { return value; } else { throw new IllegalArgumentException("Not a legal boolean value: " + value); } } /** * Normalizes a decimal to its canonical representation. For example: * <tt>120</tt> becomes <tt>120.0</tt>, <tt>+.3</tt> becomes * <tt>0.3</tt>, <tt>00012.45000</tt> becomes <tt>12.45</tt> and * <tt>-.0</tt> becomes <tt>0.0</tt>. * * @param decimal * The decimal to normalize. * @return The canonical representation of <tt>decimal</tt>. * @throws IllegalArgumentException * If one of the supplied strings is not a legal decimal. */ public static String normalizeDecimal(String decimal) { decimal = collapseWhiteSpace(decimal); String errMsg = "Not a legal decimal: " + decimal; int decLength = decimal.length(); StringBuilder result = new StringBuilder(decLength + 2); if (decLength == 0) { throwIAE(errMsg); } boolean isZeroPointZero = true; // process any sign info int idx = 0; if (decimal.charAt(idx) == '-') { result.append('-'); idx++; } else if (decimal.charAt(idx) == '+') { idx++; } if (idx == decLength) { throwIAE(errMsg); } // skip any leading zeros while (idx < decLength && decimal.charAt(idx) == '0') { idx++; } // Process digits before the dot if (idx == decLength) { // decimal consists of zeros only result.append('0'); } else if (idx < decLength && decimal.charAt(idx) == '.') { // no non-zero digit before the dot result.append('0'); } else { isZeroPointZero = false; // Copy any digits before the dot while (idx < decLength) { char c = decimal.charAt(idx); if (c == '.') { break; } if (!isDigit(c)) { throwIAE(errMsg); } result.append(c); idx++; } } result.append('.'); // Process digits after the dot if (idx == decLength) { // No dot was found in the decimal result.append('0'); } else { idx++; // search last non-zero digit int lastIdx = decLength - 1; while (lastIdx >= 0 && decimal.charAt(lastIdx) == '0') { lastIdx--; } if (idx > lastIdx) { // No non-zero digits found result.append('0'); } else { isZeroPointZero = false; while (idx <= lastIdx) { char c = decimal.charAt(idx); if (!isDigit(c)) { throwIAE(errMsg); } result.append(c); idx++; } } } if (isZeroPointZero) { // Make sure we don't return "-0.0" return "0.0"; } else { return result.toString(); } } /** * Normalizes an integer to its canonical representation. For example: * <tt>+120</tt> becomes <tt>120</tt> and <tt>00012</tt> becomes * <tt>12</tt>. * * @param value * The value to normalize. * @return The canonical representation of <tt>value</tt>. * @throws IllegalArgumentException * If the supplied value is not a legal integer. */ public static String normalizeInteger(String value) { return normalizeIntegerValue(value, null, null); } /** * Normalizes an xsd:negativeInteger. */ public static String normalizeNegativeInteger(String value) { return normalizeIntegerValue(value, null, "-1"); } /** * Normalizes an xsd:nonPositiveInteger. */ public static String normalizeNonPositiveInteger(String value) { return normalizeIntegerValue(value, null, "0"); } /** * Normalizes an xsd:nonNegativeInteger. */ public static String normalizeNonNegativeInteger(String value) { return normalizeIntegerValue(value, "0", null); } /** * Normalizes an xsd:positiveInteger. */ public static String normalizePositiveInteger(String value) { return normalizeIntegerValue(value, "1", null); } /** * Normalizes an xsd:long. */ public static String normalizeLong(String value) { return normalizeIntegerValue(value, "-9223372036854775808", "9223372036854775807"); } /** * Normalizes an xsd:int. */ public static String normalizeInt(String value) { return normalizeIntegerValue(value, "-2147483648", "2147483647"); } /** * Normalizes an xsd:short. */ public static String normalizeShort(String value) { return normalizeIntegerValue(value, "-32768", "32767"); } /** * Normalizes an xsd:byte. */ public static String normalizeByte(String value) { return normalizeIntegerValue(value, "-128", "127"); } /** * Normalizes an xsd:unsignedLong. */ public static String normalizeUnsignedLong(String value) { return normalizeIntegerValue(value, "0", "18446744073709551615"); } /** * Normalizes an xsd:unsignedInt. */ public static String normalizeUnsignedInt(String value) { return normalizeIntegerValue(value, "0", "4294967295"); } /** * Normalizes an xsd:unsignedShort. */ public static String normalizeUnsignedShort(String value) { return normalizeIntegerValue(value, "0", "65535"); } /** * Normalizes an xsd:unsignedByte. */ public static String normalizeUnsignedByte(String value) { return normalizeIntegerValue(value, "0", "255"); } /** * Normalizes an integer to its canonical representation and checks that the * value is in the range [minValue, maxValue]. */ private static String normalizeIntegerValue(String integer, String minValue, String maxValue) { integer = collapseWhiteSpace(integer); String errMsg = "Not a legal integer: " + integer; int intLength = integer.length(); if (intLength == 0) { throwIAE(errMsg); } int idx = 0; // process any sign info boolean isNegative = false; if (integer.charAt(idx) == '-') { isNegative = true; idx++; } else if (integer.charAt(idx) == '+') { idx++; } if (idx == intLength) { throwIAE(errMsg); } if (integer.charAt(idx) == '0' && idx < intLength - 1) { // integer starts with a zero followed by more characters, // skip any leading zeros idx++; while (idx < intLength - 1 && integer.charAt(idx) == '0') { idx++; } } String norm = integer.substring(idx); // Check that all characters in 'norm' are digits for (int i = 0; i < norm.length(); i++) { if (!isDigit(norm.charAt(i))) { throwIAE(errMsg); } } if (isNegative && norm.charAt(0) != '0') { norm = "-" + norm; } // Check lower and upper bounds, if applicable if (minValue != null) { if (compareCanonicalIntegers(norm, minValue) < 0) { throwIAE("Value smaller than minimum value"); } } if (maxValue != null) { if (compareCanonicalIntegers(norm, maxValue) > 0) { throwIAE("Value larger than maximum value"); } } return norm; } /** * Normalizes a float to its canonical representation. * * @param value * The value to normalize. * @return The canonical representation of <tt>value</tt>. * @throws IllegalArgumentException * If the supplied value is not a legal float. */ public static String normalizeFloat(String value) { return normalizeFPNumber(value, "-16777215.0", "16777215.0", "-149", "104"); } /** * Normalizes a double to its canonical representation. * * @param value * The value to normalize. * @return The canonical representation of <tt>value</tt>. * @throws IllegalArgumentException * If the supplied value is not a legal double. */ public static String normalizeDouble(String value) { return normalizeFPNumber(value, "-9007199254740991.0", "9007199254740991.0", "-1075", "970"); } /** * Normalizes a floating point number to its canonical representation. * * @param value * The value to normalize. * @return The canonical representation of <tt>value</tt>. * @throws IllegalArgumentException * If the supplied value is not a legal floating point number. */ public static String normalizeFPNumber(String value) { return normalizeFPNumber(value, null, null, null, null); } /** * Normalizes a floating point number to its canonical representation. * * @param value * The value to normalize. * @param minMantissa * A normalized decimal indicating the lowest value that the mantissa * may have. * @param maxMantissa * A normalized decimal indicating the highest value that the mantissa * may have. * @param minExponent * A normalized integer indicating the lowest value that the exponent * may have. * @param maxExponent * A normalized integer indicating the highest value that the exponent * may have. * @return The canonical representation of <tt>value</tt>. * @throws IllegalArgumentException * If the supplied value is not a legal floating point number. */ private static String normalizeFPNumber( String value, String minMantissa, String maxMantissa, String minExponent, String maxExponent) { value = collapseWhiteSpace(value); // handle special values if (value.equals("INF") || value.equals("-INF") || value.equals("NaN")) { return value; } // Search for the exponent character E or e int eIdx = value.indexOf('E'); if (eIdx == -1) { // try lower case eIdx = value.indexOf('e'); } // Extract mantissa and exponent String mantissa, exponent; if (eIdx == -1) { mantissa = normalizeDecimal(value); exponent = "0"; } else { mantissa = normalizeDecimal(value.substring(0, eIdx)); exponent = normalizeInteger(value.substring(eIdx + 1)); } // Check lower and upper bounds, if applicable if (minMantissa != null) { if (compareCanonicalDecimals(mantissa, minMantissa) < 0) { throwIAE("Mantissa smaller than minimum value (" + minMantissa + ")"); } } if (maxMantissa != null) { if (compareCanonicalDecimals(mantissa, maxMantissa) > 0) { throwIAE("Mantissa larger than maximum value (" + maxMantissa + ")"); } } if (minExponent != null) { if (compareCanonicalIntegers(exponent, minExponent) < 0) { throwIAE("Exponent smaller than minimum value (" + minExponent + ")"); } } if (maxExponent != null) { if (compareCanonicalIntegers(exponent, maxExponent) > 0) { throwIAE("Exponent larger than maximum value (" + maxExponent + ")"); } } // Normalize mantissa to one non-zero digit before the dot int shift = 0; int dotIdx = mantissa.indexOf('.'); int digitCount = dotIdx; if (mantissa.charAt(0) == '-') { digitCount--; } if (digitCount > 1) { // more than one digit before the dot, e.g 123.45, -10.0 or 100.0 StringBuilder sb = new StringBuilder(mantissa.length()); int firstDigitIdx = 0; if (mantissa.charAt(0) == '-') { sb.append('-'); firstDigitIdx = 1; } sb.append(mantissa.charAt(firstDigitIdx)); sb.append('.'); sb.append(mantissa.substring(firstDigitIdx + 1, dotIdx)); sb.append(mantissa.substring(dotIdx + 1)); mantissa = sb.toString(); // Check if the mantissa has excessive trailing zeros. // For example, 100.0 will be normalize to 1.000 and // -10.0 to -1.00. int nonZeroIdx = mantissa.length() - 1; while (nonZeroIdx >= 3 && mantissa.charAt(nonZeroIdx) == '0') { nonZeroIdx--; } if (nonZeroIdx < 3 && mantissa.charAt(0) == '-') { nonZeroIdx++; } if (nonZeroIdx < mantissa.length() - 1) { mantissa = mantissa.substring(0, nonZeroIdx + 1); } shift = 1 - digitCount; } else if (mantissa.startsWith("0.") || mantissa.startsWith("-0.")) { // Example mantissas: 0.0, -0.1, 0.00345 and 0.09 // search first non-zero digit int nonZeroIdx = 2; while (nonZeroIdx < mantissa.length() && mantissa.charAt(nonZeroIdx) == '0') { nonZeroIdx++; } // 0.0 does not need any normalization: if (nonZeroIdx < mantissa.length()) { StringBuilder sb = new StringBuilder(mantissa.length()); sb.append(mantissa.charAt(nonZeroIdx)); sb.append('.'); if (nonZeroIdx == mantissa.length() - 1) { // There was only one non-zero digit, e.g. as in 0.09 sb.append('0'); } else { sb.append(mantissa.substring(nonZeroIdx + 1)); } mantissa = sb.toString(); shift = nonZeroIdx - 1; } } if (shift != 0) { try { int exp = Integer.parseInt(exponent); exponent = String.valueOf(exp - shift); } catch (NumberFormatException e) { throw new RuntimeException("NumberFormatException: " + e.getMessage()); } } return mantissa + "E" + exponent; } /** * Normalizes an xsd:dateTime. * * @param value * The value to normalize. * @return The normalized value. * @throws IllegalArgumentException * If the supplied value is not a legal xsd:dateTime value. */ public static String normalizeDateTime(String value) { XMLDateTime dt = new XMLDateTime(value); dt.normalize(); return dt.toString(); } /** * Replaces all occurences of #x9 (tab), #xA (line feed) and #xD (carriage * return) with #x20 (space), as specified for whiteSpace facet * <tt>replace</tt>. */ // private static String replaceWhiteSpace(String s) { // s = StringUtil.gsub("\t", " ", s); // s = StringUtil.gsub("\r", " ", s); // s = StringUtil.gsub("\n", " ", s); // return s; // } /** * Replaces all contiguous sequences of #x9 (tab), #xA (line feed) and #xD * (carriage return) with a single #x20 (space) character, and removes any * leading and trailing whitespace characters, as specified for whiteSpace * facet <tt>collapse</tt>. */ public static String collapseWhiteSpace(String s) { StringBuilder sb = new StringBuilder(s.length()); StringTokenizer st = new StringTokenizer(s, "\t\r\n "); if (st.hasMoreTokens()) { sb.append(st.nextToken()); } while (st.hasMoreTokens()) { sb.append(' ').append(st.nextToken()); } return sb.toString(); } /*------------------* * Value comparison * *------------------*/ public static int compare(String value1, String value2, URI datatype) { if (datatype.equals(XMLSchema.DECIMAL)) { return compareDecimals(value1, value2); } else if (datatype.equals(XMLSchema.INTEGER)) { return compareIntegers(value1, value2); } else if (datatype.equals(XMLSchema.NEGATIVE_INTEGER)) { return compareNegativeIntegers(value1, value2); } else if (datatype.equals(XMLSchema.NON_POSITIVE_INTEGER)) { return compareNonPositiveIntegers(value1, value2); } else if (datatype.equals(XMLSchema.NON_NEGATIVE_INTEGER)) { return compareNonNegativeIntegers(value1, value2); } else if (datatype.equals(XMLSchema.POSITIVE_INTEGER)) { return comparePositiveIntegers(value1, value2); } else if (datatype.equals(XMLSchema.LONG)) { return compareLongs(value1, value2); } else if (datatype.equals(XMLSchema.INT)) { return compareInts(value1, value2); } else if (datatype.equals(XMLSchema.SHORT)) { return compareShorts(value1, value2); } else if (datatype.equals(XMLSchema.BYTE)) { return compareBytes(value1, value2); } else if (datatype.equals(XMLSchema.UNSIGNED_LONG)) { return compareUnsignedLongs(value1, value2); } else if (datatype.equals(XMLSchema.UNSIGNED_INT)) { return compareUnsignedInts(value1, value2); } else if (datatype.equals(XMLSchema.UNSIGNED_SHORT)) { return compareUnsignedShorts(value1, value2); } else if (datatype.equals(XMLSchema.UNSIGNED_BYTE)) { return compareUnsignedBytes(value1, value2); } else if (datatype.equals(XMLSchema.FLOAT)) { return compareFloats(value1, value2); } else if (datatype.equals(XMLSchema.DOUBLE)) { return compareDoubles(value1, value2); } else if (datatype.equals(XMLSchema.DATETIME)) { return compareDateTime(value1, value2); } else { throw new IllegalArgumentException("datatype is not ordered"); } } /** * Compares two decimals to eachother. * * @return A negative number if <tt>dec1</tt> is smaller than <tt>dec2</tt>, * <tt>0</tt> if they are equal, or positive (>0) if * <tt>dec1</tt> is larger than <tt>dec2</tt>. * @throws IllegalArgumentException * If one of the supplied strings is not a legal decimal. */ public static int compareDecimals(String dec1, String dec2) { dec1 = normalizeDecimal(dec1); dec2 = normalizeDecimal(dec2); return compareCanonicalDecimals(dec1, dec2); } /** * Compares two canonical decimals to eachother. * * @return A negative number if <tt>dec1</tt> is smaller than <tt>dec2</tt>, * <tt>0</tt> if they are equal, or positive (>0) if * <tt>dec1</tt> is larger than <tt>dec2</tt>. The result is * undefined when one or both of the arguments is not a canonical * decimal. * @throws IllegalArgumentException * If one of the supplied strings is not a legal decimal. */ public static int compareCanonicalDecimals(String dec1, String dec2) { if (dec1.equals(dec2)) { return 0; } // Check signs if (dec1.charAt(0) == '-' && dec2.charAt(0) != '-') { // dec1 is negative, dec2 is not return -1; } if (dec2.charAt(0) == '-' && dec1.charAt(0) != '-') { // dec2 is negative, dec1 is not return 1; } int dotIdx1 = dec1.indexOf('.'); int dotIdx2 = dec2.indexOf('.'); // The decimal with the most digits before the dot is the largest int result = dotIdx1 - dotIdx2; if (result == 0) { // equal number of digits before the dot, compare them for (int i = 0; result == 0 && i < dotIdx1; i++) { result = dec1.charAt(i) - dec2.charAt(i); } // Continue comparing digits after the dot if necessary int dec1Length = dec1.length(); int dec2Length = dec2.length(); int lastIdx = dec1Length <= dec2Length ? dec1Length : dec2Length; for (int i = dotIdx1 + 1; result == 0 && i < lastIdx; i++) { result = dec1.charAt(i) - dec2.charAt(i); } // Still equal? The decimal with the most digits is the largest if (result == 0) { result = dec1Length - dec2Length; } } if (dec1.charAt(0) == '-') { // reverse result for negative values result = -result; } return result; } /** * Compares two integers to eachother. * * @return A negative number if <tt>int1</tt> is smaller than <tt>int2</tt>, * <tt>0</tt> if they are equal, or positive (>0) if * <tt>int1</tt> is larger than <tt>int2</tt>. * @throws IllegalArgumentException * If one of the supplied strings is not a legal integer. */ public static int compareIntegers(String int1, String int2) { int1 = normalizeInteger(int1); int2 = normalizeInteger(int2); return compareCanonicalIntegers(int1, int2); } /** * Compares two canonical integers to eachother. * * @return A negative number if <tt>int1</tt> is smaller than <tt>int2</tt>, * <tt>0</tt> if they are equal, or positive (>0) if * <tt>int1</tt> is larger than <tt>int2</tt>. The result is * undefined when one or both of the arguments is not a canonical * integer. * @throws IllegalArgumentException * If one of the supplied strings is not a legal integer. */ public static int compareCanonicalIntegers(String int1, String int2) { if (int1.equals(int2)) { return 0; } // Check signs if (int1.charAt(0) == '-' && int2.charAt(0) != '-') { // int1 is negative, int2 is not return -1; } if (int2.charAt(0) == '-' && int1.charAt(0) != '-') { // int2 is negative, int1 is not return 1; } // The integer with the most digits is the largest int result = int1.length() - int2.length(); if (result == 0) { // equal number of digits, compare them for (int i = 0; result == 0 && i < int1.length(); i++) { result = int1.charAt(i) - int2.charAt(i); } } if (int1.charAt(0) == '-') { // reverse result for negative values result = -result; } return result; } public static int compareNegativeIntegers(String int1, String int2) { int1 = normalizeNegativeInteger(int1); int2 = normalizeNegativeInteger(int2); return compareCanonicalIntegers(int1, int2); } public static int compareNonPositiveIntegers(String int1, String int2) { int1 = normalizeNonPositiveInteger(int1); int2 = normalizeNonPositiveInteger(int2); return compareCanonicalIntegers(int1, int2); } public static int compareNonNegativeIntegers(String int1, String int2) { int1 = normalizeNonNegativeInteger(int1); int2 = normalizeNonNegativeInteger(int2); return compareCanonicalIntegers(int1, int2); } public static int comparePositiveIntegers(String int1, String int2) { int1 = normalizePositiveInteger(int1); int2 = normalizePositiveInteger(int2); return compareCanonicalIntegers(int1, int2); } public static int compareLongs(String int1, String int2) { int1 = normalizeLong(int1); int2 = normalizeLong(int2); return compareCanonicalIntegers(int1, int2); } public static int compareInts(String int1, String int2) { int1 = normalizeInt(int1); int2 = normalizeInt(int2); return compareCanonicalIntegers(int1, int2); } public static int compareShorts(String int1, String int2) { int1 = normalizeShort(int1); int2 = normalizeShort(int2); return compareCanonicalIntegers(int1, int2); } public static int compareBytes(String int1, String int2) { int1 = normalizeByte(int1); int2 = normalizeByte(int2); return compareCanonicalIntegers(int1, int2); } public static int compareUnsignedLongs(String int1, String int2) { int1 = normalizeUnsignedLong(int1); int2 = normalizeUnsignedLong(int2); return compareCanonicalIntegers(int1, int2); } public static int compareUnsignedInts(String int1, String int2) { int1 = normalizeUnsignedInt(int1); int2 = normalizeUnsignedInt(int2); return compareCanonicalIntegers(int1, int2); } public static int compareUnsignedShorts(String int1, String int2) { int1 = normalizeUnsignedShort(int1); int2 = normalizeUnsignedShort(int2); return compareCanonicalIntegers(int1, int2); } public static int compareUnsignedBytes(String int1, String int2) { int1 = normalizeUnsignedByte(int1); int2 = normalizeUnsignedByte(int2); return compareCanonicalIntegers(int1, int2); } /** * Compares two floats to eachother. * * @return A negative number if <tt>float1</tt> is smaller than * <tt>float2</tt>, <tt>0</tt> if they are equal, or positive * (>0) if <tt>float1</tt> is larger than <tt>float2</tt>. * @throws IllegalArgumentException * If one of the supplied strings is not a legal float or if * <tt>NaN</tt> is compared to a float other than <tt>NaN</tt>. */ public static int compareFloats(String float1, String float2) { float1 = normalizeFloat(float1); float2 = normalizeFloat(float2); return compareCanonicalFloats(float1, float2); } /** * Compares two canonical floats to eachother. * * @return A negative number if <tt>float1</tt> is smaller than * <tt>float2</tt>, <tt>0</tt> if they are equal, or positive * (>0) if <tt>float1</tt> is larger than <tt>float2</tt>. * The result is undefined when one or both of the arguments is not a * canonical float. * @throws IllegalArgumentException * If one of the supplied strings is not a legal float or if * <tt>NaN</tt> is compared to a float other than <tt>NaN</tt>. */ public static int compareCanonicalFloats(String float1, String float2) { return compareCanonicalFPNumbers(float1, float2); } /** * Compares two doubles to eachother. * * @return A negative number if <tt>double1</tt> is smaller than * <tt>double2</tt>, <tt>0</tt> if they are equal, or positive * (>0) if <tt>double1</tt> is larger than <tt>double2</tt>. * @throws IllegalArgumentException * If one of the supplied strings is not a legal double or if * <tt>NaN</tt> is compared to a double other than <tt>NaN</tt>. */ public static int compareDoubles(String double1, String double2) { double1 = normalizeDouble(double1); double2 = normalizeDouble(double2); return compareCanonicalDoubles(double1, double2); } /** * Compares two canonical doubles to eachother. * * @return A negative number if <tt>double1</tt> is smaller than * <tt>double2</tt>, <tt>0</tt> if they are equal, or positive * (>0) if <tt>double1</tt> is larger than <tt>double2</tt>. * The result is undefined when one or both of the arguments is not a * canonical double. * @throws IllegalArgumentException * If one of the supplied strings is not a legal double or if * <tt>NaN</tt> is compared to a double other than <tt>NaN</tt>. */ public static int compareCanonicalDoubles(String double1, String double2) { return compareCanonicalFPNumbers(double1, double2); } /** * Compares two floating point numbers to eachother. * * @return A negative number if <tt>float1</tt> is smaller than * <tt>float2</tt>, <tt>0</tt> if they are equal, or positive * (>0) if <tt>float1</tt> is larger than <tt>float2</tt>. * @throws IllegalArgumentException * If one of the supplied strings is not a legal floating point * number or if <tt>NaN</tt> is compared to a floating point number * other than <tt>NaN</tt>. */ public static int compareFPNumbers(String fp1, String fp2) { fp1 = normalizeFPNumber(fp1); fp2 = normalizeFPNumber(fp2); return compareCanonicalFPNumbers(fp1, fp2); } /** * Compares two canonical floating point numbers to eachother. * * @return A negative number if <tt>float1</tt> is smaller than * <tt>float2</tt>, <tt>0</tt> if they are equal, or positive * (>0) if <tt>float1</tt> is larger than <tt>float2</tt>. * The result is undefined when one or both of the arguments is not a * canonical floating point number. * @throws IllegalArgumentException * If one of the supplied strings is not a legal floating point * number or if <tt>NaN</tt> is compared to a floating point number * other than <tt>NaN</tt>. */ public static int compareCanonicalFPNumbers(String float1, String float2) { // Handle special case NaN if (float1.equals("NaN") || float2.equals("NaN")) { if (float1.equals(float2)) { // NaN is equal to itself return 0; } else { throwIAE("NaN cannot be compared to other floats"); } } // Handle special case INF if (float1.equals("INF")) { return (float2.equals("INF")) ? 0 : 1; } else if (float2.equals("INF")) { return -1; } // Handle special case -INF if (float1.equals("-INF")) { return (float2.equals("-INF")) ? 0 : -1; } else if (float2.equals("-INF")) { return 1; } // Check signs if (float1.charAt(0) == '-' && float2.charAt(0) != '-') { // float1 is negative, float2 is not return -1; } if (float2.charAt(0) == '-' && float1.charAt(0) != '-') { // float2 is negative, float1 is not return 1; } int eIdx1 = float1.indexOf('E'); String mantissa1 = float1.substring(0, eIdx1); String exponent1 = float1.substring(eIdx1 + 1); int eIdx2 = float2.indexOf('E'); String mantissa2 = float2.substring(0, eIdx2); String exponent2 = float2.substring(eIdx2 + 1); // Compare exponents int result = compareCanonicalIntegers(exponent1, exponent2); if (result != 0 && float1.charAt(0) == '-') { // reverse result for negative values result = -result; } if (result == 0) { // Equal exponents, compare mantissas result = compareCanonicalDecimals(mantissa1, mantissa2); } return result; } /** * Compares two dateTime objects. <b>Important:</b> The comparison only * works if both values have, or both values don't have specified a valid * value for the timezone. * * @param value1 * An xsd:dateTime value. * @param value2 * An xsd:dateTime value. * @return <tt>-1</tt> if <tt>value1</tt> is before <tt>value2</tt> * (i.e. if the dateTime object represented by value1 is before the * dateTime object represented by value2), <tt>0</tt> if both are * equal and <tt>1</tt> if <tt>value2</tt> is before * <tt>value1</tt><br>. */ public static int compareDateTime(String value1, String value2) { XMLDateTime dateTime1 = new XMLDateTime(value1); XMLDateTime dateTime2 = new XMLDateTime(value2); dateTime1.normalize(); dateTime2.normalize(); return dateTime1.compareTo(dateTime2); } /*---------------* * Value parsing * *---------------*/ /** * Parses the supplied xsd:boolean string and returns its value. * * @param s * A string representation of an xsd:boolean value. * @return The <tt>boolean</tt> value represented by the supplied string * argument. * @throws NumberFormatException * If the supplied string is not a valid xsd:boolean value. */ public static boolean parseBoolean(String s) { return normalizeBoolean(s).equals("true"); } /** * Parses the supplied xsd:byte string and returns its value. * * @param s * A string representation of an xsd:byte value. * @return The <tt>byte</tt> value represented by the supplied string * argument. * @throws NumberFormatException * If the supplied string is not a valid xsd:byte value. */ public static byte parseByte(String s) { s = trimPlusSign(s); return Byte.parseByte(s); } /** * Parses the supplied xsd:short string and returns its value. * * @param s * A string representation of an xsd:short value. * @return The <tt>short</tt> value represented by the supplied string * argument. * @throws NumberFormatException * If the supplied string is not a valid xsd:short value. */ public static short parseShort(String s) { s = trimPlusSign(s); return Short.parseShort(s); } /** * Parses the supplied xsd:int strings and returns its value. * * @param s * A string representation of an xsd:int value. * @return The <tt>int</tt> value represented by the supplied string * argument. * @throws NumberFormatException * If the supplied string is not a valid xsd:int value. */ public static int parseInt(String s) { s = trimPlusSign(s); return Integer.parseInt(s); } /** * Parses the supplied xsd:long string and returns its value. * * @param s * A string representation of an xsd:long value. * @return The <tt>long</tt> value represented by the supplied string * argument. * @throws NumberFormatException * If the supplied string is not a valid xsd:long value. */ public static long parseLong(String s) { s = trimPlusSign(s); return Long.parseLong(s); } /** * Parses the supplied xsd:float string and returns its value. * * @param s * A string representation of an xsd:float value. * @return The <tt>float</tt> value represented by the supplied string * argument. * @throws NumberFormatException * If the supplied string is not a valid xsd:float value. */ public static float parseFloat(String s) { s = trimPlusSign(s); return Float.parseFloat(s); } /** * Parses the supplied xsd:double string and returns its value. * * @param s * A string representation of an xsd:double value. * @return The <tt>double</tt> value represented by the supplied string * argument. * @throws NumberFormatException * If the supplied string is not a valid xsd:double value. */ public static double parseDouble(String s) { s = trimPlusSign(s); return Double.parseDouble(s); } /** * Parses the supplied xsd:integer string and returns its value. * * @param s * A string representation of an xsd:integer value. * @return The integer value represented by the supplied string argument. * @throws NumberFormatException * If the supplied string is not a valid xsd:integer value. */ public static BigInteger parseInteger(String s) { s = trimPlusSign(s); return new BigInteger(s); } /** * Parses the supplied decimal/floating point string and returns its value. * * @param s * A string representation of an xsd:decimal or xsd:double value. * @return The decimal/floating point value represented by the supplied * string argument. * @throws NumberFormatException * If the supplied string is not a valid xsd:decimal or xsd:double * value. */ public static BigDecimal parseDecimal(String s) { // Note: BigDecimal can handle leading plus signs itself return new BigDecimal(s); } /** * Parses the supplied calendar value string and returns its value. * * @param s * A string representation of an xsd:dateTime, xsd:time, xsd:date, * xsd:gYearMonth, xsd:gMonthDay, xsd:gYear, xsd:gMonth or xsd:gDay * value. * @return The calendar value represented by the supplied string argument. * @throws NumberFormatException * If the supplied string is not a valid calendar value. */ public static XMLGregorianCalendar parseCalendar(String s) { return dtFactory.newXMLGregorianCalendar(s); } /** * Removes the first character from the supplied string if this is a plus * sign ('+'). Number strings with leading plus signs cannot be parsed by * methods such as {@link Integer#parseInt(String)}. */ private static String trimPlusSign(String s) { if (s.length() > 0 && s.charAt(0) == '+') { return s.substring(1); } else { return s; } } /** * Maps a datatype QName from the javax.xml.namespace package to an XML * Schema URI for the corresponding datatype. This method recognizes the XML * Schema qname mentioned in {@link DatatypeConstants}. * * @param qname * One of the XML Schema qnames from {@link DatatypeConstants}. * @return A URI for the specified datatype. * @throws IllegalArgumentException * If the supplied qname was not recognized by this method. * @see DatatypeConstants */ public static URI qnameToURI(QName qname) { if (DatatypeConstants.DATETIME.equals(qname)) { return XMLSchema.DATETIME; } else if (DatatypeConstants.DATE.equals(qname)) { return XMLSchema.DATE; } else if (DatatypeConstants.TIME.equals(qname)) { return XMLSchema.TIME; } else if (DatatypeConstants.GYEARMONTH.equals(qname)) { return XMLSchema.GYEARMONTH; } else if (DatatypeConstants.GMONTHDAY.equals(qname)) { return XMLSchema.GMONTHDAY; } else if (DatatypeConstants.GYEAR.equals(qname)) { return XMLSchema.GYEAR; } else if (DatatypeConstants.GMONTH.equals(qname)) { return XMLSchema.GMONTH; } else if (DatatypeConstants.GDAY.equals(qname)) { return XMLSchema.GDAY; } else if (DatatypeConstants.DURATION.equals(qname)) { return XMLSchema.DURATION; } else { throw new IllegalArgumentException("QName cannot be mapped to an XML Schema URI: " + qname.toString()); } } /*-----------------* * Utility methods * *-----------------*/ /** * Checks whether the supplied character is a digit. */ private static final boolean isDigit(char c) { return c >= '0' && c <= '9'; } /** * Throws an IllegalArgumentException that contains the supplied message. */ private static final void throwIAE(String msg) { throw new IllegalArgumentException(msg); } }