diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/jpa/jpql/ExpressionBuilderVisitor.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/jpa/jpql/ExpressionBuilderVisitor.java index a24d3a415f9..5e337a34a78 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/jpa/jpql/ExpressionBuilderVisitor.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/jpa/jpql/ExpressionBuilderVisitor.java @@ -23,11 +23,14 @@ // IBM - Bug 537795: CASE THEN and ELSE scalar expression Constants should not be casted to CASE operand type // 04/21/2022: Tomas Kraus // - Issue 1474: Update JPQL Grammar for Jakarta Persistence 2.2, 3.0 and 3.1 +// - Issue 317: Implement LOCAL DATE, LOCAL TIME and LOCAL DATETIME. package org.eclipse.persistence.internal.jpa.jpql; import java.sql.Date; import java.sql.Time; import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -106,6 +109,8 @@ import org.eclipse.persistence.jpa.jpql.parser.KeywordExpression; import org.eclipse.persistence.jpa.jpql.parser.LengthExpression; import org.eclipse.persistence.jpa.jpql.parser.LikeExpression; +import org.eclipse.persistence.jpa.jpql.parser.LocalDateTime; +import org.eclipse.persistence.jpa.jpql.parser.LocalExpression; import org.eclipse.persistence.jpa.jpql.parser.LocateExpression; import org.eclipse.persistence.jpa.jpql.parser.LowerExpression; import org.eclipse.persistence.jpa.jpql.parser.MathDoubleExpression; @@ -1265,6 +1270,39 @@ public void visit(LikeExpression expression) { type[0] = Boolean.class; } + @Override + public void visit(LocalExpression expression) { + expression.getDateType().accept(this); + // Type is set by child expression + } + + // LocalDateTime visitor helper method + private void buildLocalDate() { + queryExpression = queryContext.getBaseExpression().localDate(); + type[0] = LocalDate.class; + } + + // LocalDateTime visitor helper method + private void buildLocalTime() { + queryExpression = queryContext.getBaseExpression().localTime(); + type[0] = LocalTime.class; + } + + // LocalDateTime visitor helper method + private void buildLocalDateTime() { + queryExpression = queryContext.getBaseExpression().localDateTime(); + type[0] = LocalDateTime.class; + } + + @Override + public void visit(LocalDateTime expression) { + expression.runByType( + this::buildLocalDate, + this::buildLocalTime, + this::buildLocalDateTime + ); + } + @Override public void visit(LocateExpression expression) { diff --git a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/jpa/jpql/TypeResolver.java b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/jpa/jpql/TypeResolver.java index 33aa01f3207..73c7f82e7c8 100644 --- a/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/jpa/jpql/TypeResolver.java +++ b/foundation/org.eclipse.persistence.core/src/main/java/org/eclipse/persistence/internal/jpa/jpql/TypeResolver.java @@ -14,6 +14,7 @@ // Oracle - initial API and implementation // 04/21/2022: Tomas Kraus // - Issue 1474: Update JPQL Grammar for Jakarta Persistence 2.2, 3.0 and 3.1 +// - Issue 317: Implement LOCAL DATE, LOCAL TIME and LOCAL DATETIME. package org.eclipse.persistence.internal.jpa.jpql; import java.lang.reflect.Field; @@ -23,6 +24,8 @@ import java.sql.Date; import java.sql.Time; import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -755,6 +758,21 @@ public void visit(LikeExpression expression) { type = Boolean.class; } + @Override + public void visit(LocalExpression expression) { + // Visit the child expression (LocalDateTime) in order to set the type + expression.getDateType().accept(this); + } + + @Override + public void visit(LocalDateTime expression) { + type = expression.getValueByType( + () -> LocalDate.class, + () -> LocalTime.class, + () -> LocalDateTime.class + ); + } + @Override public void visit(LocateExpression expression) { type = Integer.class; diff --git a/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/jpql/TestDateTimeFunctions.java b/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/jpql/TestDateTimeFunctions.java new file mode 100644 index 00000000000..52101002cb8 --- /dev/null +++ b/jpa/eclipselink.jpa.test.jse/src/it/java/org/eclipse/persistence/jpa/test/jpql/TestDateTimeFunctions.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +// Contributors: +// 04/21/2022: Tomas Kraus +// - Issue 317: Implement LOCAL DATE, LOCAL TIME and LOCAL DATETIME. +package org.eclipse.persistence.jpa.test.jpql; + +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.temporal.ChronoUnit; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; + +import org.eclipse.persistence.jpa.test.criteria.model.DateTimeEntity; +import org.eclipse.persistence.jpa.test.framework.DDLGen; +import org.eclipse.persistence.jpa.test.framework.Emf; +import org.eclipse.persistence.jpa.test.framework.EmfRunner; +import org.eclipse.persistence.jpa.test.framework.Property; +import org.eclipse.persistence.logging.AbstractSessionLog; +import org.eclipse.persistence.logging.SessionLog; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(EmfRunner.class) +public class TestDateTimeFunctions { + + @Emf(createTables = DDLGen.DROP_CREATE, + classes = { + DateTimeEntity.class + }, + properties = { + @Property(name = "eclipselink.cache.shared.default", value = "false"), + // This property remaps String from VARCHAR->NVARCHAR(or db equivalent) + @Property(name = "eclipselink.target-database-properties", + value = "UseNationalCharacterVaryingTypeForString=true"), + }) + private EntityManagerFactory emf; + + private final LocalDateTime[] TS = { + LocalDateTime.of(1970, 1, 1, 1, 11, 11), + LocalDateTime.of(1970, 1, 1, 0, 0, 0, 0) + }; + + private final DateTimeEntity[] ENTITY = { + new DateTimeEntity(1, TS[0].toLocalTime(), TS[0].toLocalDate(), TS[0]), + new DateTimeEntity(2, TS[0].toLocalTime(), TS[0].toLocalDate(), TS[0]), + new DateTimeEntity(3, TS[0].toLocalTime(), TS[0].toLocalDate(), TS[0]), + new DateTimeEntity(4, TS[1].toLocalTime(), TS[1].toLocalDate(), TS[1]) + }; + + // Database vs. Java timezone offset in seconds. Must be applied to LocalDateTime calculations. + private long dbOffset = 0; + + // Update database vs. Java timezone offset using current database time. + private void updateDbOffset() { + final EntityManager em = emf.createEntityManager(); + try { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery(LocalTime.class); + cq.select(cb.localTime()); + Root entity = cq.from(DateTimeEntity.class); + cq.where(cb.equal(entity.get("id"), 1)); + LocalTime dbTime = em.createQuery(cq).getSingleResult(); + LocalTime javaTime = LocalTime.now(); + this.dbOffset = dbTime.truncatedTo(ChronoUnit.SECONDS).toSecondOfDay() - javaTime.truncatedTo(ChronoUnit.SECONDS).toSecondOfDay(); + } catch (Throwable t) { + AbstractSessionLog.getLog().log(SessionLog.WARNING, "Can't update DB offset: " + t.getMessage()); + } finally { + if (em.getTransaction().isActive()) { + em.getTransaction().rollback(); + } + em.close(); + } + } + + @Before + public void setup() { + final EntityManager em = emf.createEntityManager(); + try { + em.getTransaction().begin(); + for (DateTimeEntity e : ENTITY) { + em.persist(e); + } + em.flush(); + em.getTransaction().commit(); + } finally { + if (em.getTransaction().isActive()) { + em.getTransaction().rollback(); + } + em.close(); + } + updateDbOffset(); + } + + @After + public void cleanup() { + final EntityManager em = emf.createEntityManager(); + try { + em.getTransaction().begin(); + em.createQuery("DELETE FROM DateTimeEntity e").executeUpdate(); + em.flush(); + em.getTransaction().commit(); + } finally { + if (em.getTransaction().isActive()) { + em.getTransaction().rollback(); + } + em.close(); + } + } + + // Test JPQL with localTime in WHERE condition. + @Test + public void testCriteriaQueryWhereLocalTime() { + final EntityManager em = emf.createEntityManager(); + try { + TypedQuery query = em.createQuery( + "SELECT e.id FROM DateTimeEntity e WHERE e.time < LOCAL TIME AND e.id = :id", + Integer.class); + query.setParameter("id", 4); + em.getTransaction().begin(); + query.getSingleResult(); + em.getTransaction().commit(); + } finally { + if (em.getTransaction().isActive()) { + em.getTransaction().rollback(); + } + em.close(); + } + } + + // Test JPQL with localDate in WHERE condition. + @Test + public void testCriteriaQueryWhereLocalDate() { + final EntityManager em = emf.createEntityManager(); + try { + TypedQuery query = em.createQuery( + "SELECT e.id FROM DateTimeEntity e WHERE e.date < LOCAL DATE AND e.id = :id", + Integer.class); + query.setParameter("id", 4); + em.getTransaction().begin(); + query.getSingleResult(); + em.getTransaction().commit(); + } finally { + if (em.getTransaction().isActive()) { + em.getTransaction().rollback(); + } + em.close(); + } + } + + // Test JPQL with localDateTime in WHERE condition. + @Test + public void testCriteriaQueryWhereLocalDateTime() { + final EntityManager em = emf.createEntityManager(); + try { + TypedQuery query = em.createQuery( + "SELECT e.id FROM DateTimeEntity e WHERE e.datetime < LOCAL DATETIME AND e.id = :id", + Integer.class); + query.setParameter("id", 4); + em.getTransaction().begin(); + query.getSingleResult(); + em.getTransaction().commit(); + } finally { + if (em.getTransaction().isActive()) { + em.getTransaction().rollback(); + } + em.close(); + } + } + +} diff --git a/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/AbstractExpressionVisitor.java b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/AbstractExpressionVisitor.java index 6999be16330..0925d355298 100644 --- a/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/AbstractExpressionVisitor.java +++ b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/AbstractExpressionVisitor.java @@ -14,6 +14,7 @@ // Oracle - initial API and implementation // 04/21/2022: Tomas Kraus // - Issue 1474: Update JPQL Grammar for Jakarta Persistence 2.2, 3.0 and 3.1 +// - Issue 317: Implement LOCAL DATE, LOCAL TIME and LOCAL DATETIME. package org.eclipse.persistence.jpa.jpql.parser; /** @@ -199,13 +200,21 @@ public void visit(KeywordExpression expression) { public void visit(LengthExpression expression) { } + @Override + public void visit(LocalExpression expression) { + } + + @Override + public void visit(LocalDateTime expression) { + } + @Override public void visit(LikeExpression expression) { } - @Override - public void visit(LocateExpression expression) { - } + @Override + public void visit(LocateExpression expression) { + } @Override public void visit(LowerExpression expression) { diff --git a/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/AnonymousExpressionVisitor.java b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/AnonymousExpressionVisitor.java index 28c97b10e65..aa93579d181 100644 --- a/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/AnonymousExpressionVisitor.java +++ b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/AnonymousExpressionVisitor.java @@ -14,6 +14,7 @@ // Oracle - initial API and implementation // 04/21/2022: Tomas Kraus // - Issue 1474: Update JPQL Grammar for Jakarta Persistence 2.2, 3.0 and 3.1 +// - Issue 317: Implement LOCAL DATE, LOCAL TIME and LOCAL DATETIME. package org.eclipse.persistence.jpa.jpql.parser; /** @@ -253,6 +254,16 @@ public void visit(LikeExpression expression) { visit((Expression) expression); } + @Override + public void visit(LocalExpression expression) { + visit((Expression) expression); + } + + @Override + public void visit(LocalDateTime expression) { + visit((Expression) expression); + } + @Override public void visit(LocateExpression expression) { visit((Expression) expression); diff --git a/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/Expression.java b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/Expression.java index f8468ee8d3a..ae6bd40d73e 100644 --- a/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/Expression.java +++ b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/Expression.java @@ -14,6 +14,7 @@ // Oracle - initial API and implementation // 04/21/2022: Tomas Kraus // - Issue 1474: Update JPQL Grammar for Jakarta Persistence 2.2, 3.0 and 3.1 +// - Issue 317: Implement LOCAL DATE, LOCAL TIME and LOCAL DATETIME. package org.eclipse.persistence.jpa.jpql.parser; import org.eclipse.persistence.jpa.jpql.utility.iterable.ListIterable; @@ -167,6 +168,16 @@ public interface Expression { */ String CURRENT_TIMESTAMP = "CURRENT_TIMESTAMP"; + /** + * The constant for 'DATE'. + */ + String DATE = "DATE"; + + /** + * The constant for 'DATETIME'. + */ + String DATETIME = "DATETIME"; + /** * The constant for 'DELETE'. */ @@ -434,6 +445,11 @@ public interface Expression { */ String LN = "LN"; + /** + * The constant for 'LOCAL'. + */ + String LOCAL = "LOCAL"; + /** * The constant for 'LOCATE'. */ @@ -727,6 +743,11 @@ public interface Expression { */ String THEN = "THEN"; + /** + * The constant for 'TIME'. + */ + String TIME = "TIME"; + /** * The constant for 'TIMESTAMP'. * diff --git a/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/ExpressionVisitor.java b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/ExpressionVisitor.java index b77b4e99ec8..5a5ee4f7888 100644 --- a/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/ExpressionVisitor.java +++ b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/ExpressionVisitor.java @@ -14,6 +14,7 @@ // Oracle - initial API and implementation // 04/21/2022: Tomas Kraus // - Issue 1474: Update JPQL Grammar for Jakarta Persistence 2.2, 3.0 and 3.1 +// - Issue 317: Implement LOCAL DATE, LOCAL TIME and LOCAL DATETIME. package org.eclipse.persistence.jpa.jpql.parser; /** @@ -323,6 +324,20 @@ public interface ExpressionVisitor { */ void visit(LikeExpression expression); + /** + * Visits the {@link LocalExpression} expression. + * + * @param expression The {@link Expression} to visit + */ + void visit(LocalExpression expression); + + /** + * Visits the {@link LocalDateTime} expression. + * + * @param expression The {@link Expression} to visit + */ + void visit(LocalDateTime expression); + /** * Visits the {@link LocateExpression} expression. * diff --git a/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/JPQLGrammar3_1.java b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/JPQLGrammar3_1.java index 22dee550691..10e004bc313 100644 --- a/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/JPQLGrammar3_1.java +++ b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/JPQLGrammar3_1.java @@ -13,18 +13,23 @@ // Contributors: // 04/21/2022: Tomas Kraus // - Issue 1474: Update JPQL Grammar for Jakarta Persistence 2.2, 3.0 and 3.1 +// - Issue 317: Implement LOCAL DATE, LOCAL TIME and LOCAL DATETIME. package org.eclipse.persistence.jpa.jpql.parser; import org.eclipse.persistence.jpa.jpql.ExpressionTools; import org.eclipse.persistence.jpa.jpql.JPAVersion; import static org.eclipse.persistence.jpa.jpql.parser.Expression.CEILING; +import static org.eclipse.persistence.jpa.jpql.parser.Expression.DATE; +import static org.eclipse.persistence.jpa.jpql.parser.Expression.DATETIME; import static org.eclipse.persistence.jpa.jpql.parser.Expression.FLOOR; import static org.eclipse.persistence.jpa.jpql.parser.Expression.EXP; import static org.eclipse.persistence.jpa.jpql.parser.Expression.LN; +import static org.eclipse.persistence.jpa.jpql.parser.Expression.LOCAL; import static org.eclipse.persistence.jpa.jpql.parser.Expression.POWER; import static org.eclipse.persistence.jpa.jpql.parser.Expression.ROUND; import static org.eclipse.persistence.jpa.jpql.parser.Expression.SIGN; +import static org.eclipse.persistence.jpa.jpql.parser.Expression.TIME; /** * This {@link JPQLGrammar} provides support for parsing JPQL queries defined in Jakarta Persistence 3.1. @@ -46,6 +51,14 @@ * extract_datetime_field := EXTRACT(datetime_field FROM datetime_expression) * * datetime_field := identification_variable + * + * functions_returning_datetime ::= CURRENT_DATE | CURRENT_TIME | CURRENT_TIMESTAMP | + * LOCAL local_datetime_type | + * extract_datetime_par + * + * local_datetime_type ::= DATE | ..... matches Java java.time.LocalDate + * TIME | ..... matches Java java.time.LocalTime + * DATETIME ..... matches Java java.time.LocalDateTime * */ public class JPQLGrammar3_1 extends AbstractJPQLGrammar { @@ -117,17 +130,22 @@ public String getProviderVersion() { protected void initializeBNFs() { registerBNF(new InternalPowerExpressionBNF()); registerBNF(new InternalRoundExpressionBNF()); + registerBNF(new LocalDateTypeBNF()); } @Override protected void initializeExpressionFactories() { - registerFunctionExpressionFactory(new MathExpressionFactory.Ceiling(), CEILING); - registerFunctionExpressionFactory(new MathExpressionFactory.Exp(), EXP); - registerFunctionExpressionFactory(new MathExpressionFactory.Floor(), FLOOR); - registerFunctionExpressionFactory(new MathExpressionFactory.Ln(), LN); - registerFunctionExpressionFactory(new MathExpressionFactory.Power(), POWER); - registerFunctionExpressionFactory(new MathExpressionFactory.Round(), ROUND); - registerFunctionExpressionFactory(new MathExpressionFactory.Sign(), SIGN); + registerExpressionFactory(new MathExpressionFactory.Ceiling(), FunctionsReturningNumericsBNF.ID, CEILING); + registerExpressionFactory(new MathExpressionFactory.Exp(), FunctionsReturningNumericsBNF.ID, EXP); + registerExpressionFactory(new MathExpressionFactory.Floor(), FunctionsReturningNumericsBNF.ID, FLOOR); + registerExpressionFactory(new MathExpressionFactory.Ln(), FunctionsReturningNumericsBNF.ID, LN); + registerExpressionFactory(new MathExpressionFactory.Power(), FunctionsReturningNumericsBNF.ID, POWER); + registerExpressionFactory(new MathExpressionFactory.Round(), FunctionsReturningNumericsBNF.ID, ROUND); + registerExpressionFactory(new MathExpressionFactory.Sign(), FunctionsReturningNumericsBNF.ID, SIGN); + + registerExpressionFactory( + new LocalExpressionFactory(), FunctionsReturningDatetimeBNF.ID, LocalExpressionFactory.ID); + registerFactory(new LocalDateTypeFactory()); } @Override @@ -139,6 +157,11 @@ protected void initializeIdentifiers() { registerFunctionIdentifier(POWER); registerFunctionIdentifier(ROUND); registerFunctionIdentifier(SIGN); + + registerFunctionIdentifier(LOCAL); + registerFunctionIdentifier(DATE); + registerFunctionIdentifier(TIME); + registerFunctionIdentifier(DATETIME); } @Override @@ -147,13 +170,14 @@ public String toString() { } // Register math function expression factory - private void registerFunctionExpressionFactory(MathExpressionFactory factory, String identifier) { + private void registerExpressionFactory( + final ExpressionFactory factory, final String queryBNFId, final String identifier) { registerFactory(factory); - addChildFactory(FunctionsReturningNumericsBNF.ID, identifier); + addChildFactory(queryBNFId, identifier); } - // Register role and version of math function identifier. - private void registerFunctionIdentifier(String identifier) { + // Register role and version of function identifier. + private void registerFunctionIdentifier(final String identifier) { registerIdentifierRole(identifier, IdentifierRole.FUNCTION); registerIdentifierVersion(identifier, JPAVersion.VERSION_3_1); } diff --git a/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/LocalDateTime.java b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/LocalDateTime.java new file mode 100644 index 00000000000..cdb36ea48c6 --- /dev/null +++ b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/LocalDateTime.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +// Contributors: +// 04/21/2022: Tomas Kraus +// - Issue 317: Implement LOCAL DATE, LOCAL TIME and LOCAL DATETIME. +package org.eclipse.persistence.jpa.jpql.parser; + +import java.util.Collection; +import java.util.List; +import java.util.function.Supplier; + +import org.eclipse.persistence.jpa.jpql.WordParser; + +/** + * The {@code DATE | TIME | DATETIME} argument of {@code LOCAL} local_datetime_type expression. + *
+ * Jakarta Persistence 3.1: + *
BNF: + * local_datetime_type ::= DATE | ..... matches Java java.time.LocalDate + * TIME | ..... matches Java java.time.LocalTime + * DATETIME ..... matches Java java.time.LocalDateTime + *
+ */ +public class LocalDateTime extends AbstractExpression { + + /** + * Local date/time type identifier. + */ + private enum Identifier { + /** "DATE" type identifier. */ + DATE("date"), + /** "DATETIME" type identifier. */ + DATETIME("datetime"), + /** "TIME" type identifier. */ + TIME("time"); + + private final String name; + + Identifier(final String name) { + this.name = name; + } + + /** + * Convert local date/time text identifier to {@link Identifier}. + * Conversion is case insensitive. + * + * @param name local date/time text identifier + * @return {@link Identifier} matching local date/time text identifier + * or {@code null} when identifier is unknown. + */ + private static final Identifier getIdentifier(final String name) { + switch(name.toUpperCase()) { + case Expression.DATE: + return DATE; + case Expression.TIME: + return TIME; + case Expression.DATETIME: + return DATETIME; + default: + return null; + } + } + + /** + * Parse identifier (DATE/DATETIME/TIME) at current parser position. + * Determines proper identifier with minimal cost. Input always contains DATE | DATETIME | TIME + * so no exact matching is required + * + * @param wordParser source JPQL parser + * @return local date/time type identifier + */ + private static Identifier parse(final WordParser wordParser) { + final int position = wordParser.position(); + // Check 1st identifier chatacter: + // - D|ATE[TIME] + // - T|IME + switch(wordParser.character(position)) { + case 'd': + case 'D': + // Prefix is "D" and possible options are DATE | DATETIME + switch (wordParser.character(position + 4)) { + // Prefix is "DATET" which points to DATETIME + case 't': + case 'T': + return DATETIME; + // Anything else points to LOCAL_DATE + default: + return DATE; + } + // Anything else points to TIME + default: + return TIME; + } + } + + } + + /** + * The actual local_datetime_type identifier found in the string representation of the JPQL query. + */ + private Identifier identifier; + + /** + * Creates a new LocalExpression. + * + * @param parent The parent of this expression + */ + public LocalDateTime(AbstractExpression parent) { + super(parent); + } + + @Override + public void accept(ExpressionVisitor visitor) { + visitor.visit(this); + } + + @Override + protected void addChildrenTo(Collection children) { + } + + @Override + public void acceptChildren(ExpressionVisitor visitor) { + } + + @Override + protected void addOrderedChildrenTo(List children) { + } + + @Override + protected void parse(final WordParser wordParser, boolean tolerant) { + identifier = Identifier.parse(wordParser); + setText(identifier.name); + wordParser.moveForward(identifier.name); + } + + /** + * Execute action depending on local date/time text identifier in {@link LocalDateTime} expression. + * + * @param dateAction function executed for {@code LOCAL DATE} + * @param timeAction function executed for {@code LOCAL TIME} + * @param dateTimeAction function executed for {@code LOCAL DATETIME} + */ + public void runByType(Runnable dateAction, Runnable timeAction, Runnable dateTimeAction) { + // Make sure there is some value available. + switch(this.identifier != null ? this.identifier : Identifier.getIdentifier(getText())) { + case DATE: + dateAction.run(); + return; + case TIME: + timeAction.run(); + return; + case DATETIME: + dateTimeAction.run(); + return; + default: + throw new IllegalStateException("Unknown value of " + getText() + " LocalDateTime expression"); + } + } + + /** + * Execute supplier depending on local date/time text identifier in {@link LocalDateTime} expression. + * + * @param dateAction function executed for {@code LOCAL DATE} + * @param timeAction function executed for {@code LOCAL TIME} + * @param dateTimeAction function executed for {@code LOCAL DATETIME} + */ + public R getValueByType(Supplier dateAction, Supplier timeAction, Supplier dateTimeAction) { + // Make sure there is some value available. + switch(this.identifier != null ? this.identifier : Identifier.getIdentifier(getText())) { + case DATE: + return dateAction.get(); + case TIME: + return timeAction.get(); + case DATETIME: + return dateTimeAction.get(); + default: + throw new IllegalStateException("Unknown value of " + getText() + " LocalDateTime expression"); + } + } + + @Override + public JPQLQueryBNF getQueryBNF() { + return getQueryBNF(LocalDateTypeBNF.ID); + } + + @Override + protected void toParsedText(StringBuilder writer, boolean actual) { + writer.append(actual ? identifier.name : getText()); + } + + @Override + public String toActualText() { + return getText(); + } + +} diff --git a/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/LocalDateTypeBNF.java b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/LocalDateTypeBNF.java new file mode 100644 index 00000000000..de6205b3b23 --- /dev/null +++ b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/LocalDateTypeBNF.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +// Contributors: +// 04/21/2022: Tomas Kraus +// - Issue 317: Implement LOCAL DATE, LOCAL TIME and LOCAL DATETIME. +package org.eclipse.persistence.jpa.jpql.parser; + +/** + * The query BNF for type part of local date/time expression. + *
+ * Jakarta Persistence 3.1: + *
BNF: + * local_datetime_type ::= DATE | ..... matches Java java.time.LocalDate + * TIME | ..... matches Java java.time.LocalTime + * DATETIME ..... matches Java java.time.LocalDateTime + *
+ */ +public class LocalDateTypeBNF extends JPQLQueryBNF { + + /** + * The unique identifier of this BNF rule. + */ + public static final String ID = "local_datetime_type"; + + /** + * Creates a new LocalDateTypeBNF. + */ + public LocalDateTypeBNF() { + super(ID); + } + + @Override + protected void initialize() { + super.initialize(); + registerExpressionFactory(LocalDateTypeFactory.ID); + } + +} diff --git a/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/LocalDateTypeFactory.java b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/LocalDateTypeFactory.java new file mode 100644 index 00000000000..f7a1ffae262 --- /dev/null +++ b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/LocalDateTypeFactory.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +// Contributors: +// 04/21/2022: Tomas Kraus +// - Issue 317: Implement LOCAL DATE, LOCAL TIME and LOCAL DATETIME. +package org.eclipse.persistence.jpa.jpql.parser; + +import org.eclipse.persistence.jpa.jpql.WordParser; + +/** + * This {@link LocalDateTypeFactory} creates a new {@link LocalDateTime} when the portion + * of the query to parse starts with LOCAL. + * + * @see LocalExpression + * @see LocalDateTime + */ +public class LocalDateTypeFactory extends ExpressionFactory { + + /** + * The unique identifier of this {@link LocalDateTypeFactory}. + */ + public static final String ID = "local_datetime_type"; + + /** + * Creates a new LocalDateTypeFactory. + */ + public LocalDateTypeFactory() { + super(ID, Expression.DATE, Expression.DATETIME, Expression.TIME); + } + + @Override + protected AbstractExpression buildExpression(AbstractExpression parent, + WordParser wordParser, + String word, + JPQLQueryBNF queryBNF, + AbstractExpression expression, + boolean tolerant) { + + expression = new LocalDateTime(parent); + expression.parse(wordParser, tolerant); + return expression; + } + +} diff --git a/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/LocalExpression.java b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/LocalExpression.java new file mode 100644 index 00000000000..12896586ea3 --- /dev/null +++ b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/LocalExpression.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +// Contributors: +// 04/21/2022: Tomas Kraus +// - Issue 317: Implement LOCAL DATE, LOCAL TIME and LOCAL DATETIME. +package org.eclipse.persistence.jpa.jpql.parser; + +import java.util.Collection; +import java.util.List; + +import org.eclipse.persistence.jpa.jpql.WordParser; + +/** + * The {@code LOCAL local_datetime_type} expression. + *
+ * Jakarta Persistence 3.1: + *
BNF: + * functions_returning_datetime ::= CURRENT_DATE | CURRENT_TIME | CURRENT_TIMESTAMP | + * LOCAL local_datetime_type | + * extract_datetime_par + * + * local_datetime_type ::= DATE | ..... matches Java java.time.LocalDate + * TIME | ..... matches Java java.time.LocalTime + * DATETIME ..... matches Java java.time.LocalDateTime + *
+ */ +public class LocalExpression extends AbstractExpression { + + /** + * The expression being negated by this expression. + */ + private AbstractExpression dateType; + + /** + * The actual LOCAL identifier found in the string representation of the JPQL query. + */ + private String identifier; + + /** + * Creates a new LocalExpression. + * + * @param parent The parent of this expression + */ + public LocalExpression(AbstractExpression parent) { + super(parent, LOCAL); + } + + @Override + public void accept(ExpressionVisitor visitor) { + visitor.visit(this); + } + + @Override + public void acceptChildren(ExpressionVisitor visitor) { + getDateType().accept(visitor); + } + + @Override + protected void addChildrenTo(Collection children) { + children.add(getDateType()); + } + + @Override + protected void addOrderedChildrenTo(List children) { + children.add(buildStringExpression(LOCAL)); + children.add(buildStringExpression(SPACE)); + if (dateType != null) { + children.add(dateType); + } + } + + @Override + protected void parse(WordParser wordParser, boolean tolerant) { + identifier = wordParser.moveForward(LOCAL); + wordParser.skipLeadingWhitespace(); + dateType = parse(wordParser, LocalDateTypeBNF.ID, tolerant); + } + + @Override + public JPQLQueryBNF getQueryBNF() { + return getQueryBNF(LocalExpressionBNF.ID); + } + + @Override + protected void toParsedText(StringBuilder writer, boolean actual) { + // LOCAL + writer.append(actual ? identifier : getText()); + writer.append(SPACE); + // expression + if (dateType != null) { + dateType.toParsedText(writer, actual); + } + } + + /** + * Returns the {@link Expression} representing the expression with date type. + * + * @return The expression representing the expression with date type + */ + public Expression getDateType() { + if (dateType == null) { + dateType = buildNullExpression(); + } + return dateType; + } + + /** + * Determines whether the expression with date type was parsed. + * + * @return true if the expression with date type was parsed; + * false if it was not parsed + */ + public boolean hasDateType() { + return dateType != null && !dateType.isNull(); + } + +} diff --git a/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/LocalExpressionBNF.java b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/LocalExpressionBNF.java new file mode 100644 index 00000000000..c087d5029ea --- /dev/null +++ b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/LocalExpressionBNF.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +// Contributors: +// 04/21/2022: Tomas Kraus +// - Issue 317: Implement LOCAL DATE, LOCAL TIME and LOCAL DATETIME. +package org.eclipse.persistence.jpa.jpql.parser; + +/** + * The query BNF for local date/time expression. + *
+ * Jakarta Persistence 3.1: + *
BNF: + * functions_returning_datetime ::= CURRENT_DATE | CURRENT_TIME | CURRENT_TIMESTAMP | + * LOCAL local_datetime_type | + * extract_datetime_par + * + * local_datetime_type ::= DATE | ..... matches Java java.time.LocalDate + * TIME | ..... matches Java java.time.LocalTime + * DATETIME ..... matches Java java.time.LocalDateTime + *
+ */ +public class LocalExpressionBNF extends JPQLQueryBNF { + + /** + * The unique identifier of this BNF rule. + */ + public static final String ID = "local_expression"; + + /** + * Creates a new LocalExpressionBNF. + */ + public LocalExpressionBNF() { + super(ID); + } + + @Override + protected void initialize() { + super.initialize(); + registerExpressionFactory(LocalExpressionFactory.ID); + registerChild(LocalDateTypeBNF.ID); + } + +} diff --git a/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/LocalExpressionFactory.java b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/LocalExpressionFactory.java new file mode 100644 index 00000000000..04fe4b3dca0 --- /dev/null +++ b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/parser/LocalExpressionFactory.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0, + * or the Eclipse Distribution License v. 1.0 which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + */ + +// Contributors: +// 04/21/2022: Tomas Kraus +// - Issue 317: Implement LOCAL DATE, LOCAL TIME and LOCAL DATETIME. +package org.eclipse.persistence.jpa.jpql.parser; + +import org.eclipse.persistence.jpa.jpql.WordParser; + +/** + * This {@link LocalExpressionFactory} creates a new {@link LocalExpression} when the portion + * of the query to parse starts with LOCAL. + * + * @see LocalExpression + * @see LocalDateTime + */ +public class LocalExpressionFactory extends ExpressionFactory { + + /** + * The unique identifier of this {@link DateTimeFactory}. + */ + public static final String ID = "local_expression"; + + /** + * Creates a new LocalDateTimeFactory. + */ + public LocalExpressionFactory() { + super(ID, Expression.LOCAL); + } + + @Override + protected AbstractExpression buildExpression(AbstractExpression parent, + WordParser wordParser, + String word, + JPQLQueryBNF queryBNF, + AbstractExpression expression, + boolean tolerant) { + expression = new LocalExpression(parent); + expression.parse(wordParser, tolerant); + return expression; + } + +} diff --git a/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/tools/AbstractContentAssistVisitor.java b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/tools/AbstractContentAssistVisitor.java index c44a2959d10..1044ab79a81 100644 --- a/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/tools/AbstractContentAssistVisitor.java +++ b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/tools/AbstractContentAssistVisitor.java @@ -16,6 +16,7 @@ // - 527415: Fix code when locale is tr, az or lt // 04/21/2022: Tomas Kraus // - Issue 1474: Update JPQL Grammar for Jakarta Persistence 2.2, 3.0 and 3.1 +// - Issue 317: Implement LOCAL DATE, LOCAL TIME and LOCAL DATETIME. package org.eclipse.persistence.jpa.jpql.tools; import java.util.ArrayList; @@ -109,6 +110,8 @@ import org.eclipse.persistence.jpa.jpql.parser.KeywordExpression; import org.eclipse.persistence.jpa.jpql.parser.LengthExpression; import org.eclipse.persistence.jpa.jpql.parser.LikeExpression; +import org.eclipse.persistence.jpa.jpql.parser.LocalDateTime; +import org.eclipse.persistence.jpa.jpql.parser.LocalExpression; import org.eclipse.persistence.jpa.jpql.parser.LocateExpression; import org.eclipse.persistence.jpa.jpql.parser.LogicalExpression; import org.eclipse.persistence.jpa.jpql.parser.LowerExpression; @@ -3221,6 +3224,36 @@ else if (expression.hasSpaceAfterLike()) { } } + @Override + public void visit(LocalExpression expression) { + super.visit(expression); + addFunctionIdentifiers(expression.getParent().findQueryBNF(expression)); + } + + @Override + public void visit(LocalDateTime expression) { + super.visit(expression); + int position = queryPosition.getPosition(expression) - corrections.peek(); + switch (expression.toActualText().charAt(0)) { + case 'D': case 'd': + if (position < 4) { + proposals.addIdentifier(DATE); + proposals.addIdentifier(DATETIME); + } else { + switch (expression.toActualText().charAt(4)) { + case 'T': case 't': + proposals.addIdentifier(DATETIME); + break; + default: + proposals.addIdentifier(DATE); + } + } + break; + default: + proposals.addIdentifier(TIME); + } + } + @Override public void visit(LocateExpression expression) { super.visit(expression); @@ -7696,6 +7729,37 @@ else if (expression.hasPatternValue() && } } + @Override + public void visit(LocalExpression expression) { + if (badExpression) { + if (positionWithinInvalidExpression <= 5 /* LOCAL */) { + queryPosition.setExpression(expression); + queryPosition.addPosition(expression, positionWithinInvalidExpression); + } + } + else { + if (expression.hasDateType()) { + expression.getDateType().accept(this); + } + if (queryPosition.getExpression() == null) { + queryPosition.setExpression(expression); + } + queryPosition.addPosition(expression, expression.getLength() - correction); + } + } + + @Override + public void visit(LocalDateTime expression) { + if (badExpression) { + correction = expression.getLength() - positionWithinInvalidExpression; + queryPosition.setExpression(expression); + queryPosition.addPosition(expression, positionWithinInvalidExpression); + } else if (invalidExpression == expression) { + queryPosition.setExpression(expression); + queryPosition.addPosition(expression, positionWithinInvalidExpression); + } + } + @Override public void visit(LocateExpression expression) { visitAbstractTripleEncapsulatedExpression(expression); diff --git a/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/tools/resolver/ResolverBuilder.java b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/tools/resolver/ResolverBuilder.java index 1f755a20d9e..4f378fed81c 100644 --- a/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/tools/resolver/ResolverBuilder.java +++ b/jpa/org.eclipse.persistence.jpa.jpql/src/main/java/org/eclipse/persistence/jpa/jpql/tools/resolver/ResolverBuilder.java @@ -14,11 +14,14 @@ // Oracle - initial API and implementation // 04/21/2022: Tomas Kraus // - Issue 1474: Update JPQL Grammar for Jakarta Persistence 2.2, 3.0 and 3.1 +// - Issue 317: Implement LOCAL DATE, LOCAL TIME and LOCAL DATETIME. package org.eclipse.persistence.jpa.jpql.tools.resolver; import java.sql.Date; import java.sql.Time; import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -73,6 +76,8 @@ import org.eclipse.persistence.jpa.jpql.parser.KeywordExpression; import org.eclipse.persistence.jpa.jpql.parser.LengthExpression; import org.eclipse.persistence.jpa.jpql.parser.LikeExpression; +import org.eclipse.persistence.jpa.jpql.parser.LocalDateTime; +import org.eclipse.persistence.jpa.jpql.parser.LocalExpression; import org.eclipse.persistence.jpa.jpql.parser.LocateExpression; import org.eclipse.persistence.jpa.jpql.parser.LowerExpression; import org.eclipse.persistence.jpa.jpql.parser.MathDoubleExpression; @@ -714,6 +719,20 @@ public void visit(LikeExpression expression) { resolver = buildClassResolver(Boolean.class); } + @Override + public void visit(LocalExpression expression) { + expression.getDateType().accept(this); + } + + @Override + public void visit(LocalDateTime expression) { + resolver = expression.getValueByType( + () -> buildClassResolver(LocalDate.class), + () -> buildClassResolver(LocalTime.class), + () -> buildClassResolver(LocalDateTime.class) + ); + } + @Override public void visit(LocateExpression expression) { resolver = buildClassResolver(Integer.class); diff --git a/jpa/org.eclipse.persistence.jpa.jpql/src/test/java/org/eclipse/persistence/jpa/tests/jpql/tools/AbstractContentAssistTest.java b/jpa/org.eclipse.persistence.jpa.jpql/src/test/java/org/eclipse/persistence/jpa/tests/jpql/tools/AbstractContentAssistTest.java index 30c429d42f7..b6433dfae58 100644 --- a/jpa/org.eclipse.persistence.jpa.jpql/src/test/java/org/eclipse/persistence/jpa/tests/jpql/tools/AbstractContentAssistTest.java +++ b/jpa/org.eclipse.persistence.jpa.jpql/src/test/java/org/eclipse/persistence/jpa/tests/jpql/tools/AbstractContentAssistTest.java @@ -3010,8 +3010,7 @@ protected final void test_Function(String fragment, sb.append(position); sb.append("]"); Assert.fail(sb.toString()); - } - finally { + } finally { tearDown(); } }