diff --git a/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDate.java b/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDate.java index 41aa70f06..25a82e557 100644 --- a/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDate.java +++ b/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDate.java @@ -19,6 +19,8 @@ public final class SimpleDate extends AbstractTemporalPointLiteral { @Serial private static final long serialVersionUID = 1L; + private static final DatatypeFactory DATATYPE_FACTORY = DatatypeFactory.newDefaultInstance(); + private final String label; @SuppressWarnings("serial") // Calendar implementations have VM-specific serialization; RDF is the portable representation. private final XMLGregorianCalendar calendar; @@ -36,7 +38,7 @@ public SimpleDate(XMLGregorianCalendar calendar) { public SimpleDate(String lexicalValue, IRI datatype) { super(datatype == null ? XSDDatatype.DATE.getIRI() : datatype); this.label = Objects.requireNonNull(lexicalValue, "lexicalValue"); - this.calendar = DatatypeFactory.newDefaultInstance().newXMLGregorianCalendar(lexicalValue); + this.calendar = DATATYPE_FACTORY.newXMLGregorianCalendar(lexicalValue); } @Override diff --git a/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDateTime.java b/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDateTime.java index c2277b83a..809bff030 100644 --- a/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDateTime.java +++ b/src/main/java/fr/inria/corese/core/next/data/api/term/SimpleDateTime.java @@ -19,6 +19,8 @@ public final class SimpleDateTime extends AbstractTemporalPointLiteral { @Serial private static final long serialVersionUID = 1L; + private static final DatatypeFactory DATATYPE_FACTORY = DatatypeFactory.newDefaultInstance(); + private final String label; @SuppressWarnings("serial") // Calendar implementations have VM-specific serialization; RDF is the portable representation. private final XMLGregorianCalendar calendar; @@ -36,7 +38,7 @@ public SimpleDateTime(XMLGregorianCalendar calendar) { public SimpleDateTime(String lexicalValue, IRI datatype) { super(datatype == null ? XSDDatatype.DATETIME.getIRI() : datatype); this.label = Objects.requireNonNull(lexicalValue, "lexicalValue"); - this.calendar = DatatypeFactory.newDefaultInstance().newXMLGregorianCalendar(lexicalValue); + this.calendar = DATATYPE_FACTORY.newXMLGregorianCalendar(lexicalValue); } @Override diff --git a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeCastExpressionEvaluator.java b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeCastExpressionEvaluator.java index 45125debc..12b8850ae 100644 --- a/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeCastExpressionEvaluator.java +++ b/src/main/java/fr/inria/corese/core/next/query/impl/sparql/bridge/NativeCastExpressionEvaluator.java @@ -150,7 +150,10 @@ private static String floatingString(Literal literal, double number) { } private static DatatypeValue dateTime(Literal value, NativeEvaluationContext context) { - if (value.getCoreDatatype() != XSDDatatype.STRING && value.getCoreDatatype() != XSDDatatype.DATETIME) { + if (value.getCoreDatatype() == XSDDatatype.DATETIME) { + return value; + } + if (value.getCoreDatatype() != XSDDatatype.STRING) { throw new QueryTypeErrorException("Cannot cast this datatype to xsd:dateTime"); } Literal result = context.values().createLiteral(whitespaceCollapsed(value.getLabel()), XSDDatatype.DATETIME.getIRI()); diff --git a/src/main/java/fr/inria/corese/core/sparql/datatype/CoreseDatatype.java b/src/main/java/fr/inria/corese/core/sparql/datatype/CoreseDatatype.java index c0a353413..7749e8605 100644 --- a/src/main/java/fr/inria/corese/core/sparql/datatype/CoreseDatatype.java +++ b/src/main/java/fr/inria/corese/core/sparql/datatype/CoreseDatatype.java @@ -129,20 +129,68 @@ public static IDatatype create(String valueJType, lang = lang.toLowerCase(); } switch (valueJType) { - case Cst.jTypeString: + case Cst.jTypeString -> { return new CoreseString(label); - case Cst.jTypeURI: + } + case Cst.jTypeURI -> { return new CoreseURI(label); - case Cst.jTypeLiteral: + } + case Cst.jTypeLiteral -> { return new CoreseLiteral(label, lang); - case Cst.jTypeJSON: + } + case Cst.jTypeJSON -> { return DatatypeMap.json(label); + } + default -> { + // Non-primitive datatypes fall through to constructBuiltin + } } String display = (datatype == null) ? valueJType : datatype; - CoreseDatatype o = null; - Class valueClass = null; + + try { + o = constructBuiltin(valueJType, label); + } catch (CoreseDatatypeException | RuntimeException e) { + if (cast) { + throw failure; + } else { + throw new CoreseDatatypeException(e, display, label); + } + } + + if (o == null) { + o = constructReflective(valueJType, label, display, cast); + } + + o.setLang(lang); + o.setDatatype(datatype); + return o.typeCheck(); + } + + private static CoreseDatatype constructBuiltin(String valueJType, String label) throws CoreseDatatypeException { + return switch (valueJType) { + case Cst.jTypeBoolean -> new CoreseBoolean(label); + case Cst.jTypeInteger -> new CoreseInteger(label); + case Cst.jTypeGenericInteger -> new CoreseGenericInteger(label); + case Cst.jTypeDecimal -> new CoreseDecimal(label); + case Cst.jTypeFloat -> new CoreseFloat(label); + case Cst.jTypeDouble -> new CoreseDouble(label); + case Cst.jTypeDate -> new CoreseDate(label); + case Cst.jTypeDateTime -> new CoreseDateTime(label); + case Cst.jTypeDay -> new CoreseDay(label); + case Cst.jTypeMonth -> new CoreseMonth(label); + case Cst.jTypeYear -> new CoreseYear(label); + case Cst.jTypeUndef -> new CoreseUndefLiteral(label); + case Cst.jTypeXMLString -> new CoreseXMLLiteral(label); + case Cst.jTypeGeneric -> new CoreseGeneric(label); + default -> null; + }; + } + + private static CoreseDatatype constructReflective(String valueJType, String label, String display, boolean cast) + throws CoreseDatatypeException { + Class> valueClass; try { valueClass = Class.forName(valueJType); } catch (ClassNotFoundException e) { @@ -150,25 +198,18 @@ public static IDatatype create(String valueJType, } try { - Class[] argClass = {label.getClass()}; + Class>[] argClass = {label.getClass()}; String[] arg = {label}; - o = (CoreseDatatype) valueClass.getConstructor(argClass).newInstance((Object[]) arg); + return (CoreseDatatype) valueClass.getConstructor(argClass).newInstance((Object[]) arg); } catch (ClassCastException | IllegalAccessException | InstantiationException | NoSuchMethodException e) { throw new CoreseDatatypeException(e, valueJType, label); } catch (java.lang.reflect.InvocationTargetException e) { - // CoreseDatatypeException arrives here if (cast) { - // to speed up sparql cast we throw a static exception - // because it will be transformed to null throw failure; } else { throw new CoreseDatatypeException(e, display, label); } } - - o.setLang(lang); - o.setDatatype(datatype); - return o.typeCheck(); } static boolean isNumber(String name, String cname) { diff --git a/src/main/java/fr/inria/corese/core/sparql/datatype/CoreseDate.java b/src/main/java/fr/inria/corese/core/sparql/datatype/CoreseDate.java index dbbe6eb77..53e8f2efd 100644 --- a/src/main/java/fr/inria/corese/core/sparql/datatype/CoreseDate.java +++ b/src/main/java/fr/inria/corese/core/sparql/datatype/CoreseDate.java @@ -14,6 +14,10 @@ public class CoreseDate extends CoreseDatatype { private final XMLGregorianCalendar cal; + private final long epochMillis; + private final boolean hasZone; + private final boolean hasFastEpoch; + private static final java.util.TimeZone UTC_ZONE = java.util.TimeZone.getTimeZone("UTC"); private static final String TODAY = "today"; private static final IDatatype.Datatype code = IDatatype.Datatype.DATE; private static final CoreseURI datatype = new CoreseURI(RDF.xsddate); @@ -22,11 +26,19 @@ public class CoreseDate extends CoreseDatatype { public CoreseDate() { this.cal = DatatypeMap.newXMLGregorianCalendar(); setLabel(cal.toString()); + this.hasZone = (this.cal.getTimezone() != DatatypeConstants.FIELD_UNDEFINED); + long em = computeSafeEpoch(this.cal, this.hasZone); + this.epochMillis = em; + this.hasFastEpoch = (em != Long.MIN_VALUE); } public CoreseDate(XMLGregorianCalendar cal) { this.cal = cal; setLabel(cal.toString()); + this.hasZone = (this.cal.getTimezone() != DatatypeConstants.FIELD_UNDEFINED); + long em = computeSafeEpoch(this.cal, this.hasZone); + this.epochMillis = em; + this.hasFastEpoch = (em != Long.MIN_VALUE); } public CoreseDate(String label) { @@ -37,6 +49,25 @@ public CoreseDate(String label) { this.cal = DatatypeMap.newXMLGregorianCalendar(label); setLabel(label); } + this.hasZone = (this.cal.getTimezone() != DatatypeConstants.FIELD_UNDEFINED); + long em = computeSafeEpoch(this.cal, this.hasZone); + this.epochMillis = em; + this.hasFastEpoch = (em != Long.MIN_VALUE); + } + + private static long computeSafeEpoch(XMLGregorianCalendar calendar, boolean zone) { + try { + return computeEpoch(calendar, zone); + } catch (IllegalArgumentException | IllegalStateException | ArithmeticException e) { + return Long.MIN_VALUE; + } + } + + private static long computeEpoch(XMLGregorianCalendar calendar, boolean zone) { + if (zone) { + return calendar.normalize().toGregorianCalendar().getTimeInMillis(); + } + return calendar.toGregorianCalendar(UTC_ZONE, java.util.Locale.ROOT, null).getTimeInMillis(); } @@ -161,88 +192,76 @@ void check(IDatatype icod) throws CoreseDatatypeException { } } + public int compareTo(CoreseDate dt) throws CoreseDatatypeException { + if (this.hasFastEpoch && dt.hasFastEpoch && this.hasZone == dt.hasZone) { + if (this.epochMillis < dt.epochMillis) { + return -1; + } else if (this.epochMillis > dt.epochMillis) { + return 1; + } + } + return this.compare(this.getCalendar(), dt.getCalendar()); + } + + @Override + public IDatatype cast(String target) { + if (RDF.xsddate.equals(target) && getCode() == IDatatype.Datatype.DATE) { + return this; + } + return super.cast(target); + } + @Override public int compare(IDatatype iod) throws CoreseDatatypeException { - switch (iod.getCode()) { - case DATE: - case DATETIME: - CoreseDate dt = (CoreseDate) iod; - XMLGregorianCalendar cal1 = this.getCalendar(); - XMLGregorianCalendar cal2 = dt.getCalendar(); - return this.compare(cal1, cal2); + if (iod.getCode() == IDatatype.Datatype.DATE || iod.getCode() == IDatatype.Datatype.DATETIME) { + return this.compareTo((CoreseDate) iod); } throw failure(); } @Override public boolean less(IDatatype iod) throws CoreseDatatypeException { - switch (iod.getCode()) { - case DATE: - case DATETIME: - this.check(iod); - CoreseDate dt = (CoreseDate) iod; - XMLGregorianCalendar cal1 = this.getCalendar(); - XMLGregorianCalendar cal2 = dt.getCalendar(); - return this.compare(cal1, cal2) < 0; + if (iod.getCode() == IDatatype.Datatype.DATE || iod.getCode() == IDatatype.Datatype.DATETIME) { + this.check(iod); + return this.compareTo((CoreseDate) iod) < 0; } throw failure(); } @Override public boolean lessOrEqual(IDatatype iod) throws CoreseDatatypeException { - switch (iod.getCode()) { - case DATE: - case DATETIME: - this.check(iod); - CoreseDate dt = (CoreseDate) iod; - XMLGregorianCalendar cal1 = this.getCalendar(); - XMLGregorianCalendar cal2 = dt.getCalendar(); - return this.compare(cal1, cal2) <= 0; + if (iod.getCode() == IDatatype.Datatype.DATE || iod.getCode() == IDatatype.Datatype.DATETIME) { + this.check(iod); + return this.compareTo((CoreseDate) iod) <= 0; } throw failure(); } @Override public boolean greater(IDatatype iod) throws CoreseDatatypeException { - switch (iod.getCode()) { - case DATE: - case DATETIME: - this.check(iod); - CoreseDate dt = (CoreseDate) iod; - XMLGregorianCalendar cal1 = this.getCalendar(); - XMLGregorianCalendar cal2 = dt.getCalendar(); - return this.compare(cal1, cal2) > 0; + if (iod.getCode() == IDatatype.Datatype.DATE || iod.getCode() == IDatatype.Datatype.DATETIME) { + this.check(iod); + return this.compareTo((CoreseDate) iod) > 0; } throw failure(); } @Override public boolean greaterOrEqual(IDatatype iod) throws CoreseDatatypeException { - switch (iod.getCode()) { - case DATE: - case DATETIME: - this.check(iod); - CoreseDate dt = (CoreseDate) iod; - XMLGregorianCalendar cal1 = this.getCalendar(); - XMLGregorianCalendar cal2 = dt.getCalendar(); - return this.compare(cal1, cal2) >= 0; + if (iod.getCode() == IDatatype.Datatype.DATE || iod.getCode() == IDatatype.Datatype.DATETIME) { + this.check(iod); + return this.compareTo((CoreseDate) iod) >= 0; } throw failure(); } @Override public boolean equalsWE(IDatatype iod) throws CoreseDatatypeException { - switch (iod.getCode()) { - case DATE: - case DATETIME: - CoreseDate dt = (CoreseDate) iod; - XMLGregorianCalendar cal1 = this.getCalendar(); - XMLGregorianCalendar cal2 = dt.getCalendar(); - return this.compare(cal1, cal2) == 0; - - case URI: - case BLANK: case TRIPLE: - return false; + if (iod.getCode() == IDatatype.Datatype.DATE || iod.getCode() == IDatatype.Datatype.DATETIME) { + return this.compareTo((CoreseDate) iod) == 0; + } + if (iod.isURI() || iod.isBlank() || iod.isTriple()) { + return false; } throw failure(); } diff --git a/src/main/java/fr/inria/corese/core/sparql/datatype/CoreseDateTime.java b/src/main/java/fr/inria/corese/core/sparql/datatype/CoreseDateTime.java index c81cb53da..ccfdc8c76 100755 --- a/src/main/java/fr/inria/corese/core/sparql/datatype/CoreseDateTime.java +++ b/src/main/java/fr/inria/corese/core/sparql/datatype/CoreseDateTime.java @@ -31,4 +31,12 @@ public IDatatype.Datatype getCode() { return code; } + @Override + public IDatatype cast(String target) { + if (RDF.xsddateTime.equals(target)) { + return this; + } + return super.cast(target); + } + } diff --git a/src/main/java/fr/inria/corese/core/sparql/datatype/CoreseStringableImpl.java b/src/main/java/fr/inria/corese/core/sparql/datatype/CoreseStringableImpl.java index 0cc15351b..49eebf718 100644 --- a/src/main/java/fr/inria/corese/core/sparql/datatype/CoreseStringableImpl.java +++ b/src/main/java/fr/inria/corese/core/sparql/datatype/CoreseStringableImpl.java @@ -1,12 +1,7 @@ package fr.inria.corese.core.sparql.datatype; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import fr.inria.corese.core.sparql.api.IDatatype; import fr.inria.corese.core.sparql.exceptions.CoreseDatatypeException; -import org.slf4j.Marker; -import org.slf4j.MarkerFactory; /** *
@@ -33,26 +28,26 @@ */ public abstract class CoreseStringableImpl extends CoreseDatatype { - /** - * logger from log4j - */ - private static final Logger logger = LoggerFactory.getLogger(CoreseStringableImpl.class); - private final Marker fatal = MarkerFactory.getMarker("FATAL"); - static IDatatype.Datatype code = IDatatype.Datatype.STRINGABLE; public static int count = 0; String value = ""; - public CoreseStringableImpl() { + private transient volatile IDatatype cachedDateTime; + private transient volatile IDatatype cachedDate; + private static final CoreseUndefLiteral CAST_FAILURE = new CoreseUndefLiteral("CAST_FAILURE"); + + protected CoreseStringableImpl() { } - public CoreseStringableImpl(String str) { + protected CoreseStringableImpl(String str) { setValue(str); } @Override public void setValue(String str) { this.value = str; + this.cachedDateTime = null; + this.cachedDate = null; } @Override @@ -67,20 +62,48 @@ public String getLabel() { */ @Override public IDatatype cast(String target) { + if (RDF.xsddateTime.equals(target)) { + return castDateTime(); + } + if (RDF.xsddate.equals(target)) { + return castDate(); + } if (target.equals(RDF.xsdboolean)) { - try { - Float f = Float.parseFloat(getLabel()); - if (f == 0) { - return CoreseBoolean.FALSE; - } else if (f == 1) { - return CoreseBoolean.TRUE; - } else { - return null; - } - } catch (NumberFormatException e) { - return super.cast(target); + return castBoolean(target); + } + return super.cast(target); + } + + private IDatatype castDateTime() { + IDatatype cached = cachedDateTime; + if (cached != null) { + return (cached == CAST_FAILURE) ? null : cached; + } + IDatatype res = super.cast(RDF.xsddateTime); + cachedDateTime = (res != null) ? res : CAST_FAILURE; + return res; + } + + private IDatatype castDate() { + IDatatype cached = cachedDate; + if (cached != null) { + return (cached == CAST_FAILURE) ? null : cached; + } + IDatatype res = super.cast(RDF.xsddate); + cachedDate = (res != null) ? res : CAST_FAILURE; + return res; + } + + private IDatatype castBoolean(String target) { + try { + float f = Float.parseFloat(getLabel()); + if (f == 0) { + return CoreseBoolean.FALSE; + } else if (f == 1) { + return CoreseBoolean.TRUE; } - } else { + return null; + } catch (NumberFormatException e) { return super.cast(target); } } @@ -107,7 +130,7 @@ public boolean isTrue() throws CoreseDatatypeException { @Override public boolean booleanValue() { - return getLabel().length() > 0; + return !getLabel().isEmpty(); } @Override diff --git a/src/test/java/fr/inria/corese/core/datatype/CoreseDateTest.java b/src/test/java/fr/inria/corese/core/datatype/CoreseDateTest.java index 6f120a876..a4894d3f5 100644 --- a/src/test/java/fr/inria/corese/core/datatype/CoreseDateTest.java +++ b/src/test/java/fr/inria/corese/core/datatype/CoreseDateTest.java @@ -11,6 +11,7 @@ import fr.inria.corese.core.sparql.datatype.CoreseDateTime; import fr.inria.corese.core.sparql.datatype.CoreseDecimal; import fr.inria.corese.core.sparql.datatype.DatatypeMap; +import fr.inria.corese.core.sparql.datatype.RDF; import fr.inria.corese.core.sparql.datatype.XSD; public class CoreseDateTest { @@ -148,4 +149,22 @@ public void getTimezone() throws DatatypeConfigurationException { assertEquals(DatatypeMap.newLiteral("+PT11H"), date.getTimezone()); } + @Test + public void testCastIdentityAndConversions() { + IDatatype dateTime = DatatypeMap.newDateTime("2024-03-15T10:30:00Z"); + IDatatype date = DatatypeMap.newDate("2024-03-15Z"); + + org.junit.jupiter.api.Assertions.assertSame(dateTime, dateTime.cast(RDF.xsddateTime)); + org.junit.jupiter.api.Assertions.assertSame(date, date.cast(RDF.xsddate)); + + IDatatype dateFromDateTime = dateTime.cast(RDF.xsddate); + assertEquals(IDatatype.Datatype.DATE, dateFromDateTime.getCode()); + assertEquals(RDF.xsddate, dateFromDateTime.getDatatypeURI()); + + IDatatype str = DatatypeMap.newLiteral("2024-03-15T10:30:00Z"); + IDatatype cast1 = str.cast(RDF.xsddateTime); + IDatatype cast2 = str.cast(RDF.xsddateTime); + org.junit.jupiter.api.Assertions.assertSame(cast1, cast2); + } + }