This is an automated email from the ASF dual-hosted git repository. andy pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/jena.git
commit 7a07d1cba13a32346a7a1d8b41b196ec951bf769 Author: Andy Seaborne <[email protected]> AuthorDate: Tue Nov 4 14:00:45 2025 +0000 GH-3562: Better NodeValue --- .../expr/{NodeValueCmp.java => NVCompare.java} | 2 +- .../org/apache/jena/sparql/expr/NVDatatypes.java | 95 +++++ .../org/apache/jena/sparql/expr/NVFactory.java | 242 +++++++++++ .../java/org/apache/jena/sparql/expr/NVOps.java | 154 +++++++ .../org/apache/jena/sparql/expr/NodeValue.java | 365 ++++------------ .../sparql/expr/nodevalue/NodeValueDateTime.java | 15 - .../java/org/apache/jena/sparql/expr/TS_Expr.java | 3 +- .../apache/jena/sparql/expr/TestComparison.java | 2 +- .../org/apache/jena/sparql/expr/TestNVFactory.java | 457 +++++++++++++++++++++ .../org/apache/jena/sparql/expr/TestNodeValue.java | 32 +- .../apache/jena/sparql/expr/TestSortOrdering.java | 16 +- .../querybuilder/rewriters/NodeValueRewriter.java | 2 +- .../rewriters/NodeValueRewriterTest.java | 3 +- 13 files changed, 1062 insertions(+), 326 deletions(-) diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/expr/NodeValueCmp.java b/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVCompare.java similarity index 99% rename from jena-arq/src/main/java/org/apache/jena/sparql/expr/NodeValueCmp.java rename to jena-arq/src/main/java/org/apache/jena/sparql/expr/NVCompare.java index eb66c7acae..5f12156779 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/expr/NodeValueCmp.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVCompare.java @@ -39,7 +39,7 @@ import org.apache.jena.sparql.expr.nodevalue.NodeFunctions; import org.apache.jena.sparql.expr.nodevalue.XSDFuncOp; import org.apache.jena.sparql.util.NodeCmp; -public class NodeValueCmp { +class NVCompare { // ---------------------------------------------------------------- // ---- sameValueAs diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVDatatypes.java b/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVDatatypes.java new file mode 100644 index 0000000000..6cd527a772 --- /dev/null +++ b/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVDatatypes.java @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jena.sparql.expr; + +import java.util.Set; + +import org.apache.jena.datatypes.RDFDatatype; +import org.apache.jena.datatypes.xsd.XSDDatatype; +import org.apache.jena.vocabulary.RDF; + +/** Constants related to datatypes with values support in ARQ/SPARQL. */ +class NVDatatypes { + + /* + * Datatype representing xsd:precisionDecimal https://www.w3.org/TR/xsd-precisionDecimal/ + */ + // Not a derived type of xsd:decimal. + //public static final RDFDatatype XSDprecisionDecimal = new XSDPRecisionDecimal("precisionDecimal", BigDecimal.class); + + public static final RDFDatatype XSDdecimal = XSDDatatype.XSDdecimal; + public static final RDFDatatype XSDfloat = XSDDatatype.XSDfloat; + public static final RDFDatatype XSDdouble = XSDDatatype.XSDdouble; + + public static final RDFDatatype XSDinteger = XSDDatatype.XSDinteger; + public static final RDFDatatype XSDpositiveInteger = XSDDatatype.XSDpositiveInteger; + public static final RDFDatatype XSDnegativeInteger = XSDDatatype.XSDnegativeInteger; + public static final RDFDatatype XSDnonPositiveInteger = XSDDatatype.XSDnonPositiveInteger; + public static final RDFDatatype XSDnonNegativeInteger = XSDDatatype.XSDnonNegativeInteger; + + public static final RDFDatatype XSDlong = XSDDatatype.XSDlong; + public static final RDFDatatype XSDint = XSDDatatype.XSDint; + public static final RDFDatatype XSDshort = XSDDatatype.XSDshort; + public static final RDFDatatype XSDbyte = XSDDatatype.XSDbyte; + + public static final RDFDatatype XSDunsignedLong = XSDDatatype.XSDunsignedLong; + public static final RDFDatatype XSDunsignedInt = XSDDatatype.XSDunsignedInt; + public static final RDFDatatype XSDunsignedShort = XSDDatatype.XSDunsignedShort; + public static final RDFDatatype XSDunsignedByte = XSDDatatype.XSDunsignedByte; + + public static final RDFDatatype XSDboolean = XSDDatatype.XSDboolean; + + public static final RDFDatatype XSDstring = XSDDatatype.XSDstring; + public static final RDFDatatype langString = RDF.dtLangString; + public static final RDFDatatype dirLangString = RDF.dtDirLangString; + + + public static final RDFDatatype XSDnormalizedString = XSDDatatype.XSDnormalizedString; +// public static final RDFDatatype XSDtoken = XSDDatatype.XSDtoken; +// public static final RDFDatatype XSDlanguage = XSDDatatype.XSDlanguage; + +// public static final RDFDatatype XSDhexBinary = XSDDatatype.XSDhexBinary; +// public static final RDFDatatype XSDbase64Binary = XSDDatatype.XSDbase64Binary; + + public static final RDFDatatype XSDdateTime = XSDDatatype.XSDdateTime; + public static final RDFDatatype XSDdateTimeStamp = XSDDatatype.XSDdateTimeStamp; + public static final RDFDatatype XSDdate = XSDDatatype.XSDdate; + public static final RDFDatatype XSDtime = XSDDatatype.XSDtime; + + public static final RDFDatatype XSDduration = XSDDatatype.XSDduration; + public static final RDFDatatype XSDdayTimeDuration = XSDDatatype.XSDdayTimeDuration; + public static final RDFDatatype XSDyearMonthDuration = XSDDatatype.XSDyearMonthDuration; + + public static final RDFDatatype XSDgYear = XSDDatatype.XSDgYear; + public static final RDFDatatype XSDgMonth = XSDDatatype.XSDgMonth; + public static final RDFDatatype XSDgDay = XSDDatatype.XSDgDay; + public static final RDFDatatype XSDgYearMonth = XSDDatatype.XSDgYearMonth; + public static final RDFDatatype XSDgMonthDay = XSDDatatype.XSDgMonthDay; + + public static final Set<RDFDatatype> numerics = Set.of(XSDdecimal, XSDfloat, XSDdouble, XSDinteger, + XSDpositiveInteger, XSDnegativeInteger, XSDnonPositiveInteger, XSDnonNegativeInteger, + XSDlong, XSDint, XSDshort, XSDbyte, + XSDunsignedLong, XSDunsignedInt, XSDunsignedShort, XSDunsignedByte); + + public static final Set<RDFDatatype> durations = Set.of(XSDduration, XSDdayTimeDuration, XSDyearMonthDuration); + + public static final Set<RDFDatatype> temporal = Set.of(XSDdateTime, XSDdateTimeStamp,XSDdate, XSDtime, + XSDgYear, XSDgMonth, XSDgDay, + XSDgYearMonth, XSDgMonthDay); +} diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVFactory.java b/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVFactory.java new file mode 100644 index 0000000000..5da176ba7d --- /dev/null +++ b/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVFactory.java @@ -0,0 +1,242 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jena.sparql.expr; + +import static org.apache.jena.sparql.expr.NVDatatypes.*; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.HashMap; +import java.util.Map; + +import javax.xml.datatype.Duration; +import javax.xml.datatype.XMLGregorianCalendar; + +import org.apache.jena.datatypes.RDFDatatype; +import org.apache.jena.graph.Node; +import org.apache.jena.graph.impl.LiteralLabel; +import org.apache.jena.sparql.ARQInternalErrorException; +import org.apache.jena.sparql.SystemARQ; +import org.apache.jena.sparql.expr.nodevalue.*; +import org.apache.jena.sparql.util.RomanNumeral; +import org.apache.jena.sparql.util.RomanNumeralDatatype; +import org.apache.jena.vocabulary.RDF; + +class NVFactory { + @FunctionalInterface + interface ToNodeValue { NodeValue create(RDFDatatype datatype, Node node); } + + private static Map<RDFDatatype, ToNodeValue> mapper = dtSetup(); + + public static NodeValue create(Node node) { + RDFDatatype datatype = node.getLiteralDatatype(); + ToNodeValue function = mapper.get(datatype); + if ( function == null ) + return new NodeValueNode(node); + NodeValue nv = function.create(datatype, node); + if ( nv == null ) + return new NodeValueNode(node); + return nv; + } + + public static NodeValue create(RDFDatatype datatype, Node node) { + return mapper.get(datatype).create(datatype, node); + } + + /** Create an immutable map of datatype to NodeValue maker */ + private static Map<RDFDatatype, ToNodeValue> dtSetup() { + Map<RDFDatatype, ToNodeValue> mapper = new HashMap<>(); + + entry(mapper, XSDdecimal, NVFactory::decimalMaker); + entry(mapper, XSDfloat, NVFactory::floatMaker); + entry(mapper, XSDdouble, NVFactory::doubleMaker); + + entry(mapper, XSDinteger, NVFactory::integerMaker); + entry(mapper, XSDnonPositiveInteger, NVFactory::integerMaker); + entry(mapper, XSDnonNegativeInteger, NVFactory::integerMaker); + entry(mapper, XSDpositiveInteger, NVFactory::integerMaker); + entry(mapper, XSDnegativeInteger, NVFactory::integerMaker); + + entry(mapper, XSDbyte, NVFactory::integerMaker); + entry(mapper, XSDshort, NVFactory::integerMaker); + entry(mapper, XSDint, NVFactory::integerMaker); + entry(mapper, XSDlong, NVFactory::integerMaker); + + entry(mapper, XSDunsignedByte, NVFactory::integerMaker); + entry(mapper, XSDunsignedShort, NVFactory::integerMaker); + entry(mapper, XSDunsignedInt, NVFactory::integerMaker); + entry(mapper, XSDunsignedLong, NVFactory::integerMaker); + + entry(mapper, XSDboolean, NVFactory::booleanMaker); + + entry(mapper, XSDstring, NVFactory::stringMaker); + //[DT] XXX needs validation + entry(mapper, XSDnormalizedString, NVFactory::stringMaker); + //[DT] XXX xsd;token, xsd:language + + entry(mapper, RDF.dtLangString, NVFactory::langStringMaker); + entry(mapper, RDF.dtDirLangString, NVFactory::dirLangStringMaker); + +// entry(mapper, XSDhexBinary, null); +// entry(mapper, XSDbase64Binary, null); + + entry(mapper, XSDdate, NVFactory::dateTimeMaker); + entry(mapper, XSDtime, NVFactory::dateTimeMaker); + entry(mapper, XSDdateTime, NVFactory::dateTimeMaker); + entry(mapper, XSDdateTimeStamp, NVFactory::dateTimeMaker); + + entry(mapper, XSDgDay, NVFactory::dateTimeMaker); + entry(mapper, XSDgMonth, NVFactory::dateTimeMaker); + entry(mapper, XSDgYear, NVFactory::dateTimeMaker); + entry(mapper, XSDgYearMonth, NVFactory::dateTimeMaker); + entry(mapper, XSDgMonthDay, NVFactory::dateTimeMaker); + + entry(mapper, XSDduration, NVFactory::durationMaker); + entry(mapper, XSDdayTimeDuration, NVFactory::durationMaker); + entry(mapper, XSDyearMonthDuration, NVFactory::durationMaker); + + if ( SystemARQ.EnableRomanNumerals ) + entry(mapper, RomanNumeralDatatype.get(), NVFactory::romanNumeralMaker); + + return Map.copyOf(mapper); + } + + private static void entry( Map<RDFDatatype, ToNodeValue> map, RDFDatatype rdfDatatype, ToNodeValue toNodeValue) { + map.put(rdfDatatype, toNodeValue); + } + + private static NodeValue integerMaker(RDFDatatype datatype, Node node) { + if ( ! node.getLiteral().isWellFormed() ) + return null; + String trimmedLexical = node.getLiteralLexicalForm().trim(); + if ( ! datatype.isValid(trimmedLexical) ) + return null; + BigInteger bigInteger = new BigInteger(trimmedLexical); + return new NodeValueInteger(bigInteger, node); + } + + private static NodeValue floatMaker(RDFDatatype datatype, Node node) { + if ( ! node.getLiteral().isWellFormed() ) + return null; + // Uses getValue - no harm using isWellformed. + LiteralLabel lit = node.getLiteral(); + float f = ((Number)lit.getValue()).floatValue(); + return new NodeValueFloat(f, node); + } + + private static NodeValue doubleMaker(RDFDatatype datatype, Node node) { + LiteralLabel lit = node.getLiteral(); + if ( ! lit.isWellFormed() ) + return null; + double d = ((Number)lit.getValue()).doubleValue(); + return new NodeValueDouble(d, node); + } + + private static NodeValue decimalMaker(RDFDatatype datatype, Node node) { + LiteralLabel lit = node.getLiteral(); + if ( ! lit.isWellFormed() ) + return null; + String trimmedLexical = node.getLiteralLexicalForm().trim(); + // jena-core narrows dataypes. + BigDecimal decimal = new BigDecimal(trimmedLexical); + return new NodeValueDecimal(decimal, node); + } + + private static NodeValue booleanMaker(RDFDatatype datatype, Node node) { + LiteralLabel lit = node.getLiteral(); + if ( ! lit.isWellFormed() ) + return null; + boolean b = (Boolean) lit.getValue(); + return new NodeValueBoolean(b, node); + } + + private static NodeValue stringMaker(RDFDatatype datatype, Node node) { + return new NodeValueString(node.getLiteralLexicalForm(), node); + } + + private static NodeValue langStringMaker(RDFDatatype datatype, Node node) { + return new NodeValueLang(node); + } + + private static NodeValue dirLangStringMaker(RDFDatatype datatype, Node node) { + return new NodeValueLangDir(node); + } + + private static NodeValue dateTimeMaker(RDFDatatype datatype, Node node) { + String trimmedLexical = node.getLiteralLexicalForm().trim(); + try { + XMLGregorianCalendar gCal = createXMLGregorianCalendar(trimmedLexical, node); + // Check the expected fields. + boolean isCorrect = NVOps.checkCalendarInstance(gCal, datatype); + if (! isCorrect ) + return null; + return new NodeValueDateTime(gCal, node); + } catch (IllegalArgumentException ex) { + return null; + } + } + + // Fixup + private static XMLGregorianCalendar createXMLGregorianCalendar(String lex, Node n) { + // Java bug : gMonth with a timezone of Z causes IllegalArgumentException + if ( XSDgMonth.equals(n.getLiteralDatatype()) ) { + if ( lex.endsWith("Z") ) { + String lex2 = lex.substring(0, lex.length() - 1); + XMLGregorianCalendar gCal = NodeValue.xmlDatatypeFactory.newXMLGregorianCalendar(lex2); + gCal.setTimezone(0); + return gCal; + } + } + XMLGregorianCalendar gCal = NodeValue.xmlDatatypeFactory.newXMLGregorianCalendar(lex); + return gCal; + } + + private static NodeValue durationMaker(RDFDatatype datatype, Node node) { + String trimmedLexical = node.getLiteralLexicalForm().trim(); + try { + Duration duration = NodeValue.xmlDatatypeFactory.newDuration(trimmedLexical); + boolean isCorrect = NVOps.checkDurationInstance(duration, datatype); + if (! isCorrect ) + return null; + return new NodeValueDuration(duration, node); + } catch (IllegalArgumentException ex) { + return null; + } + } + + // Test extension type! + private static NodeValue romanNumeralMaker(RDFDatatype datatype, Node node) { + LiteralLabel lit = node.getLiteral(); + Object obj = RomanNumeralDatatype.get().parse(lit.getLexicalForm()); + if ( obj instanceof Integer ) + return new NodeValueInteger(((Integer)obj).longValue()); + if ( obj instanceof RomanNumeral ) + return new NodeValueInteger( ((RomanNumeral)obj).intValue() ); + throw new ARQInternalErrorException("DatatypeFormatException: Roman numeral is unknown class"); + } + + /** + * Converts a hexBinary literal node to a NodeValueNode. + * Assumes the node is a valid hexBinary literal. + */ + private static NodeValue hexBinaryToNodeValue(RDFDatatype datatype, Node node) { + // No conversion, just wrap the node as a NodeValueNode + return new NodeValueNode(node); + } +} diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVOps.java b/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVOps.java new file mode 100644 index 0000000000..4c86f3469c --- /dev/null +++ b/jena-arq/src/main/java/org/apache/jena/sparql/expr/NVOps.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jena.sparql.expr; + +import static org.apache.jena.sparql.expr.NVDatatypes.*; +import static java.util.Map.entry; + +import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; + +import javax.xml.datatype.DatatypeConstants; +import javax.xml.datatype.Duration; +import javax.xml.datatype.XMLGregorianCalendar; + +import org.apache.jena.datatypes.RDFDatatype; + +/** Operations related to data/times */ +class NVOps { + + // ---- Data/time with XMLGregorianCalendar + + private static final Map<RDFDatatype, Predicate<XMLGregorianCalendar>> TEMPORAL_VALIDATE = Map.ofEntries( + // date/time + entry(XSDdateTime, + gCal -> checkDateFields(gCal, true) && checkTimeFields(gCal, true)), + entry(XSDdateTimeStamp, + gCal -> checkDateFields(gCal, true) && checkTimeFields(gCal, true) && checkTimezoneField(gCal, true)), + entry(XSDdate, + gCal -> checkDateFields(gCal, true) && checkTimeFields(gCal, false)), + entry(XSDtime, + gCal -> checkDateFields(gCal, false) && checkTimeFields(gCal, true)), + // g* + entry(XSDgYear, + gCal -> checkDateFields(gCal, true, false, false) && checkTimeFields(gCal, false)), + entry(XSDgYearMonth, + gCal -> checkDateFields(gCal, true, true, false) && checkTimeFields(gCal, false)), + entry(XSDgMonth, + gCal -> checkDateFields(gCal, false, true, false) && checkTimeFields(gCal, false)), + entry(XSDgMonthDay, + gCal -> checkDateFields(gCal, false, true, true) && checkTimeFields(gCal, false)), + entry(XSDgDay, + gCal -> checkDateFields(gCal, false, false, true) && checkTimeFields(gCal, false)) + ); + + static boolean checkCalendarInstance(XMLGregorianCalendar gCal, RDFDatatype xsdDatatype) { + Objects.requireNonNull(xsdDatatype); + var predicate = TEMPORAL_VALIDATE.get(xsdDatatype); + return predicate.test(gCal); + } + + // No need to check getEonAndYear or getFractionalSecond. + // These can not be set unless their companion field is set. + + /** Check the set/undefined status of the 6 fields (not timezone, which is optional except in xsd:dateTimeStamp) */ + private static boolean checkFields(XMLGregorianCalendar gCal, boolean yearField, boolean monthField, boolean dayField, boolean hourField, boolean minuteField, boolean secondField) { + return checkDateFields(gCal, yearField, monthField, dayField) && checkTimeFields(gCal, hourField, minuteField, secondField); + } + + /** Check the set/undefined status of the date fields. */ + private static boolean checkDateFields(XMLGregorianCalendar gCal, boolean present) { + return checkDateFields(gCal, present, present, present); + } + + /** Check the set/undefined status of the 3 date fields. */ + private static boolean checkDateFields(XMLGregorianCalendar gCal, boolean yearField, boolean monthField, boolean dayField) { + if ( ! checkTemporalField(gCal.getYear(), yearField) ) + return false; + // No need to check getEonAndYear -- getYear will be set. + //gCal.getEonAndYear() + if ( ! checkTemporalField(gCal.getMonth(),monthField ) ) + return false; + if ( ! checkTemporalField(gCal.getDay() , dayField ) ) + return false; + return true; + } + + /** Check the set/undefined status of the time fields. */ + private static boolean checkTimeFields(XMLGregorianCalendar gCal, boolean present) { + return checkTimeFields(gCal, present, present, present); + } + + /** Check the set/undefined status of the 3 time fields. */ + private static boolean checkTimeFields(XMLGregorianCalendar gCal, boolean hourField, boolean minuteField, boolean secondField) { + if ( ! checkTemporalField(gCal.getHour(), hourField ) ) + return false; + if ( ! checkTemporalField(gCal.getMinute(), minuteField ) ) + return false; + if ( ! checkTemporalField(gCal.getSecond(), secondField ) ) + return false; + // No need to check getFractionalSecond -- getSecond will be set. + //gCal.getFractionalSecond(); + return true; + } + + /** Check the set/undefined status of the timezone field. */ + private static boolean checkTimezoneField(XMLGregorianCalendar gCal, boolean timezonePresent) { + return checkTemporalField(gCal.getTimezone(), timezonePresent); + } + + /** Check the set/undefined status of a field value in an XMLGregorialCalendar. */ + private static boolean checkTemporalField(int fieldValue, boolean isSet) { + return ( fieldValue != DatatypeConstants.FIELD_UNDEFINED ) == isSet; + } + + // ---- Duration + // A Duration of nothing set is illegal so we only need to test of missing + + private static final Map<RDFDatatype, Predicate<Duration>> DURATION_VALIDATE = Map.ofEntries + ( + entry(XSDduration, dur -> true), + entry(XSDdayTimeDuration, dur ->isDayTimeDuration(dur)), + entry(XSDyearMonthDuration, dur -> isYearMonthDuration(dur)) + ); + + static boolean checkDurationInstance(Duration duration, RDFDatatype xsdDatatype) { + Objects.requireNonNull(xsdDatatype); + var predicate = DURATION_VALIDATE.get(xsdDatatype); + return predicate.test(duration); + } + + static boolean isDayTimeDuration(Duration duration) { + return checkDurationField(duration, DatatypeConstants.YEARS, false) && + checkDurationField(duration, DatatypeConstants.MONTHS, false) ; + } + + static boolean isYearMonthDuration(Duration duration) { + return checkDurationField(duration, DatatypeConstants.DAYS, false) && + checkDurationField(duration, DatatypeConstants.HOURS, false) && + checkDurationField(duration, DatatypeConstants.MINUTES, false) && + checkDurationField(duration, DatatypeConstants.SECONDS, false) ; + } + + /** Check the set/undefined status of a field value in a Duration. */ + private static boolean checkDurationField(Duration duration, DatatypeConstants.Field field, boolean isSet) { + return duration.isSet(field) == isSet; + } +} diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/expr/NodeValue.java b/jena-arq/src/main/java/org/apache/jena/sparql/expr/NodeValue.java index 877007806a..0e619b453a 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/expr/NodeValue.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/expr/NodeValue.java @@ -18,7 +18,6 @@ package org.apache.jena.sparql.expr; -import static javax.xml.datatype.DatatypeConstants.*; import static org.apache.jena.datatypes.xsd.XSDDatatype.*; import static org.apache.jena.sparql.expr.ValueSpace.VSPACE_DIFFERENT; import static org.apache.jena.sparql.expr.ValueSpace.VSPACE_UNKNOWN; @@ -34,17 +33,12 @@ import javax.xml.datatype.XMLGregorianCalendar; import org.apache.jena.atlas.lib.DateTimeUtils; import org.apache.jena.atlas.logging.Log; -import org.apache.jena.datatypes.DatatypeFormatException; import org.apache.jena.datatypes.RDFDatatype; import org.apache.jena.datatypes.TypeMapper; -import org.apache.jena.datatypes.xsd.XSDDatatype; import org.apache.jena.ext.xerces.DatatypeFactoryInst; import org.apache.jena.graph.Node; import org.apache.jena.graph.NodeFactory; import org.apache.jena.graph.TextDirection; -import org.apache.jena.graph.impl.LiteralLabel; -import org.apache.jena.sparql.ARQInternalErrorException; -import org.apache.jena.sparql.SystemARQ; import org.apache.jena.sparql.core.Var; import org.apache.jena.sparql.engine.ExecutionContext; import org.apache.jena.sparql.engine.binding.Binding; @@ -53,7 +47,11 @@ import org.apache.jena.sparql.function.FunctionEnv; import org.apache.jena.sparql.graph.NodeConst; import org.apache.jena.sparql.graph.NodeTransform; import org.apache.jena.sparql.serializer.SerializationContext; -import org.apache.jena.sparql.util.*; +import org.apache.jena.sparql.sse.SSE; +import org.apache.jena.sparql.util.FmtUtils; +import org.apache.jena.sparql.util.NodeFactoryExtra; +import org.apache.jena.sparql.util.NodeUtils; +import org.apache.jena.sparql.util.XSDNumUtils; import org.apache.jena.sys.JenaSystem; import org.apache.jena.vocabulary.RDF; import org.slf4j.Logger; @@ -63,56 +61,34 @@ public abstract class NodeValue extends ExprNode { static { JenaSystem.init(); } - // Maybe:: NodeValueStringLang - strings with language tag - - /* Naming: - * getXXX => plain accessor - * asXXX => force to the required thing if necessary. - * + /* * Implementation notes: * - * 1. There is little point delaying turning a node into its value - * because it has to be verified anyway (e.g. illegal literals). - * Because a NodeValue is being created, it is reasonably likely it - * is going to be used for it's value, so processing the datatype - * can be done at creation time where it is clearer. + * Delaying turning a value into a graph Node is + * valuable because intermediates, like the result of 2+3, will not + * be needed as nodes unless used for assignment. * - * 2. Conversely, delaying turning a value into a graph node is - * valuable because intermediates, like the result of 2+3, will not - * be needed as nodes unless assignment (and there is no assignment - * in SPARQL even if there is for ARQ). - * Node level operations like str() don't need a full node. + * Operations: + * See also NV* for NodeValue code. + * See also NodeValueCmp for comparison operations. + * See also XSDFuncOp for XQuery/XPath functions. + * See also NodeFunctions for RDF Term related functions. * - * 3. nodevalue.NodeFunctions contains the SPARQL builtin implementations. - * nodevalue.XSDFuncOp contains the implementation of the XQuery/Xpath - * functions and operations. - * See also NodeUtils. * - * 4. Note that SPARQL "=" is "known to be sameValueAs". Similarly "!=" is - * known to be different. - * - * 5. To add a new number type: - * Add sub type into nodevalue.NodeValueXXX - * Must implement .hashCode() and .equals() based on value. - * Add Functions.add/subtract/etc code and compareNumeric - * Add to compare code - * Fix TestExprNumeric - * Write lots of tests. - * Library code Maths1 and Maths2 for maths functions + * Note that SPARQL "=" is "known to be sameValueAs". + * Similarly "!=" is known to be different. */ /* * Effective boolean value rules. * boolean: value of the boolean * string: length(string) > 0 is true - * numeric: number != Nan && number != 0 is true + * numeric: number != NaN && number != 0 is true * ref: http://www.w3.org/TR/xquery/#dt-ebv */ private static Logger log = LoggerFactory.getLogger(NodeValue.class); - // ---- Constants and initializers / public - public static boolean VerboseWarnings = true; public static boolean VerboseExceptions = false; @@ -146,7 +122,6 @@ public abstract class NodeValue extends ExprNode private Node node = null; // Null used when a value has not been turned into a Node. - // Don't create direct - the static builders manage the value/node relationship protected NodeValue() { super(); } protected NodeValue(Node n) { super(); node = n; } @@ -155,24 +130,16 @@ public abstract class NodeValue extends ExprNode return Set.of(); } -// protected makeNodeValue(NodeValue nv) -// { -// if ( v.isNode() ) { ... } -// if ( v.isBoolean() ) { ... } -// if ( v.isInteger() ) { ... } -// if ( v.isDouble() ) { ... } -// if ( v.isDecimal() ) { ... } -// if ( v.isString() ) { ... } -// if ( v.isDate() ) { ... } -// } - - // ---------------------------------------------------------------- - // ---- Construct NodeValue without a graph node. - - /** Convenience operation - parse a string to produce a NodeValue - common namespaces like xsd: are built-in */ + /** + * Convenience operation,primarily for tests. + * Parse a string (using {@link SSE}) to produce a NodeValue - common namespaces like xsd: are built-in + */ public static NodeValue parse(String string) { return makeNode(NodeFactoryExtra.parseNode(string)); } + // ---------------------------------------------------------------- + // ---- Construct NodeValue without a graph node (calculated later if needed) + public static NodeValue makeInteger(long i) { return new NodeValueInteger(BigInteger.valueOf(i)); } @@ -221,6 +188,8 @@ public abstract class NodeValue extends ExprNode public static NodeValue makeDate(String lexicalForm) { return NodeValue.makeNode(lexicalForm, XSDdate); } + /** @deprecated Use a XMLGregorianCalendar */ + @Deprecated(forRemoval=true) public static NodeValue makeDateTime(Calendar cal) { String lex = DateTimeUtils.calendarToXSDDateTimeString(cal); return NodeValue.makeNode(lex, XSDdateTime); @@ -229,9 +198,11 @@ public abstract class NodeValue extends ExprNode public static NodeValue makeDateTime(XMLGregorianCalendar cal) { String lex = cal.toXMLFormat(); Node node = NodeFactory.createLiteralDT(lex, XSDdateTime); - return NodeValueDateTime.create(lex, node); + return new NodeValueDateTime(cal, node); } + /** @deprecated Use a XMLGregorianCalendar */ + @Deprecated(forRemoval=true) public static NodeValue makeDate(Calendar cal) { String lex = DateTimeUtils.calendarToXSDDateString(cal); return NodeValue.makeNode(lex, XSDdate); @@ -240,7 +211,7 @@ public abstract class NodeValue extends ExprNode public static NodeValue makeDate(XMLGregorianCalendar cal) { String lex = cal.toXMLFormat(); Node node = NodeFactory.createLiteralDT(lex, XSDdate); - return NodeValueDateTime.create(lex, node); + return new NodeValueDateTime(cal, node); } public static NodeValue makeDuration(String lexicalForm) @@ -249,9 +220,6 @@ public abstract class NodeValue extends ExprNode public static NodeValue makeDuration(Duration duration) { return new NodeValueDuration(duration); } - public static NodeValue makeNodeDuration(Duration duration, Node node) - { return new NodeValueDuration(duration, node); } - public static NodeValue makeBoolean(boolean b) { return b ? NodeValue.TRUE : NodeValue.FALSE; } @@ -267,7 +235,7 @@ public abstract class NodeValue extends ExprNode public static NodeValue makeNode(String lexicalForm, RDFDatatype dtype) { Node n = NodeFactory.createLiteralDT(lexicalForm, dtype); - return NodeValue.makeNode(n); + return makeNode(n); } // Convenience - knows that lang tags aren't allowed with datatypes. @@ -302,64 +270,46 @@ public abstract class NodeValue extends ExprNode public static NodeValue makeNodeBoolean(boolean b) { return b ? NodeValue.TRUE : NodeValue.FALSE; } - public static NodeValue makeNodeBoolean(String lexicalForm) { - return makeNode(lexicalForm, null, XSDboolean.getURI()); - } + public static NodeValue makeNodeBoolean(String lexicalForm) + { return makeNode(lexicalForm, XSDboolean); } - public static NodeValue makeNodeInteger(long v) { - return makeNode(Long.toString(v), null, XSDinteger.getURI()); - } + public static NodeValue makeNodeInteger(long v) + { return makeNode(Long.toString(v), XSDinteger); } - public static NodeValue makeNodeInteger(String lexicalForm) { - return makeNode(lexicalForm, null, XSDinteger.getURI()); - } + public static NodeValue makeNodeInteger(String lexicalForm) + { return makeNode(lexicalForm, XSDinteger); } - public static NodeValue makeNodeFloat(float f) { - return makeNode(XSDNumUtils.stringForm(f), null, XSDfloat.getURI()); - } + public static NodeValue makeNodeFloat(float f) + { return makeNode(XSDNumUtils.stringForm(f), XSDfloat); } - public static NodeValue makeNodeFloat(String lexicalForm) { - return makeNode(lexicalForm, null, XSDfloat.getURI()); - } + public static NodeValue makeNodeFloat(String lexicalForm) + { return makeNode(lexicalForm, XSDfloat); } - public static NodeValue makeNodeDouble(double v) { - return makeNode(XSDNumUtils.stringForm(v), null, XSDdouble.getURI()); - } + public static NodeValue makeNodeDouble(double v) + { return makeNode(XSDNumUtils.stringForm(v), XSDdouble); } - public static NodeValue makeNodeDouble(String lexicalForm) { - return makeNode(lexicalForm, null, XSDdouble.getURI()); - } + public static NodeValue makeNodeDouble(String lexicalForm) + { return makeNode(lexicalForm, XSDdouble); } public static NodeValue makeNodeDecimal(BigDecimal decimal) { String lex = XSDNumUtils.stringFormatARQ(decimal); - return makeNode(lex, XSDDatatype.XSDdecimal); + return makeNode(lex, XSDdecimal); } - public static NodeValue makeNodeDecimal(String lexicalForm) { - return makeNode(lexicalForm, null, XSDdecimal.getURI()); - } - - public static NodeValue makeNodeString(String string) { - return makeNode(string, null, (String)null); - } + public static NodeValue makeNodeDecimal(String lexicalForm) + { return makeNode(lexicalForm, XSDdecimal); } - public static NodeValue makeNodeDateTime(Calendar date) { - String lex = DateTimeUtils.calendarToXSDDateTimeString(date); - return makeNode(lex, XSDdateTime); - } + public static NodeValue makeNodeString(String string) + { return makeNode(string, XSDstring); } - public static NodeValue makeNodeDateTime(String lexicalForm) { - return makeNode(lexicalForm, XSDdateTime); - } + public static NodeValue makeNodeDateTime(String lexicalForm) + { return makeNode(lexicalForm, XSDdateTime); } - public static NodeValue makeNodeDate(Calendar date) { - String lex = DateTimeUtils.calendarToXSDDateString(date); - return makeNode(lex, XSDdate); - } + public static NodeValue makeNodeDate(String lexicalForm) + { return makeNode(lexicalForm, XSDdate); } - public static NodeValue makeNodeDate(String lexicalForm) { - return makeNode(lexicalForm, XSDdate); - } + public static NodeValue makeNodeDuration(Duration duration, Node node) + { return new NodeValueDuration(duration, node); } // ---------------------------------------------------------------- // ---- Expr interface @@ -437,7 +387,7 @@ public abstract class NodeValue extends ExprNode * if known to be different values, throw ExprEvalException otherwise */ public static boolean sameValueAs(NodeValue nv1, NodeValue nv2) { - return NodeValueCmp.sameValueAs(nv1, nv2); + return NVCompare.sameValueAs(nv1, nv2); } /** @@ -453,7 +403,7 @@ public abstract class NodeValue extends ExprNode * the two NodeValues are known to be the same, else throw ExprEvalException */ public static boolean notSameValueAs(NodeValue nv1, NodeValue nv2) { - return NodeValueCmp.notSameValueAs(nv1, nv2); + return NVCompare.notSameValueAs(nv1, nv2); } // ---------------------------------------------------------------- @@ -468,7 +418,7 @@ public abstract class NodeValue extends ExprNode */ public static int compare(NodeValue nv1, NodeValue nv2) { //return NodeValueCompare.compare(nv1, nv2); - int x = NodeValueCmp.compareByValue(nv1, nv2); + int x = NVCompare.compareByValue(nv1, nv2); if ( x == Expr.CMP_INDETERMINATE || x == Expr.CMP_UNEQUAL ) throw new ExprNotComparableException(null); return x; @@ -484,7 +434,7 @@ public abstract class NodeValue extends ExprNode */ public static int compareAlways(NodeValue nv1, NodeValue nv2) { //return NodeValueCompare.compareAlways(nv1, nv2); - return NodeValueCmp.compareAlways(nv1, nv2); + return NVCompare.compareAlways(nv1, nv2); } // ---------------------------------------------------------------- @@ -514,6 +464,10 @@ public abstract class NodeValue extends ExprNode // ---------------------------------------------------------------- // ---- Subclass operations + // "isX" means "can it be used where X is expected", according to the XPath/XQuery Functions and Operator rules. + // e.g. NodeValueFloat.isDouble is true. + + public boolean isLiteral() { return getNode() == null || getNode().isLiteral(); } public boolean isBoolean() { return false; } public boolean isString() { return false; } @@ -529,23 +483,11 @@ public abstract class NodeValue extends ExprNode public boolean hasDateTime() { return isDateTime() || isDate() || isTime() || isGYear() || isGYearMonth() || isGMonth() || isGMonthDay() || isGDay(); } public boolean isDateTime() { return false; } public boolean isDate() { return false; } - public boolean isLiteral() { return getNode() == null || getNode().isLiteral(); } public boolean isTime() { return false; } - public boolean isDuration() { return false; } - public boolean isYearMonthDuration() { - if ( ! isDuration() ) return false; - Duration dur = getDuration(); - return ( dur.isSet(YEARS) || dur.isSet(MONTHS) ) && - ! dur.isSet(DAYS) && ! dur.isSet(HOURS) && ! dur.isSet(MINUTES) && ! dur.isSet(SECONDS); - } - - public boolean isDayTimeDuration() { - if ( ! isDuration() ) return false; - Duration dur = getDuration(); - return !dur.isSet(YEARS) && ! dur.isSet(MONTHS) && - ( dur.isSet(DAYS) || dur.isSet(HOURS) || dur.isSet(MINUTES) || dur.isSet(SECONDS) ); - } + public boolean isDuration() { return false; } + public boolean isYearMonthDuration() { return isDuration() && NVOps.isYearMonthDuration(getDuration()); } + public boolean isDayTimeDuration() { return isDuration() && NVOps.isDayTimeDuration(getDuration()); } public boolean isGYear() { return false; } public boolean isGYearMonth() { return false; } @@ -608,175 +550,14 @@ public abstract class NodeValue extends ExprNode return new NodeValueLang(node); } - NodeValue nv = _setByValue(node); - if ( nv != null ) - return nv; - return new NodeValueNode(node); - } - - // Jena code does not have these types (yet) - private static final String dtXSDprecisionDecimal = XSD+"#precisionDecimal"; - - // Returns null for unrecognized literal. - private static NodeValue _setByValue(Node node) { - // This should not happen. - // nodeToNodeValue should have been dealt with it. - if ( NodeUtils.hasLang(node) ) { - if ( NodeUtils.hasLangDir(node) ) - return new NodeValueLangDir(node); - return new NodeValueLang(node); - } - LiteralLabel lit = node.getLiteral(); - RDFDatatype datatype = lit.getDatatype(); - - // Quick check. - // Only XSD supported. - // And (for testing) roman numerals. - String datatypeURI = datatype.getURI(); - if ( !datatypeURI.startsWith(xsdNamespace) && !SystemARQ.EnableRomanNumerals ) { - // Not XSD. - return null; - } - - String lex = lit.getLexicalForm(); - - try { // DatatypeFormatException - should not happen - if ( XSDstring.isValidLiteral(lit) ) - // String - plain or xsd:string, or derived datatype. - return new NodeValueString(lex, node); - - // Otherwise xsd:string is like any other unknown datatype. - // Ditto literals with language tags (which are handled by nodeToNodeValue) - - // isValidLiteral is a value test - not a syntactic test. - // This makes a difference in that "1"^^xsd:decimal" is a - // valid literal for xsd:integer (all other cases are subtypes of xsd:integer) - // which we want to become integer anyway). - - // Order here is promotion order integer-decimal-float-double - - // XSD allows whitespace. Java String.trim removes too much - // so must test for validity on the untrimmed lexical form. - String lexTrimmed = lex.trim(); - - if ( ! datatype.equals(XSDdecimal) ) { // decimal covers integer, and all derived types, lexical forms - // XSD integer and derived types - if ( XSDinteger.isValidLiteral(lit) ) { - if ( ! lit.isWellFormed() ) - return null; - // BigInteger does not accept such whitespace. - String s = lexTrimmed; - if ( s.startsWith("+") ) - // BigInteger does not accept leading "+" - s = s.substring(1); - // Includes subtypes (int, byte, postiveInteger etc). - // NB Known to be valid for type by now - BigInteger integer = new BigInteger(s); - return new NodeValueInteger(integer, node); - } - } - - if ( datatype.equals(XSDdecimal) && XSDdecimal.isValidLiteral(lit) ) { - BigDecimal decimal = new BigDecimal(lexTrimmed); - return new NodeValueDecimal(decimal, node); - } - - if ( datatype.equals(XSDfloat) && XSDfloat.isValidLiteral(lit) ) { - // NB If needed, call to floatValue, then assign to double. - // Gets 1.3f != 1.3d right - float f = ((Number)lit.getValue()).floatValue(); - return new NodeValueFloat(f, node); - } - - if ( datatype.equals(XSDdouble) && XSDdouble.isValidLiteral(lit) ) { - double d = ((Number)lit.getValue()).doubleValue(); - return new NodeValueDouble(d, node); - } - - if ( datatype.equals(XSDboolean) && XSDboolean.isValidLiteral(lit) ) { - boolean b = (Boolean) lit.getValue(); - return new NodeValueBoolean(b, node); - } - - if ( datatype.equals(XSDdateTime) || datatype.equals(XSDdateTimeStamp) ) { - if ( ! XSDdateTime.isValid(lex) ) - return null; - return NodeValueDateTime.create(lexTrimmed, node); - } - - if ( datatype.equals(XSDdate) && XSDdate.isValidLiteral(lit) ) { - return NodeValueDateTime.create(lexTrimmed, node); - } - - if ( datatype.equals(XSDtime) && XSDtime.isValidLiteral(lit) ) { - return NodeValueDateTime.create(lexTrimmed, node); - } - - if ( datatype.equals(XSDgYear) && XSDgYear.isValidLiteral(lit) ) { - return NodeValueDateTime.create(lexTrimmed, node); - } - if ( datatype.equals(XSDgYearMonth) && XSDgYearMonth.isValidLiteral(lit) ) { - return NodeValueDateTime.create(lexTrimmed, node); - } - if ( datatype.equals(XSDgMonth) && XSDgMonth.isValidLiteral(lit) ) { - return NodeValueDateTime.create(lexTrimmed, node); - } - - if ( datatype.equals(XSDgMonthDay) && XSDgMonthDay.isValidLiteral(lit) ) { - return NodeValueDateTime.create(lexTrimmed, node); - } - if ( datatype.equals(XSDgDay) && XSDgDay.isValidLiteral(lit) ) { - return NodeValueDateTime.create(lexTrimmed, node); - } - - // -- Duration - - if ( datatype.equals(XSDduration) && XSDduration.isValid(lex) ) { - Duration duration = xmlDatatypeFactory.newDuration(lexTrimmed); - return new NodeValueDuration(duration, node); - } - - if ( datatype.equals(XSDyearMonthDuration) && XSDyearMonthDuration.isValid(lex) ) { - Duration duration = xmlDatatypeFactory.newDuration(lexTrimmed); - return new NodeValueDuration(duration, node); - } - if ( datatype.equals(XSDdayTimeDuration) && XSDdayTimeDuration.isValid(lex) ) { - Duration duration = xmlDatatypeFactory.newDuration(lexTrimmed); - return new NodeValueDuration(duration, node); - } - - // If wired into the TypeMapper via RomanNumeralDatatype.enableAsFirstClassDatatype -// if ( RomanNumeralDatatype.get().isValidLiteral(lit) ) -// { -// int i = ((RomanNumeral)lit.getValue()).intValue(); -// return new NodeValueInteger(i); -// } - - // Not wired in - if ( SystemARQ.EnableRomanNumerals ) - { - if ( lit.getDatatypeURI().equals(RomanNumeralDatatype.get().getURI()) ) - { - Object obj = RomanNumeralDatatype.get().parse(lexTrimmed); - if ( obj instanceof Integer ) - return new NodeValueInteger(((Integer)obj).longValue()); - if ( obj instanceof RomanNumeral ) - return new NodeValueInteger( ((RomanNumeral)obj).intValue() ); - throw new ARQInternalErrorException("DatatypeFormatException: Roman numeral is unknown class"); - } - } - - } catch (DatatypeFormatException ex) - { - // Should have been caught earlier by special test in nodeToNodeValue - throw new ARQInternalErrorException("DatatypeFormatException: "+lit, ex); - } - return null; + // Includes creating NodeValueNode for ill-formed literals. + NodeValue nv = NVFactory.create(node); + return nv; } // ---------------------------------------------------------------- - // Point to catch all exceptions. + /** Common point for exceptions during evaluation. */ public static void raise(ExprException ex) { throw ex; } @@ -787,7 +568,6 @@ public abstract class NodeValue extends ExprNode private void forceToNode() { if ( node == null ) node = asNode(); - if ( node == null ) raise(new ExprEvalException("Not a node: " + this)); } @@ -813,7 +593,6 @@ public abstract class NodeValue extends ExprNode // Convert to a string - usually overridden. public String asString() { - // Do not call .toString() forceToNode(); return NodeFunctions.str(node); } @@ -825,13 +604,13 @@ public abstract class NodeValue extends ExprNode @Override public boolean equals(Expr other, boolean bySyntax) { + // Java equals, not "same value" or "same term" if ( other == null ) return false; if ( this == other ) return true; // This is the equality condition Jena uses - lang tags are different by case. if ( ! ( other instanceof NodeValue nv) ) return false; return asNode().equals(nv.asNode()); - // Not NodeFunctions.sameTerm (which smooshes language tags by case) } public abstract void visit(NodeValueVisitor visitor); diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/NodeValueDateTime.java b/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/NodeValueDateTime.java index 06850afdd6..503153eb37 100644 --- a/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/NodeValueDateTime.java +++ b/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/NodeValueDateTime.java @@ -30,21 +30,6 @@ public class NodeValueDateTime extends NodeValue { final private XMLGregorianCalendar datetime; - /** Lex - caller removes leading and trailing whitespace. */ - public static NodeValueDateTime create(String lex, Node n) { - // Java bug : gMonth with a timezone of Z causes IllegalArgumentException - if ( XSDgMonth.equals(n.getLiteralDatatype()) ) { - if ( lex.endsWith("Z") ) { - lex = lex.substring(0, lex.length() - 1); - XMLGregorianCalendar datetime = NodeValue.xmlDatatypeFactory.newXMLGregorianCalendar(lex); - datetime.setTimezone(0); - return new NodeValueDateTime(datetime, n); - } - } - XMLGregorianCalendar datetime = NodeValue.xmlDatatypeFactory.newXMLGregorianCalendar(lex); - return new NodeValueDateTime(datetime, n); - } - public NodeValueDateTime(XMLGregorianCalendar datetime, Node n) { super(n); this.datetime = datetime; diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TS_Expr.java b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TS_Expr.java index c89caac691..2d70825d94 100644 --- a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TS_Expr.java +++ b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TS_Expr.java @@ -27,7 +27,8 @@ import org.apache.jena.sparql.expr.nodevalue.TestNodeValueSortKey; @Suite @SelectClasses({ - TestNodeValue.class + TestNVFactory.class + , TestNodeValue.class , TestExpressions.class , TestExpressions2.class , TestExpressions3.class diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestComparison.java b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestComparison.java index 01dda93ac2..c9a53aaa71 100644 --- a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestComparison.java +++ b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestComparison.java @@ -77,7 +77,7 @@ public class TestComparison { private static void compare(NodeValue nv1, NodeValue nv2, int expected) { try { - int cmp = NodeValueCmp.compareByValue(nv1, nv2); + int cmp = NVCompare.compareByValue(nv1, nv2); assertEquals(expected, cmp); } catch (ExprNotComparableException ex) { throw ex; diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestNVFactory.java b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestNVFactory.java new file mode 100644 index 0000000000..35c330e357 --- /dev/null +++ b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestNVFactory.java @@ -0,0 +1,457 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jena.sparql.expr; + +import static java.lang.String.format; +import static org.apache.jena.datatypes.xsd.XSDDatatype.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import org.apache.jena.datatypes.RDFDatatype; +import org.apache.jena.graph.Node; +import org.apache.jena.graph.NodeFactory; +import org.apache.jena.sparql.expr.nodevalue.*; +import org.apache.jena.vocabulary.RDF; + +@TestMethodOrder(MethodOrderer.MethodName.class) +public class TestNVFactory { + + // basic testing + private <T extends NodeValue> NodeValue test(String lex, RDFDatatype dt, Class<T> expectedClass) { + Node n = NodeFactory.createLiteralDT(lex, dt); + NodeValue nv = NVFactory.create(n); + assertTrue(expectedClass.isInstance(nv), + format("Expected instance of %s but got %s", expectedClass.getSimpleName(), nv.getClass().getSimpleName())); + assertEquals(lex, nv.asNode().getLiteralLexicalForm()); + return nv; + } + + @Test + public void testDecimal_1() { + NodeValue nv = test("12.34", XSDdecimal, NodeValueDecimal.class); + assertTrue(nv.isDecimal()); + assertNotNull(nv.getDecimal()); + assertTrue(nv.isFloat()); + assertTrue(nv.isDouble()); + assertFalse(nv.isInteger()); + assertTrue(nv.isDecimal()); + } + + @Test + public void testDecimal_2() { + NodeValue nv = test("12", XSDdecimal, NodeValueDecimal.class); + assertTrue(nv.isDecimal()); + assertNotNull(nv.getDecimal()); + } + + @Test + public void testDecimal_3() { + NodeValue nv = test("-12", XSDdecimal, NodeValueDecimal.class); + assertTrue(nv.isDecimal()); + assertNotNull(nv.getDecimal()); + } + + @Test + public void testFloat() { + NodeValue nv = test("12.34", XSDfloat, NodeValueFloat.class); + assertTrue(nv.isFloat()); + assertTrue(nv.isDouble()); + assertFalse(nv.isInteger()); + assertFalse(nv.isDecimal()); + } + + @Test + public void testDouble() { + NodeValue nv = test("12.34", XSDdouble, NodeValueDouble.class); + assertFalse(nv.isFloat()); + assertTrue(nv.isDouble()); + assertFalse(nv.isInteger()); + assertFalse(nv.isDecimal()); + } + + @Test + public void testInteger() { + NodeValue nv = test("123", XSDinteger, NodeValueInteger.class); + assertTrue(nv.isFloat()); + assertTrue(nv.isDouble()); + assertTrue(nv.isInteger()); + assertTrue(nv.isDecimal()); + } + + @Test + public void testBoolean() { + NodeValue nv1 = test("true", XSDboolean, NodeValueBoolean.class); + NodeValue nv2 = test("false", XSDboolean, NodeValueBoolean.class); + } + + @Test + public void testString() { + NodeValue nv = test("test string", XSDstring, NodeValueString.class); + } + + // Keep original language string tests as they have special handling + @Test + public void testLangString() { + String lex = "hello"; + String lang = "en"; + Node n = NodeFactory.createLiteralLang(lex, lang); + NodeValue nv = NVFactory.create(n); + assertTrue(NodeValueLang.class.isInstance(nv)); + assertEquals(lex, nv.getString()); + assertEquals(lang, nv.getLang()); + } + + @Test + public void testDirLangString() { + String lex = "hello"; + String lang = "en-GB"; + String ltr = "ltr"; + Node n = NodeFactory.createLiteralDirLang(lex, lang, ltr); + NodeValue nv = NVFactory.create(RDF.dtDirLangString, n); + assertTrue(NodeValueLangDir.class.isInstance(nv)); + assertEquals(lex, nv.getString()); + assertEquals(lang, nv.getLang()); + } + + @Test + public void testInvalidIntegerLiteral() { + NodeValue nv = test("not-an-integer", XSDinteger, NodeValueNode.class); + assertNotNull(nv); + assertFalse(nv.isNumber()); + assertFalse(nv.isInteger()); + } + + @Test + public void testDerivedInt() { + NodeValue nv = test("123", XSDint, NodeValueInteger.class); + assertTrue(nv.isInteger()); + } + + @Test + public void testDerivedLong() { + NodeValue nv = test("123", XSDlong, NodeValueInteger.class); + assertTrue(nv.isInteger()); + } + + @Test + public void testDerivedShort() { + NodeValue nv = test("123", XSDshort, NodeValueInteger.class); + assertTrue(nv.isInteger()); + } + + @Test + public void testDerivedByte() { + NodeValue nv = test("123", XSDbyte, NodeValueInteger.class); + assertTrue(nv.isInteger()); + } + + @Test + public void testDerivedUnsignedByte() { + NodeValue nv = test("123", XSDunsignedByte, NodeValueInteger.class); + assertTrue(nv.isInteger()); + } + + @Test + public void testDerivedUnsignedShort() { + NodeValue nv = test("123", XSDunsignedShort, NodeValueInteger.class); + assertTrue(nv.isInteger()); + } + + @Test + public void testDerivedUnsignedInt() { + NodeValue nv = test("123", XSDunsignedInt, NodeValueInteger.class); + assertTrue(nv.isInteger()); + } + + @Test + public void testDerivedUnsignedLong() { + NodeValue nv = test("123", XSDunsignedLong, NodeValueInteger.class); + assertTrue(nv.isInteger()); + } + + @Test + public void testDerivedNonPositiveInteger() { + NodeValue nv = test("-123", XSDnonPositiveInteger, NodeValueInteger.class); + assertTrue(nv.isInteger()); + } + + @Test + public void testDerivedNonNegativeInteger() { + NodeValue nv = test("123", XSDnonNegativeInteger, NodeValueInteger.class); + assertTrue(nv.isInteger()); + } + + @Test + public void testDerivedPositiveInteger() { + NodeValue nv = test("123", XSDpositiveInteger, NodeValueInteger.class); + assertTrue(nv.isInteger()); + } + + @Test + public void testDerivedNegativeInteger() { + NodeValue nv = test("-123", XSDnegativeInteger, NodeValueInteger.class); + assertTrue(nv.isInteger()); + } + + // Date and time types + + + @Test + public void testDateTimeType() { + NodeValue nv = test("2025-11-03T12:00:00", XSDdateTime, NodeValueDateTime.class); + assertTrue(nv.isDateTime()); + assertFalse(nv.isDate()); + assertFalse(nv.isTime()); + } + + @Test + public void testDate() { + NodeValue nv = test("2025-11-03", XSDdate, NodeValueDateTime.class); + assertFalse(nv.isDateTime()); + assertTrue(nv.isDate()); + assertFalse(nv.isTime()); + } + + @Test + public void testTime() { + NodeValue nv = test("12:00:00", XSDtime, NodeValueDateTime.class); + assertFalse(nv.isDateTime()); + assertFalse(nv.isDate()); + assertTrue(nv.isTime()); + } + + @Test + public void testDateTimeStamp() { + NodeValue nv = test("2025-11-03T12:00:00Z", XSDdateTimeStamp, NodeValueDateTime.class); + assertTrue(nv.isDateTime()); + } + + @Test + public void testGDay() { + NodeValue nv = test("---03", XSDgDay, NodeValueDateTime.class); + assertFalse(nv.isDateTime()); + assertTrue(nv.isGDay()); + assertFalse(nv.isGMonthDay()); + } + + @Test + public void testGMonth() { + NodeValue nv = test("--11", XSDgMonth, NodeValueDateTime.class); + assertFalse(nv.isDateTime()); + assertTrue(nv.isGMonth()); + assertFalse(nv.isGMonthDay()); + } + + @Test + public void testGMonthDay() { + NodeValue nv = test("--11-03", XSDgMonthDay, NodeValueDateTime.class); + assertFalse(nv.isDateTime()); + assertFalse(nv.isGMonth()); + assertTrue(nv.isGMonthDay()); + } + + @Test + public void testGYear() { + NodeValue nv = test("2025", XSDgYear, NodeValueDateTime.class); + assertFalse(nv.isDateTime()); + assertTrue(nv.isGYear()); + assertFalse(nv.isGYearMonth()); + } + + @Test + public void testGYearMonth() { + NodeValue nv = test("2025-11", XSDgYearMonth, NodeValueDateTime.class); + assertFalse(nv.isDateTime()); + assertFalse(nv.isGYear()); + assertTrue(nv.isGYearMonth()); + } + + // Keep original duration types test as it has special assertions + @Test + public void testDuration() { + NodeValue nv = test("P1Y2M3DT4H5M6S", XSDduration, NodeValueDuration.class); + assertTrue(nv.isDuration()); + assertFalse(nv.isDayTimeDuration()); + assertFalse(nv.isYearMonthDuration()); + } + + @Test + public void testDuration_2() { + NodeValue nv = test("P3DT4H5M6S", XSDduration, NodeValueDuration.class); + assertTrue(nv.isDuration()); + assertTrue(nv.isDayTimeDuration()); + assertFalse(nv.isYearMonthDuration()); + } + + @Test + public void testDuration_3() { + NodeValue nv = test("P1Y2M", XSDduration, NodeValueDuration.class); + assertTrue(nv.isDuration()); + assertFalse(nv.isDayTimeDuration()); + assertTrue(nv.isYearMonthDuration()); + } + + @Test + public void testDayTimeDuration() { + NodeValue nv = test("P3DT4H5M6S", XSDdayTimeDuration, NodeValueDuration.class); + assertTrue(nv.isDuration()); + assertTrue(nv.isDayTimeDuration()); + assertFalse(nv.isYearMonthDuration()); + } + + @Test + public void testYearMonthDuration() { + NodeValue nv = test("P1Y2M", XSDyearMonthDuration, NodeValueDuration.class); + assertTrue(nv.isDuration()); + assertFalse(nv.isDayTimeDuration()); + assertTrue(nv.isYearMonthDuration()); + } + + @Test + public void testInvalidDayTimeDuration() { + NodeValue nv = test("P1Y", XSDdayTimeDuration, NodeValueNode.class); + assertFalse(nv.isDuration()); + assertFalse(nv.isDayTimeDuration()); + assertFalse(nv.isYearMonthDuration()); + } + + @Test + public void testInvalidYearMonthDuration() { + NodeValue nv = test("PT3H", XSDyearMonthDuration, NodeValueNode.class); + assertFalse(nv.isDuration()); + assertFalse(nv.isDayTimeDuration()); + assertFalse(nv.isYearMonthDuration()); + } + + @Test + public void testInvalidDateFormat_1() { + test("not-a-date", XSDdate, NodeValueNode.class); + } + + @Test + public void testInvalidDateFormat_2() { + // Invalid for xsd:date + test("2025-11-03T15:04:30", XSDdate, NodeValueNode.class); + } + + @Test + public void testInvalidTimeFormat() { + // Invalid hours + test("25:00:00", XSDtime, NodeValueNode.class); + } + + @Test + public void testInvalidDateTimeFormat_1() { + // Invalid valid for month, day, and hour + NodeValue nv = test("2025-13-45T25:00:00", XSDdateTime, NodeValueNode.class); + } + + @Test + public void testInvalidDateTimeFormat_2() { + // Invalid for xsd:dateTime + NodeValue nv = test("2025-12-25", XSDdateTime, NodeValueNode.class); + } + + @Test + public void testInvalidDecimalFormat() { + // Multiple decimal points + NodeValue nv = test("12.34.56", XSDdecimal, NodeValueNode.class); + assertFalse(nv.isInteger()); + } + + @Test + public void testInvalidFloatFormat() { + // Incomplete exponent + NodeValue nv = test("12.34e", XSDfloat, NodeValueNode.class); + assertFalse(nv.isInteger()); + } + + @Test + public void testInvalidIntegerFormat() { + // Decimal not allowed for integer + NodeValue nv = test("123.45", XSDinteger, NodeValueNode.class); + assertFalse(nv.isInteger()); + } + + @Test + public void testInvalidDurationFormat() { + // Invalid duration designator + test("P1X", XSDduration, NodeValueNode.class); + } + + @Test + public void testInvalidGMonthFormat() { + // Invalid month value + test("--13", XSDgMonth, NodeValueNode.class); + } + + @Test + public void testInvalidGDayFormat() { + // Invalid day value + test("---32", XSDgDay, NodeValueNode.class); + } + + @Test + public void testInvalidPositiveIntegerNegativeValue() { + // Negative value not allowed for positive integer + NodeValue nv = test("-123", XSDpositiveInteger, NodeValueNode.class); + assertFalse(nv.isInteger()); + } + + @Test + public void testInvalidUnsignedIntValue() { + // Negative value not allowed for unsigned + NodeValue nv = test("-1", XSDunsignedInt, NodeValueNode.class); + assertFalse(nv.isInteger()); + } + + @Test + public void testInvalidByteRange() { + // Outside byte range (-128 to 127) + NodeValue nv = test("128", XSDbyte, NodeValueNode.class); + assertFalse(nv.isInteger()); + } + + @Test + public void testInvalidUnsignedByteRange() { + // Outside unsigned byte range (0 to 255) + NodeValue nv = test("256", XSDunsignedByte, NodeValueNode.class); + assertFalse(nv.isInteger()); + } + + @Test + public void testInvalidNonNegativeInteger() { + // Negative value not allowed + NodeValue nv = test("-1", XSDnonNegativeInteger, NodeValueNode.class); + assertFalse(nv.isInteger()); + } + + @Test + public void testInvalidNonPositiveInteger() { + // Positive value not allowed + NodeValue nv = test("1", XSDnonPositiveInteger, NodeValueNode.class); + assertFalse(nv.isInteger()); + } +} \ No newline at end of file diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestNodeValue.java b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestNodeValue.java index 802f6784d1..ce093cc27c 100644 --- a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestNodeValue.java +++ b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestNodeValue.java @@ -23,6 +23,8 @@ import static org.junit.jupiter.api.Assertions.*; import java.math.BigDecimal; import java.util.*; +import javax.xml.datatype.XMLGregorianCalendar; + import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -163,19 +165,32 @@ public class TestNodeValue } @Test - public void testDateTime1() { + public void testDateTime1x() { + // Legacy + // Better to use XMLGregorianCalendar Calendar cal = new GregorianCalendar(); cal.setTimeZone(TimeZone.getTimeZone("GMT")); // Clear/ set all fields (milliseconds included). cal.setTimeInMillis(0); cal.set(2005, 01, 18, 20, 39, 10); // NB Months from 0, not 1 + @SuppressWarnings("removal") NodeValue v = NodeValue.makeDateTime(cal); assertTrue(v.isDateTime(), ()->"Not a dateTime: " + v); assertFalse(v.isDate(), ()->"A date: " + v); // DateTimes always have nodes because we used that to parse the thing. } + @Test + public void testDateTime1() { + // Better to use XMLGregorianCalendar + XMLGregorianCalendar cal = NodeValue.xmlDatatypeFactory.newXMLGregorianCalendar("2025-11-05T12:08:30.5Z"); + NodeValue v = NodeValue.makeDateTime(cal); + assertTrue(v.isDateTime(), ()->"Not a dateTime: " + v); + assertFalse(v.isDate(), ()->"A date: " + v); + } + + @Test public void testDateTime2() { NodeValue v = NodeValue.makeNodeDateTime("2005-02-18T20:39:10Z"); @@ -262,19 +277,26 @@ public class TestNodeValue } @Test - public void testDate1() { + public void testDate1x() { + // Legacy Calendar cal = new GregorianCalendar(); cal.setTimeZone(TimeZone.getTimeZone("GMT")); // Clear/ set all fields (milliseconds included). cal.setTimeInMillis(0); - // NB Months from 0, not 1 - // For a date, must be time = 00:00:00 cal.set(2005, 01, 18, 0, 0, 0); + @SuppressWarnings("removal") + NodeValue v = NodeValue.makeDate(cal); + assertTrue(v.isDate(), ()->"Not a date: " + v); + assertFalse(v.isDateTime(), ()->"A dateTime: " + v); + } + @Test + public void testDate1() { + // Better to use XMLGregorianCalendar + XMLGregorianCalendar cal = NodeValue.xmlDatatypeFactory.newXMLGregorianCalendar("2025-11-05"); NodeValue v = NodeValue.makeDate(cal); assertTrue(v.isDate(), ()->"Not a date: " + v); assertFalse(v.isDateTime(), ()->"A dateTime: " + v); - // DateTimes always have nodes because we used that to parse the thing. } @Test diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestSortOrdering.java b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestSortOrdering.java index 09c817fdd7..dc9c449923 100644 --- a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestSortOrdering.java +++ b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestSortOrdering.java @@ -181,9 +181,9 @@ public class TestSortOrdering { test(nv1, nv2, nv3); // And this should be <= order - int x12 = NodeValueCmp.compareWithOrdering(nv1, nv2); - int x23 = NodeValueCmp.compareWithOrdering(nv2, nv3); - int x13 = NodeValueCmp.compareWithOrdering(nv1, nv3); + int x12 = NVCompare.compareWithOrdering(nv1, nv2); + int x23 = NVCompare.compareWithOrdering(nv2, nv3); + int x13 = NVCompare.compareWithOrdering(nv1, nv3); boolean isTransitiveLE = isLE(x12) && isLE(x23) && isLE(x13); @@ -234,9 +234,9 @@ public class TestSortOrdering { * Test, return true if a transitive order of arguments holds (either "less than or equals" or "greater than or equal"). */ static boolean testWorker(NodeValue nv1, NodeValue nv2, NodeValue nv3) { - int x12 = NodeValueCmp.compareWithOrdering(nv1, nv2); - int x23 = NodeValueCmp.compareWithOrdering(nv2, nv3); - int x13 = NodeValueCmp.compareWithOrdering(nv1, nv3); + int x12 = NVCompare.compareWithOrdering(nv1, nv2); + int x23 = NVCompare.compareWithOrdering(nv2, nv3); + int x13 = NVCompare.compareWithOrdering(nv1, nv3); if ( isLE(x12) ) { if ( isLE(x23) ) { @@ -256,11 +256,11 @@ public class TestSortOrdering { } private static boolean isLE(NodeValue nv1, NodeValue nv2) { - return isLE(NodeValueCmp.compareWithOrdering(nv1, nv2)); + return isLE(NVCompare.compareWithOrdering(nv1, nv2)); } private static boolean isGE(NodeValue nv1, NodeValue nv2) { - return isGE(NodeValueCmp.compareWithOrdering(nv1, nv2)); + return isGE(NVCompare.compareWithOrdering(nv1, nv2)); } private static boolean isLE(int x) { diff --git a/jena-extras/jena-querybuilder/src/main/java/org/apache/jena/arq/querybuilder/rewriters/NodeValueRewriter.java b/jena-extras/jena-querybuilder/src/main/java/org/apache/jena/arq/querybuilder/rewriters/NodeValueRewriter.java index e157e33635..da807b3030 100644 --- a/jena-extras/jena-querybuilder/src/main/java/org/apache/jena/arq/querybuilder/rewriters/NodeValueRewriter.java +++ b/jena-extras/jena-querybuilder/src/main/java/org/apache/jena/arq/querybuilder/rewriters/NodeValueRewriter.java @@ -81,7 +81,7 @@ class NodeValueRewriter extends AbstractRewriter<NodeValue> implements NodeValue @Override public void visit(NodeValueDateTime nv) { - push(NodeValueDateTime.create(nv.getDateTime().toXMLFormat(), changeNode(nv.asNode()))); + push(new NodeValueDateTime(nv.getDateTime(), changeNode(nv.asNode()))); } @Override diff --git a/jena-extras/jena-querybuilder/src/test/java/org/apache/jena/arq/querybuilder/rewriters/NodeValueRewriterTest.java b/jena-extras/jena-querybuilder/src/test/java/org/apache/jena/arq/querybuilder/rewriters/NodeValueRewriterTest.java index f1f4ad16aa..3a3dfd32e9 100644 --- a/jena-extras/jena-querybuilder/src/test/java/org/apache/jena/arq/querybuilder/rewriters/NodeValueRewriterTest.java +++ b/jena-extras/jena-querybuilder/src/test/java/org/apache/jena/arq/querybuilder/rewriters/NodeValueRewriterTest.java @@ -188,7 +188,8 @@ public class NodeValueRewriterTest { // 2001-10-26T21:32:52 GregorianCalendar cal = new GregorianCalendar(2001, 10, 26, 21, 32, 52); Node n = NodeFactory.createLiteralByValue(cal, XSDDatatype.XSDdateTime); - NodeValue nv = NodeValueDateTime.create("2001-10-26T21:32:52", n); + @SuppressWarnings("removal") + NodeValue nv = NodeValue.makeDateTime(cal); nv.visit(rewriter); NodeValue result = rewriter.getResult(); assertEquals(nv, result);
