From e55f828427346ebf643fdb009f3e2495b30bbdca Mon Sep 17 00:00:00 2001 From: Scott M Stark Date: Wed, 6 Mar 2024 03:59:49 -0600 Subject: [PATCH 1/4] Prototype a query by name method parser Signed-off-by: Scott M Stark --- tools/src/main/antlr4/QBN.g4 | 82 ++++++++ .../tck/data/tools/qbyn/ParseUtils.java | 100 +++++++++ .../tck/data/tools/qbyn/QueryByNameInfo.java | 199 ++++++++++++++++++ tools/src/test/java/qbyn/QBNParserTest.java | 161 ++++++++++++++ 4 files changed, 542 insertions(+) create mode 100644 tools/src/main/antlr4/QBN.g4 create mode 100644 tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/ParseUtils.java create mode 100644 tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/QueryByNameInfo.java create mode 100644 tools/src/test/java/qbyn/QBNParserTest.java diff --git a/tools/src/main/antlr4/QBN.g4 b/tools/src/main/antlr4/QBN.g4 new file mode 100644 index 000000000..25bd0640e --- /dev/null +++ b/tools/src/main/antlr4/QBN.g4 @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * 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. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +// 4.6.1. BNF Grammar for Query Methods +grammar QBN; + +@header { +// TBD +package ee.jakarta.tck.data.tools.antlr; +} + +query_method : subject predicate order_clause? ; + +subject : (action | find find_expression) ignored_text? BY ; + +action : find | delete | update | count | exists ; + +find : 'find' ; +delete : 'delete' ; +update : 'update' ; +count : 'count' ; +exists : 'exists' ; + +find_expression : FIRST INTEGER? ; + +predicate : condition ( (AND | OR) condition )* ; + +condition : property ignore_case? not? operator? ; +ignore_case : IGNORE_CASE ; +not : NOT ; + +operator : CONTAINS | ENDSWITH | STARTSWITH | LESSTHAN| LESSTHANEQUAL | GREATERTHAN | + GREATERTHANEQUAL | BETWEEN | EMPTY | LIKE | IN | NULL | + TRUE | FALSE ; +property : (IDENTIFIER | IDENTIFIER '_' property)+ ; + +order_clause : ORDER_BY ( order_item )* ( order_item | property ) ; + +order_item : property ( ASC | DESC ) ; + +ignored_text : IDENTIFIER ; + +// Lexer rules +FIRST : 'First' ; +BY : 'By' ; +CONTAINS : 'Contains' ; +ENDSWITH : 'EndsWith' ; +STARTSWITH : 'StartsWith' ; +LESSTHAN : 'LessThan' ; +LESSTHANEQUAL : 'LessThanEqual' ; +GREATERTHAN : 'GreaterThan' ; +GREATERTHANEQUAL : 'GreaterThanEqual' ; +BETWEEN : 'Between' ; +EMPTY : 'Empty' ; +LIKE : 'Like' ; +IN : 'In' ; +NULL : 'Null' ; +TRUE : 'True' ; +FALSE : 'False' ; +IGNORE_CASE : 'IgnoreCase' ; +NOT : 'Not' ; +ORDER_BY : 'OrderBy' ; +AND : 'And' ; +OR : 'Or' ; +ASC : 'Asc' ; +DESC : 'Desc' ; + +IDENTIFIER : ([A-Z][a-z]+)+? ; +INTEGER : [0-9]+ ; +WS : [ \t\r\n]+ -> skip ; diff --git a/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/ParseUtils.java b/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/ParseUtils.java new file mode 100644 index 000000000..33a7e1f3a --- /dev/null +++ b/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/ParseUtils.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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 + * + * SPDX-License-Identifier: Apache-2.0 + */ +package ee.jakarta.tck.data.tools.qbyn; + +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CodePointCharStream; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.tree.ParseTree; + +import ee.jakarta.tck.data.tools.antlr.QBNLexer; +import ee.jakarta.tck.data.tools.antlr.QBNParser; +import ee.jakarta.tck.data.tools.antlr.QBNBaseListener; + +/** + * A utility class for parsing query by name method names using the Antlr4 generated parser + */ +public class ParseUtils { + /** + * Parse a query by name method name into a QueryByNameInfo object + * @param queryByName the query by name method name + * @return the parsed QueryByNameInfo object + */ + public static QueryByNameInfo parseQueryByName(String queryByName) { + CodePointCharStream input = CharStreams.fromString(queryByName); + QBNLexer lexer = new QBNLexer(input); // create a buffer of tokens pulled from the lexer + CommonTokenStream tokens = new CommonTokenStream(lexer); // create a parser that feeds off the tokens buffer + QBNParser parser = new QBNParser(tokens); + QueryByNameInfo info = new QueryByNameInfo(); + parser.addParseListener(new QBNBaseListener() { + private StringBuffer property = new StringBuffer(); + + @Override + public void exitPredicate(ee.jakarta.tck.data.tools.antlr.QBNParser.PredicateContext ctx) { + int count = ctx.condition().size(); + for (int i = 0; i < count; i++) { + ee.jakarta.tck.data.tools.antlr.QBNParser.ConditionContext cctx = ctx.condition(i); + String property = cctx.property().getText(); + QueryByNameInfo.Operator operator = QueryByNameInfo.Operator.NONE; + if(cctx.operator() != null) { + operator = QueryByNameInfo.Operator.valueOf(cctx.operator().getText().toUpperCase()); + } + boolean ignoreCase = cctx.ignore_case() != null; + boolean not = cctx.not() != null; + boolean and = false; + if(i > 0) { + // The AND/OR is only present if there is more than one condition + and = ctx.AND(i-1) != null; + } + // String property, Operator operator, boolean ignoreCase, boolean not, boolean and + info.addCondition(property, operator, ignoreCase, not, and); + } + } + + @Override + public void exitSubject(ee.jakarta.tck.data.tools.antlr.QBNParser.SubjectContext ctx) { + if(ctx.find() != null) { + System.out.println("find: " + ctx.find().getText()); + System.out.println("find_expression.INTEGER: " + ctx.find_expression().INTEGER()); + int findCount = 0; + if(ctx.find_expression().INTEGER() != null) { + findCount = Integer.parseInt(ctx.find_expression().INTEGER().getText()); + } + info.setFindExpressionCount(findCount); + } else { + QueryByNameInfo.Action action = QueryByNameInfo.Action.valueOf(ctx.action().getText().toUpperCase()); + info.setAction(action); + } + if(ctx.ignored_text() != null) { + info.setIgnoredText(ctx.ignored_text().getText()); + } + } + @Override + public void exitOrder_clause(ee.jakarta.tck.data.tools.antlr.QBNParser.Order_clauseContext ctx) { + int count = ctx.order_item().size(); + if(ctx.property() != null) { + String property = ctx.property().getText(); + info.addOrderBy(property, QueryByNameInfo.OrderBySortDirection.NONE); + } + for (int i = 0; i < count; i++) { + ee.jakarta.tck.data.tools.antlr.QBNParser.Order_itemContext octx = ctx.order_item(i); + String property = octx.property().getText(); + QueryByNameInfo.OrderBySortDirection direction = octx.ASC() != null ? QueryByNameInfo.OrderBySortDirection.ASC : QueryByNameInfo.OrderBySortDirection.DESC; + info.addOrderBy(property, direction); + } + } + }); + // Run the parser + ParseTree tree = parser.query_method(); + return info; + } +} diff --git a/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/QueryByNameInfo.java b/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/QueryByNameInfo.java new file mode 100644 index 000000000..1122b674c --- /dev/null +++ b/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/QueryByNameInfo.java @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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 + * + * SPDX-License-Identifier: Apache-2.0 + */ +package ee.jakarta.tck.data.tools.qbyn; + +import java.util.ArrayList; +import java.util.List; + +/** + * A collection of the information parsed from a Query by name method name + * using the BNF grammar defined in QBN.g4 + */ +public class QueryByNameInfo { + /** + * The support <action> types + */ + public enum Action { + // find | delete | update | count | exists + FIND, DELETE, UPDATE, COUNT, EXISTS, NONE + } + + /** + * The support <operator> types + */ + public enum Operator { + CONTAINS, ENDSWITH, STARTSWITH, LESSTHAN, LESSTHANEQUAL, GREATERTHAN, + GREATERTHANEQUAL, BETWEEN , EMPTY , LIKE , IN , NULL, + TRUE , FALSE, NONE ; + } + public enum OrderBySortDirection { + ASC, DESC, NONE + } + + /** + * A <condition> in the <predicate> statement + */ + public static class Condition { + // an entity property name + String property; + // the operator to apply to the property + Operator operator = Operator.NONE; + // is the condition case-insensitive + boolean ignoreCase; + // is the condition negated + boolean not; + // for multiple conditions, is this condition joined by AND(true) or OR(false) + boolean and; + } + + /** + * A <order-item> or <property> in the <order-clause> + */ + public static class OrderBy { + // an entity property name + String property; + // the direction to sort the property + OrderBySortDirection direction = OrderBySortDirection.NONE; + } + private Action action = Action.NONE; + private List predicates = new ArrayList<>(); + private List orderBy = new ArrayList<>(); + // >= 0 means find expression exists + int findExpressionCount = -1; + String ignoredText; + + public Action getAction() { + return action; + } + + public void setAction(Action action) { + this.action = action; + } + + public List getPredicates() { + return predicates; + } + + public void setPredicates(List predicates) { + this.predicates = predicates; + } + public List addCondition(Condition condition) { + this.predicates.add(condition); + return this.predicates; + } + public List addCondition(String property, Operator operator, boolean ignoreCase, boolean not, boolean and) { + Condition c = new Condition(); + c.property = property; + c.operator = operator; + c.ignoreCase = ignoreCase; + c.not = not; + c.and = and; + this.predicates.add(c); + return this.predicates; + } + + public int getFindExpressionCount() { + return findExpressionCount; + } + + public void setFindExpressionCount(int findExpressionCount) { + this.findExpressionCount = findExpressionCount; + } + + public String getIgnoredText() { + return ignoredText; + } + public void setIgnoredText(String ignoredText) { + this.ignoredText = ignoredText; + } + + public List getOrderBy() { + return orderBy; + } + public void setOrderBy(List orderBy) { + this.orderBy = orderBy; + } + public List addOrderBy(OrderBy orderBy) { + this.orderBy.add(orderBy); + return this.orderBy; + } + public List addOrderBy(String property, OrderBySortDirection direction) { + OrderBy ob = new OrderBy(); + ob.property = property; + ob.direction = direction; + this.orderBy.add(ob); + return this.orderBy; + } + + /** + * Returns a string representation of the parsed query by name method + * @return + */ + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append('('); + // Subject + if(action != Action.NONE) { + sb.append(action.name().toLowerCase()); + } else { + sb.append("findFirst"); + if(findExpressionCount > 0) { + sb.append(findExpressionCount); + } + } + if(ignoredText != null && !ignoredText.isEmpty()) { + sb.append(ignoredText); + } + sb.append("By"); + // Predicates + boolean first = true; + if(!predicates.isEmpty()) { + for(Condition c : predicates) { + if(!first) { + sb.append(c.and ? "AND" : "OR"); + } + sb.append('('); + sb.append(c.property); + sb.append(' '); + if(c.ignoreCase) { + sb.append("IgnoreCase"); + } + if(c.not) { + sb.append("NOT"); + } + if(c.operator != Operator.NONE) { + sb.append(c.operator.name().toUpperCase()); + } + sb.append(')'); + first = false; + } + sb.append(')'); + } + // OrderBy + if(!orderBy.isEmpty()) { + sb.append("(OrderBy "); + for(OrderBy ob : orderBy) { + sb.append('('); + sb.append(ob.property); + sb.append(' '); + if(ob.direction != OrderBySortDirection.NONE) { + sb.append(ob.direction.name().toUpperCase()); + } + sb.append(')'); + } + sb.append(')'); + } + sb.append(')'); + return sb.toString(); + } + +} diff --git a/tools/src/test/java/qbyn/QBNParserTest.java b/tools/src/test/java/qbyn/QBNParserTest.java new file mode 100644 index 000000000..0c6bc7743 --- /dev/null +++ b/tools/src/test/java/qbyn/QBNParserTest.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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 + * + * SPDX-License-Identifier: Apache-2.0 + */ +package qbyn; + +import ee.jakarta.tck.data.tools.qbyn.ParseUtils; +import ee.jakarta.tck.data.tools.qbyn.QueryByNameInfo; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CodePointCharStream; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.tree.ParseTree; +import org.junit.jupiter.api.Test; + +import ee.jakarta.tck.data.tools.antlr.QBNLexer; +import ee.jakarta.tck.data.tools.antlr.QBNParser; +import ee.jakarta.tck.data.tools.antlr.QBNBaseListener; + +import java.io.IOException; + +public class QBNParserTest { + // Some of these are not actual query by name examples even though they follow the pattern + String actionExamples = """ + findByHexadecimalContainsAndIsControlNot + findByDepartmentCountAndPriceBelow + countByHexadecimalNotNull + existsByThisCharacter + findByDepartmentsContains + findByDepartmentsEmpty + findByFloorOfSquareRootNotAndIdLessThanOrderByBitsRequiredDesc + findByFloorOfSquareRootOrderByIdAsc + findByHexadecimalIgnoreCase + findByHexadecimalIgnoreCaseBetweenAndHexadecimalNotIn + findById + findByIdBetween + findByIdBetweenOrderByNumTypeAsc + findByIdGreaterThanEqual + findByIdIn + findByIdLessThan + findByIdLessThanEqual + findByIdLessThanOrderByFloorOfSquareRootDesc + findByIsControlTrueAndNumericValueBetween + findByIsOddFalseAndIdBetween + findByIsOddTrueAndIdLessThanEqualOrderByIdDesc + findByNameLike + findByNumTypeAndFloorOfSquareRootLessThanEqual + findByNumTypeAndNumBitsRequiredLessThan + findByNumTypeInOrderByIdAsc + findByNumTypeNot + findByNumTypeOrFloorOfSquareRoot + findByNumericValue + findByNumericValueBetween + findByNumericValueLessThanEqualAndNumericValueGreaterThanEqual + findFirst3ByNumericValueGreaterThanEqualAndHexadecimalEndsWith + findFirstByHexadecimalStartsWithAndIsControlOrderByIdAsc + findByPriceNotNullAndPriceLessThanEqual + findByPriceNull + findByProductNumLike + """; + + /** + * Test the parser using a local QBNBaseListener implementation + * @throws IOException + */ + @Test + public void testQueryByNameExamples() throws IOException { + String[] examples = actionExamples.split("\n"); + for (String example : examples) { + System.out.println(example); + CodePointCharStream input = CharStreams.fromString(example); // create a lexer that feeds off of input CharStream + QBNLexer lexer = new QBNLexer(input); // create a buffer of tokens pulled from the lexer + CommonTokenStream tokens = new CommonTokenStream(lexer); // create a parser that feeds off the tokens buffer + QBNParser parser = new QBNParser(tokens); + QueryByNameInfo info = new QueryByNameInfo(); + parser.addParseListener(new QBNBaseListener() { + private StringBuffer property = new StringBuffer(); + + @Override + public void exitPredicate(QBNParser.PredicateContext ctx) { + int count = ctx.condition().size(); + for (int i = 0; i < count; i++) { + QBNParser.ConditionContext cctx = ctx.condition(i); + String property = cctx.property().getText(); + QueryByNameInfo.Operator operator = QueryByNameInfo.Operator.NONE; + if(cctx.operator() != null) { + operator = QueryByNameInfo.Operator.valueOf(cctx.operator().getText().toUpperCase()); + } + boolean ignoreCase = cctx.ignore_case() != null; + boolean not = cctx.not() != null; + boolean and = false; + if(i > 0) { + // The AND/OR is only present if there is more than one condition + and = ctx.AND(i-1) != null; + } + // String property, Operator operator, boolean ignoreCase, boolean not, boolean and + info.addCondition(property, operator, ignoreCase, not, and); + } + } + + @Override + public void exitSubject(QBNParser.SubjectContext ctx) { + if(ctx.find() != null) { + System.out.println("find: " + ctx.find().getText()); + System.out.println("find_expression.INTEGER: " + ctx.find_expression().INTEGER()); + int findCount = 0; + if(ctx.find_expression().INTEGER() != null) { + findCount = Integer.parseInt(ctx.find_expression().INTEGER().getText()); + } + info.setFindExpressionCount(findCount); + } else { + QueryByNameInfo.Action action = QueryByNameInfo.Action.valueOf(ctx.action().getText().toUpperCase()); + info.setAction(action); + } + if(ctx.ignored_text() != null) { + info.setIgnoredText(ctx.ignored_text().getText()); + } + } + @Override + public void exitOrder_clause(QBNParser.Order_clauseContext ctx) { + int count = ctx.order_item().size(); + if(ctx.property() != null) { + String property = ctx.property().getText(); + info.addOrderBy(property, QueryByNameInfo.OrderBySortDirection.NONE); + } + for (int i = 0; i < count; i++) { + QBNParser.Order_itemContext octx = ctx.order_item(i); + String property = octx.property().getText(); + QueryByNameInfo.OrderBySortDirection direction = octx.ASC() != null ? QueryByNameInfo.OrderBySortDirection.ASC : QueryByNameInfo.OrderBySortDirection.DESC; + info.addOrderBy(property, direction); + } + } + }); + ParseTree tree = parser.query_method(); + // print LISP-style tree for the + System.out.println(tree.toStringTree(parser)); + // Print out the parsed QueryByNameInfo + System.out.println(info); + + } + } + + /** + * Test the parser using the ParseUtils class + */ + @Test + public void testParseUtils() { + String[] examples = actionExamples.split("\n"); + for (String example : examples) { + System.out.println(example); + QueryByNameInfo info = ParseUtils.parseQueryByName(example); + System.out.println(info); + } + } +} From c504a6ec82f32aa70f8a8ecbee5d43fb136ba107 Mon Sep 17 00:00:00 2001 From: Scott M Stark Date: Tue, 2 Apr 2024 22:43:44 -0600 Subject: [PATCH 2/4] Prototype updates Signed-off-by: Scott M Stark --- tools/pom.xml | 139 +++++++++ .../tck/data/tools/annp/AnnProcUtils.java | 105 +++++++ .../tck/data/tools/annp/RepositoryInfo.java | 143 +++++++++ .../data/tools/annp/RespositoryProcessor.java | 184 +++++++++++ .../tck/data/tools/qbyn/ParseUtils.java | 172 ++++++++++- .../tck/data/tools/qbyn/QueryByNameInfo.java | 52 +++- tools/src/test/java/qbyn/QBNParserTest.java | 291 +++++++++++++++++- tools/src/test/java/qbyn/ST4RepoGenTest.java | 71 +++++ 8 files changed, 1145 insertions(+), 12 deletions(-) create mode 100644 tools/pom.xml create mode 100644 tools/src/main/java/ee/jakarta/tck/data/tools/annp/AnnProcUtils.java create mode 100644 tools/src/main/java/ee/jakarta/tck/data/tools/annp/RepositoryInfo.java create mode 100644 tools/src/main/java/ee/jakarta/tck/data/tools/annp/RespositoryProcessor.java create mode 100644 tools/src/test/java/qbyn/ST4RepoGenTest.java diff --git a/tools/pom.xml b/tools/pom.xml new file mode 100644 index 000000000..1a5aa8b89 --- /dev/null +++ b/tools/pom.xml @@ -0,0 +1,139 @@ + + + + + 4.0.0 + + + jakarta.data + jakarta.data-parent + 1.0.0-SNAPSHOT + + + jakarta.data-tools + Jakarta Data Tools + Jakarta Data :: Tools + + + 4.13.1 + + + + + + jakarta.data + jakarta.data-api + 1.0.0-SNAPSHOT + + + jakarta.persistence + jakarta.persistence-api + 3.1.0 + provided + + + + org.jboss.arquillian.container + arquillian-container-test-spi + + + org.jboss.arquillian.container + arquillian-container-test-api + + + + org.antlr + antlr4 + ${antlr.version} + + + org.antlr + antlr4-runtime + ${antlr.version} + + + org.antlr + ST4 + 4.3.4 + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + + org.antlr + antlr4-maven-plugin + ${antlr.version} + + true + true + ${project.build.directory}/generated-sources/ee/jakarta/tck/data/tools/antlr + + + + antlr + + antlr4 + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven.checkstyle.plugin.version} + + true + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + target/generated-sources/** + true + + ee.jakarta.tck.data.tools.antlr + + + + attach-javadocs + + jar + + + + + + + diff --git a/tools/src/main/java/ee/jakarta/tck/data/tools/annp/AnnProcUtils.java b/tools/src/main/java/ee/jakarta/tck/data/tools/annp/AnnProcUtils.java new file mode 100644 index 000000000..38932f347 --- /dev/null +++ b/tools/src/main/java/ee/jakarta/tck/data/tools/annp/AnnProcUtils.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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 + * + * SPDX-License-Identifier: Apache-2.0 + */ +package ee.jakarta.tck.data.tools.annp; + +import ee.jakarta.tck.data.tools.qbyn.ParseUtils; +import ee.jakarta.tck.data.tools.qbyn.QueryByNameInfo; +import org.stringtemplate.v4.ST; + +import javax.annotation.processing.Filer; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.tools.JavaFileObject; +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; + +public class AnnProcUtils { + static String REPO_TEMPLATE = """ + import jakarta.annotation.Generated; + import jakarta.data.repository.OrderBy; + import jakarta.data.repository.Query; + import jakarta.data.repository.Repository; + import #repo.fqn#; + + @Repository(dataStore = "#repo.dataStore#") + @Generated("ee.jakarta.tck.data.tools.annp.RespositoryProcessor") + public interface #repo.name#$ extends #repo.name# { + #repo.methods :{m | + @Override + @Query("#m.query#") + #m.orderBy :{o | @OrderBy(value="#o.property#", descending = #o.descending#)}# + public #m.returnType# #m.name# (#m.parameters: {p | #p#}; separator=", "#); + + } + # + } + """; + + public static List methodsIn(Iterable elements) { + ArrayList methods = new ArrayList<>(); + for (Element e : elements) { + if(e.getKind() == ElementKind.METHOD) { + methods.add((ExecutableElement) e); + } + } + return methods; + } + public static String getFullyQualifiedName(Element element) { + if (element instanceof TypeElement) { + return ((TypeElement) element).getQualifiedName().toString(); + } + return null; + } + + + public static QueryByNameInfo isQBN(ExecutableElement m) { + QueryByNameInfo info = null; + String methodName = m.getSimpleName().toString(); + if(methodName.startsWith("findBy") || methodName.startsWith("deleteBy") || methodName.startsWith("updateBy") + || methodName.startsWith("countBy") || methodName.startsWith("existsBy") ) { + try { + info = ParseUtils.parseQueryByName(methodName); + } catch (Throwable e) { + System.out.printf("Failed to parse %s: %s\n", methodName, e.getMessage()); + return null; + } + return info; + } + + return null; + } + + public static void writeRepositoryInterface(RepositoryInfo repo, ProcessingEnvironment processingEnv) throws IOException { + ST st = new ST(REPO_TEMPLATE, '#', '#'); + st.add("repo", repo); + String ifaceSrc = st.render(); + String ifaceName = repo.getFqn() + "$"; + Filer filer = processingEnv.getFiler(); + JavaFileObject srcFile = filer.createSourceFile(ifaceName, repo.getRepositoryElement()); + try(Writer writer = srcFile.openWriter()) { + writer.write(ifaceSrc); + writer.flush(); + } + System.out.printf("Wrote %s, to: %s\n", ifaceName, srcFile.toUri()); + } +} diff --git a/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RepositoryInfo.java b/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RepositoryInfo.java new file mode 100644 index 000000000..0af03d388 --- /dev/null +++ b/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RepositoryInfo.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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 + * + * SPDX-License-Identifier: Apache-2.0 + */ +package ee.jakarta.tck.data.tools.annp; + +import ee.jakarta.tck.data.tools.qbyn.ParseUtils; +import ee.jakarta.tck.data.tools.qbyn.QueryByNameInfo; +import ee.jakarta.tck.data.tools.qbyn.QueryByNameInfo.OrderBy; +import jakarta.data.repository.Repository; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.VariableElement; +import java.util.ArrayList; +import java.util.List; + +public class RepositoryInfo { + public static class MethodInfo { + String name; + String returnType; + String query; + List orderBy; + List parameters = new ArrayList<>(); + List exceptions = new ArrayList<>(); + + public MethodInfo(String name, String returnType, String query, List orderBy) { + this.name = name; + this.returnType = returnType; + this.query = query; + this.orderBy = orderBy; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getReturnType() { + return returnType; + } + + public void setReturnType(String returnType) { + this.returnType = returnType; + } + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + public List getParameters() { + return parameters; + } + public void addParameter(String p) { + parameters.add(p); + } + public List getOrderBy() { + return orderBy; + } + } + private Element repositoryElement; + private String fqn; + private String name; + private String dataStore = ""; + private ArrayList methods = new ArrayList<>(); + public ArrayList qbnMethods = new ArrayList<>(); + + public RepositoryInfo() { + } + public RepositoryInfo(Element repositoryElement) { + this.repositoryElement = repositoryElement; + Repository ann = repositoryElement.getAnnotation(Repository.class); + setFqn(AnnProcUtils.getFullyQualifiedName(repositoryElement)); + setName(repositoryElement.getSimpleName().toString()); + setDataStore(ann.dataStore()); + } + + public Element getRepositoryElement() { + return repositoryElement; + } + public String getFqn() { + return fqn; + } + + public void setFqn(String fqn) { + this.fqn = fqn; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDataStore() { + return dataStore; + } + + public void setDataStore(String dataStore) { + this.dataStore = dataStore; + } + + + public void addQBNMethod(ExecutableElement m, QueryByNameInfo info) { + qbnMethods.add(m); + String query = ParseUtils.toQuery(info); + StringBuilder orderBy = new StringBuilder(); + MethodInfo mi = new MethodInfo(m.getSimpleName().toString(), m.getReturnType().toString(), query, info.getOrderBy()); + for (VariableElement p : m.getParameters()) { + mi.addParameter(p.asType().toString() + " " + p.getSimpleName()); + } + addMethod(mi); + } + public List getQBNMethods() { + return qbnMethods; + } + public boolean hasQBNMethods() { + return !qbnMethods.isEmpty(); + } + + public ArrayList getMethods() { + return methods; + } + + public void addMethod(MethodInfo m) { + methods.add(m); + } +} diff --git a/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RespositoryProcessor.java b/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RespositoryProcessor.java new file mode 100644 index 000000000..28e2af519 --- /dev/null +++ b/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RespositoryProcessor.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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 + * + * SPDX-License-Identifier: Apache-2.0 + */ +package ee.jakarta.tck.data.tools.annp; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedOptions; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import ee.jakarta.tck.data.tools.qbyn.QueryByNameInfo; +import jakarta.data.repository.Delete; +import jakarta.data.repository.Insert; +import jakarta.data.repository.Repository; +import jakarta.data.repository.Save; +import jakarta.data.repository.Update; +import jakarta.persistence.Entity; + +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + + +@SupportedAnnotationTypes("jakarta.data.repository.Repository") +@SupportedSourceVersion(SourceVersion.RELEASE_17) +@SupportedOptions({"debug"}) +public class RespositoryProcessor extends AbstractProcessor { + private Map repoInfoMap = new HashMap<>(); + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + System.out.printf("RespositoryProcessor: Processing repositories, over=%s\n", roundEnv.processingOver()); + boolean newRepos = false; + Set repositories = roundEnv.getElementsAnnotatedWith(Repository.class); + for (Element repository : repositories) { + String fqn = AnnProcUtils.getFullyQualifiedName(repository); + if(repoInfoMap.containsKey(fqn) || repoInfoMap.containsKey(fqn.substring(0, fqn.length()-1))) { + System.out.printf("Repository(%s) already processed\n", fqn); + continue; + } + + System.out.printf("Repository(%s) as kind:%s\n", repository.asType(), repository.getKind()); + TypeElement entityType = null; + if(repository instanceof TypeElement) { + TypeElement typeElement = (TypeElement) repository; + entityType = getEntityType(typeElement); + System.out.printf("\tRepository(%s) entityType(%s)\n", repository, entityType); + } + // If there + if(entityType == null) { + System.out.printf("Repository(%s) does not have an JPA entity type\n", repository); + continue; + } + // + newRepos = checkRespositoryForQBN(repository, entityType); + } + + // Generate repository interfaces for QBN methods + if(newRepos) { + for (Map.Entry entry : repoInfoMap.entrySet()) { + RepositoryInfo repoInfo = entry.getValue(); + System.out.printf("Generating repository interface for %s\n", entry.getKey()); + try { + AnnProcUtils.writeRepositoryInterface(repoInfo, processingEnv); + } catch (IOException e) { + processingEnv.getMessager().printMessage(javax.tools.Diagnostic.Kind.ERROR, e.getMessage()); + } + } + } + return true; + } + + private TypeElement getEntityType(TypeElement repo) { + // Check super interfaces for Repository + for (TypeMirror iface : repo.getInterfaces()) { + System.out.printf("\tRepository(%s) interface(%s)\n", repo, iface); + if (iface instanceof DeclaredType) { + DeclaredType declaredType = (DeclaredType) iface; + if(!declaredType.getTypeArguments().isEmpty()) { + TypeElement candidateType = (TypeElement) processingEnv.getTypeUtils().asElement(declaredType.getTypeArguments().get(0)); + Entity entity = candidateType.getAnnotation(Entity.class); + if (entity != null) { + System.out.printf("Repository(%s) entityType(%s)\n", repo, candidateType); + return candidateType; + } + } + } + } + // Look for lifecycle methods + for (Element e : repo.getEnclosedElements()) { + if (e instanceof ExecutableElement) { + ExecutableElement ee = (ExecutableElement) e; + if (isLifeCycleMethod(ee)) { + List params = ee.getParameters(); + for (VariableElement parameter : params) { + // Get the type of the parameter + TypeMirror parameterType = parameter.asType(); + + if (parameterType instanceof DeclaredType) { + DeclaredType declaredType = (DeclaredType) parameterType; + Entity entity = declaredType.getAnnotation(jakarta.persistence.Entity.class); + System.out.printf("%s, declaredType: %s\n", ee.getSimpleName(), declaredType, entity); + if(entity != null) { + System.out.printf("Repository(%s) entityType(%s)\n", repo, declaredType); + return (TypeElement) processingEnv.getTypeUtils().asElement(declaredType); + } + + // Get the type arguments + List typeArguments = declaredType.getTypeArguments(); + + for (TypeMirror typeArgument : typeArguments) { + TypeElement argType = (TypeElement) processingEnv.getTypeUtils().asElement(typeArgument); + Entity entity2 = argType.getAnnotation(jakarta.persistence.Entity.class); + System.out.printf("%s, typeArgument: %s, entity: %s\n", ee.getSimpleName(), typeArgument, entity2); + if(entity2 != null) { + System.out.printf("Repository(%s) entityType(%s)\n", repo, typeArgument); + return (TypeElement) processingEnv.getTypeUtils().asElement(typeArgument); + } + } + } + } + + } + } + } + + return null; + } + + private boolean isLifeCycleMethod(ExecutableElement method) { + return method.getAnnotation(Insert.class) != null + || method.getAnnotation(Update.class) != null + || method.getAnnotation(Save.class) != null + || method.getAnnotation(Delete.class) != null; + } + private boolean checkRespositoryForQBN(Element repository, TypeElement entityType) { + System.out.println("RespositoryProcessor: Checking repository for Query By Name"); + boolean addedRepo = false; + + String entityName = entityType.getQualifiedName().toString(); + List methods = AnnProcUtils.methodsIn(repository.getEnclosedElements()); + RepositoryInfo repoInfo = new RepositoryInfo(repository); + for (ExecutableElement m : methods) { + System.out.printf("\t%s\n", m.getSimpleName()); + QueryByNameInfo qbn = AnnProcUtils.isQBN(m); + if(qbn != null) { + qbn.setEntity(entityName); + repoInfo.addQBNMethod(m, qbn); + } + } + if(repoInfo.hasQBNMethods()) { + System.out.printf("Repository(%s) has QBN(%d) methods\n", repository, repoInfo.qbnMethods.size()); + repoInfoMap.put(AnnProcUtils.getFullyQualifiedName(repository), repoInfo); + addedRepo = true; + } + return addedRepo; + } + + private void generateQBNRepositoryInterfaces() { + for (Map.Entry entry : repoInfoMap.entrySet()) { + RepositoryInfo repoInfo = entry.getValue(); + System.out.printf("Generating repository interface for %s\n", entry.getKey()); + + } + } +} \ No newline at end of file diff --git a/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/ParseUtils.java b/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/ParseUtils.java index 33a7e1f3a..b9dd785c9 100644 --- a/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/ParseUtils.java +++ b/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/ParseUtils.java @@ -11,9 +11,11 @@ */ package ee.jakarta.tck.data.tools.qbyn; +import org.antlr.v4.runtime.BaseErrorListener; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CodePointCharStream; import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.tree.ErrorNode; import org.antlr.v4.runtime.tree.ParseTree; import ee.jakarta.tck.data.tools.antlr.QBNLexer; @@ -35,8 +37,18 @@ public static QueryByNameInfo parseQueryByName(String queryByName) { CommonTokenStream tokens = new CommonTokenStream(lexer); // create a parser that feeds off the tokens buffer QBNParser parser = new QBNParser(tokens); QueryByNameInfo info = new QueryByNameInfo(); + parser.addErrorListener(new BaseErrorListener() { + @Override + public void syntaxError(org.antlr.v4.runtime.Recognizer recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, org.antlr.v4.runtime.RecognitionException e) { + throw new IllegalArgumentException("Invalid query by name method name: " + queryByName); + } + }); parser.addParseListener(new QBNBaseListener() { - private StringBuffer property = new StringBuffer(); + @Override + public void visitErrorNode(ErrorNode node) { + throw new IllegalArgumentException("Invalid query by name method name: " + queryByName); + } + @Override public void exitPredicate(ee.jakarta.tck.data.tools.antlr.QBNParser.PredicateContext ctx) { @@ -44,7 +56,7 @@ public void exitPredicate(ee.jakarta.tck.data.tools.antlr.QBNParser.PredicateCon for (int i = 0; i < count; i++) { ee.jakarta.tck.data.tools.antlr.QBNParser.ConditionContext cctx = ctx.condition(i); String property = cctx.property().getText(); - QueryByNameInfo.Operator operator = QueryByNameInfo.Operator.NONE; + QueryByNameInfo.Operator operator = QueryByNameInfo.Operator.EQUAL; if(cctx.operator() != null) { operator = QueryByNameInfo.Operator.valueOf(cctx.operator().getText().toUpperCase()); } @@ -82,12 +94,12 @@ public void exitSubject(ee.jakarta.tck.data.tools.antlr.QBNParser.SubjectContext public void exitOrder_clause(ee.jakarta.tck.data.tools.antlr.QBNParser.Order_clauseContext ctx) { int count = ctx.order_item().size(); if(ctx.property() != null) { - String property = ctx.property().getText(); + String property = camelCase(ctx.property().getText()); info.addOrderBy(property, QueryByNameInfo.OrderBySortDirection.NONE); } for (int i = 0; i < count; i++) { ee.jakarta.tck.data.tools.antlr.QBNParser.Order_itemContext octx = ctx.order_item(i); - String property = octx.property().getText(); + String property = camelCase(octx.property().getText()); QueryByNameInfo.OrderBySortDirection direction = octx.ASC() != null ? QueryByNameInfo.OrderBySortDirection.ASC : QueryByNameInfo.OrderBySortDirection.DESC; info.addOrderBy(property, direction); } @@ -95,6 +107,158 @@ public void exitOrder_clause(ee.jakarta.tck.data.tools.antlr.QBNParser.Order_cla }); // Run the parser ParseTree tree = parser.query_method(); + return info; } + + public static String camelCase(String s) { + return s.substring(0, 1).toLowerCase() + s.substring(1); + } + + /** + * Convert a QueryByNameInfo object into a JDQL query string + * @param info - parse QBN info + * @return toQuery(info, false) + * @see #toQuery(QueryByNameInfo, boolean) + */ + public static String toQuery(QueryByNameInfo info) { + return toQuery(info, false); + } + /** + * Convert a QueryByNameInfo object into a JDQL query string + * @param info - parse QBN info + * @param includeOrderBy - if the order by clause should be included in the query + * @return the JDQL query string + */ + public static String toQuery(QueryByNameInfo info, boolean includeOrderBy) { + StringBuilder sb = new StringBuilder(); + int paramIdx = 1; + QueryByNameInfo.Action action = info.getAction(); + switch (action) { + case FIND: + break; + case DELETE: + sb.append("delete ").append(info.getEntity()).append(' '); + break; + case UPDATE: + sb.append("update ").append(info.getEntity()).append(' '); + break; + case COUNT: + sb.append("select count(this) "); + break; + case EXISTS: + sb.append("select count(this)>0 "); + break; + } + // + if(info.getPredicates().isEmpty()) { + return sb.toString(); + } + + sb.append("where "); + for(int n = 0; n < info.getPredicates().size(); n ++) { + QueryByNameInfo.Condition c = info.getPredicates().get(n); + + // EndWith -> right(property, length(?1)) = ?1 + if(c.operator == QueryByNameInfo.Operator.ENDSWITH) { + sb.append("right(").append(camelCase(c.property)) + .append(", length(?") + .append(paramIdx) + .append(")) = ?") + .append(paramIdx) + ; + paramIdx ++; + } + // StartsWith -> left(property, length(?1)) = ?1 + else if(c.operator == QueryByNameInfo.Operator.STARTSWITH) { + sb.append("left(").append(camelCase(c.property)) + .append(", length(?") + .append(paramIdx) + .append(")) = ?") + .append(paramIdx) + ; + paramIdx ++; + } + // Contains -> property like '%'||?1||'%' + else if(c.operator == QueryByNameInfo.Operator.CONTAINS) { + sb.append(camelCase(c.property)).append(" like '%'||?").append(paramIdx).append("||'%'"); + paramIdx++; + } + // Null + else if(c.operator == QueryByNameInfo.Operator.NULL) { + if(c.not) { + sb.append(camelCase(c.property)).append(" is not null"); + } else { + sb.append(camelCase(c.property)).append(" is null"); + } + } + // Empty + else if(c.operator == QueryByNameInfo.Operator.EMPTY) { + if(c.not) { + sb.append(camelCase(c.property)).append(" is not empty"); + } else { + sb.append(camelCase(c.property)).append(" is empty"); + } + } + // Other operators + else { + boolean ignoreCase = c.ignoreCase; + if(ignoreCase) { + sb.append("lower("); + } + sb.append(camelCase(c.property)); + if(ignoreCase) { + sb.append(")"); + } + if (c.operator == QueryByNameInfo.Operator.EQUAL && c.not) { + sb.append(" <>"); + } else { + if(c.not) { + sb.append(" not"); + } + String jdql = c.operator.getJDQL(); + sb.append(jdql); + } + // Other operators that need a parameter, add a placeholder + if (c.operator.parameters() > 0) { + if (ignoreCase) { + sb.append(" lower(?").append(paramIdx).append(")"); + } else { + sb.append(" ?").append(paramIdx); + } + paramIdx++; + if (c.operator.parameters() == 2) { + if (ignoreCase) { + sb.append(" and lower(?").append(paramIdx).append(")"); + } else { + sb.append(" and ?").append(paramIdx); + } + paramIdx++; + } + } + } + // See if we need to add an AND or OR + if(n < info.getPredicates().size()-1) { + // The and/or comes from next condition + boolean isAnd = info.getPredicates().get(n+1).and; + if (isAnd) { + sb.append(" and "); + } else { + sb.append(" or "); + } + } + } + + // If there is an orderBy clause, add it to query + if(includeOrderBy && !info.getOrderBy().isEmpty()) { + for (QueryByNameInfo.OrderBy ob : info.getOrderBy()) { + sb.append(" order by ").append(ob.property).append(' '); + if(ob.direction != QueryByNameInfo.OrderBySortDirection.NONE) { + sb.append(ob.direction.name().toLowerCase()); + } + } + } + + return sb.toString(); + } } diff --git a/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/QueryByNameInfo.java b/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/QueryByNameInfo.java index 1122b674c..4857f0882 100644 --- a/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/QueryByNameInfo.java +++ b/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/QueryByNameInfo.java @@ -31,9 +31,26 @@ public enum Action { * The support <operator> types */ public enum Operator { - CONTAINS, ENDSWITH, STARTSWITH, LESSTHAN, LESSTHANEQUAL, GREATERTHAN, - GREATERTHANEQUAL, BETWEEN , EMPTY , LIKE , IN , NULL, - TRUE , FALSE, NONE ; + CONTAINS("%||...||%"), ENDSWITH("right(...)"), STARTSWITH("left(...)"), LESSTHAN(" <"), LESSTHANEQUAL(" <="), + GREATERTHAN(" >"), GREATERTHANEQUAL(" >="), BETWEEN(" between", 2) , EMPTY(" empty") , + LIKE(" like") , IN(" in") , NULL(" null", 0), TRUE("=true", 0) , + FALSE("=false", 0), EQUAL(" =") + ; + private Operator(String jdql) { + this(jdql, 1); + } + private Operator(String jdql, int parameters) { + this.jdql = jdql; + this.parameters = parameters; + } + private String jdql; + private int parameters = 0; + public String getJDQL() { + return jdql; + } + public int parameters() { + return parameters; + } } public enum OrderBySortDirection { ASC, DESC, NONE @@ -46,7 +63,7 @@ public static class Condition { // an entity property name String property; // the operator to apply to the property - Operator operator = Operator.NONE; + Operator operator = Operator.EQUAL; // is the condition case-insensitive boolean ignoreCase; // is the condition negated @@ -60,9 +77,19 @@ public static class Condition { */ public static class OrderBy { // an entity property name - String property; + public String property; // the direction to sort the property - OrderBySortDirection direction = OrderBySortDirection.NONE; + public OrderBySortDirection direction = OrderBySortDirection.NONE; + + public OrderBy() { + } + public OrderBy(String property, OrderBySortDirection direction) { + this.property = property; + this.direction = direction; + } + public boolean isDescending() { + return direction == OrderBySortDirection.DESC; + } } private Action action = Action.NONE; private List predicates = new ArrayList<>(); @@ -70,6 +97,16 @@ public static class OrderBy { // >= 0 means find expression exists int findExpressionCount = -1; String ignoredText; + // The entity name + String entity; + + public String getEntity() { + return entity; + } + + public void setEntity(String entity) { + this.entity = entity; + } public Action getAction() { return action; @@ -158,6 +195,7 @@ public String toString() { boolean first = true; if(!predicates.isEmpty()) { for(Condition c : predicates) { + // Add the join condition if(!first) { sb.append(c.and ? "AND" : "OR"); } @@ -170,7 +208,7 @@ public String toString() { if(c.not) { sb.append("NOT"); } - if(c.operator != Operator.NONE) { + if(c.operator != Operator.EQUAL) { sb.append(c.operator.name().toUpperCase()); } sb.append(')'); diff --git a/tools/src/test/java/qbyn/QBNParserTest.java b/tools/src/test/java/qbyn/QBNParserTest.java index 0c6bc7743..d30e2dd6b 100644 --- a/tools/src/test/java/qbyn/QBNParserTest.java +++ b/tools/src/test/java/qbyn/QBNParserTest.java @@ -17,6 +17,7 @@ import org.antlr.v4.runtime.CodePointCharStream; import org.antlr.v4.runtime.CommonTokenStream; import org.antlr.v4.runtime.tree.ParseTree; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import ee.jakarta.tck.data.tools.antlr.QBNLexer; @@ -88,7 +89,7 @@ public void exitPredicate(QBNParser.PredicateContext ctx) { for (int i = 0; i < count; i++) { QBNParser.ConditionContext cctx = ctx.condition(i); String property = cctx.property().getText(); - QueryByNameInfo.Operator operator = QueryByNameInfo.Operator.NONE; + QueryByNameInfo.Operator operator = QueryByNameInfo.Operator.EQUAL; if(cctx.operator() != null) { operator = QueryByNameInfo.Operator.valueOf(cctx.operator().getText().toUpperCase()); } @@ -158,4 +159,292 @@ public void testParseUtils() { System.out.println(info); } } + + @Test + /** Should produce: + @Query("where floorOfSquareRoot <> ?1 and id < ?2") + @OrderBy("numBitsRequired", descending = true) + */ + public void test_findByFloorOfSquareRootNotAndIdLessThanOrderByNumBitsRequiredDesc() { + QueryByNameInfo info = ParseUtils.parseQueryByName("findByFloorOfSquareRootNotAndIdLessThanOrderByNumBitsRequiredDesc"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("where floorOfSquareRoot <> ?1 and id < ?2", query); + Assertions.assertEquals(1, info.getOrderBy().size()); + Assertions.assertEquals("numBitsRequired", info.getOrderBy().get(0).property); + Assertions.assertEquals(QueryByNameInfo.OrderBySortDirection.DESC, info.getOrderBy().get(0).direction); + } + + /** Should produce + @Query("where isOdd=true and id <= ?1") + @OrderBy(value = "id", descending = true) + */ + @Test + public void test_findByIsOddTrueAndIdLessThanEqualOrderByIdDesc() { + QueryByNameInfo info = ParseUtils.parseQueryByName("findByIsOddTrueAndIdLessThanEqualOrderByIdDesc"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("where isOdd=true and id <= ?1", query); + Assertions.assertTrue(info.getOrderBy().size() == 1); + Assertions.assertEquals("id", info.getOrderBy().get(0).property); + Assertions.assertEquals(QueryByNameInfo.OrderBySortDirection.DESC, info.getOrderBy().get(0).direction); + } + /** Should produce + @Query("where isOdd=false and id between ?1 and ?2") + */ + @Test + public void test_findByIsOddFalseAndIdBetween() { + QueryByNameInfo info = ParseUtils.parseQueryByName("findByIsOddFalseAndIdBetween"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("where isOdd=false and id between ?1 and ?2", query); + Assertions.assertEquals(0, info.getOrderBy().size()); + } + /** Should produce + @Query("where numType in ?1 order by id asc") + */ + @Test + public void test_findByNumTypeInOrderByIdAsc() { + QueryByNameInfo info = ParseUtils.parseQueryByName("findByNumTypeInOrderByIdAsc"); + String query = ParseUtils.toQuery(info, true); + System.out.println(query); + Assertions.assertEquals("where numType in ?1 order by id asc", query); + Assertions.assertEquals(1, info.getOrderBy().size()); + Assertions.assertEquals(QueryByNameInfo.OrderBySortDirection.ASC, info.getOrderBy().get(0).direction); + } + + /** Should produce + @Query("where numType = ?1 or floorOfSquareRoot = ?2") + */ + @Test + public void test_findByNumTypeOrFloorOfSquareRoot() { + QueryByNameInfo info = ParseUtils.parseQueryByName("findByNumTypeOrFloorOfSquareRoot"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("where numType = ?1 or floorOfSquareRoot = ?2", query); + Assertions.assertEquals(0, info.getOrderBy().size()); + } + + /** Should produce + @Query("where numType <> ?1") + */ + @Test + public void test_findByNumTypeNot() { + QueryByNameInfo info = ParseUtils.parseQueryByName("findByNumTypeNot"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("where numType <> ?1", query); + Assertions.assertEquals(0, info.getOrderBy().size()); + } + + + /** Should produce + @Query("where numType = ?1 and numBitsRequired < ?2") + */ + @Test + public void test_findByNumTypeAndNumBitsRequiredLessThan() { + QueryByNameInfo info = ParseUtils.parseQueryByName("findByNumTypeAndNumBitsRequiredLessThan"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("where numType = ?1 and numBitsRequired < ?2", query); + Assertions.assertEquals(0, info.getOrderBy().size()); + } + + + /** Should produce + @Query("where id between ?1 and ?2") + */ + @Test + public void test_findByIdBetweenOrderByNumTypeAsc() { + QueryByNameInfo info = ParseUtils.parseQueryByName("findByIdBetweenOrderByNumTypeAsc"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("where id between ?1 and ?2", query); + Assertions.assertEquals(1, info.getOrderBy().size()); + Assertions.assertEquals(QueryByNameInfo.OrderBySortDirection.ASC, info.getOrderBy().get(0).direction); + } + + + /** Should produce + @Query("where lower(hexadecimal) between lower(?1) and lower(?2) and hexadecimal not in ?3") + */ + @Test + public void test_findByHexadecimalIgnoreCaseBetweenAndHexadecimalNotIn() { + QueryByNameInfo info = ParseUtils.parseQueryByName("findByHexadecimalIgnoreCaseBetweenAndHexadecimalNotIn"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("where lower(hexadecimal) between lower(?1) and lower(?2) and hexadecimal not in ?3", query); + Assertions.assertEquals(0, info.getOrderBy().size()); + } + + /** Should produce + @Query("where numericValue >= ?1 and right(hexadecimal, length(?2)) = ?2") + */ + @Test + public void test_findByNumericValueGreaterThanEqualAndHexadecimalEndsWith() { + QueryByNameInfo info = ParseUtils.parseQueryByName("findByNumericValueGreaterThanEqualAndHexadecimalEndsWith"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("where numericValue >= ?1 and right(hexadecimal, length(?2)) = ?2", query); + Assertions.assertEquals(0, info.getOrderBy().size()); + } + /** Should produce + @Query("where left(hexadecimal, length(?1)) = ?1 and isControl = ?2 order by id asc") + */ + @Test + public void test_findByHexadecimalStartsWithAndIsControlOrderByIdAsc() { + QueryByNameInfo info = ParseUtils.parseQueryByName("findByHexadecimalStartsWithAndIsControlOrderByIdAsc"); + String query = ParseUtils.toQuery(info, true); + System.out.println(query); + Assertions.assertEquals("where left(hexadecimal, length(?1)) = ?1 and isControl = ?2 order by id asc", query); + Assertions.assertEquals(1, info.getOrderBy().size()); + } + + /** Should produce + @Query("where name like ?1") + */ + @Test + public void test_findByNameLike() { + QueryByNameInfo info = ParseUtils.parseQueryByName("findByNameLike"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("where name like ?1", query); + Assertions.assertEquals(0, info.getOrderBy().size()); + } + + + /** Should produce + @Query("where hexadecimal like '%'||?1||'%' and isControl <> ?2") + */ + @Test + public void test_findByHexadecimalContainsAndIsControlNot() { + QueryByNameInfo info = ParseUtils.parseQueryByName("findByHexadecimalContainsAndIsControlNot"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("where hexadecimal like '%'||?1||'%' and isControl <> ?2", query); + Assertions.assertEquals(0, info.getOrderBy().size()); + } + + + /** Should produce + @Query("where price is not null and price <= ?1") + */ + @Test + public void test_findByPriceNotNullAndPriceLessThanEqual() { + QueryByNameInfo info = ParseUtils.parseQueryByName("findByPriceNotNullAndPriceLessThanEqual"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("where price is not null and price <= ?1", query); + Assertions.assertEquals(0, info.getOrderBy().size()); + } + + /** Should produce + @Query("where price is null") + */ + @Test + public void test_findByPriceNull() { + QueryByNameInfo info = ParseUtils.parseQueryByName("findByPriceNull"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("where price is null", query); + Assertions.assertEquals(0, info.getOrderBy().size()); + } + + + /** Should produce + @Query("where departments is empty") + */ + @Test + public void test_findByDepartmentsEmpty() { + QueryByNameInfo info = ParseUtils.parseQueryByName("findByDepartmentsEmpty"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("where departments is empty", query); + Assertions.assertEquals(0, info.getOrderBy().size()); + } + + @Test + public void test_countBy() { + IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> { + QueryByNameInfo info = ParseUtils.parseQueryByName("countBy"); + }); + Assertions.assertNotNull(ex, "parse of countBy should fail"); + } + + /** Should produce + @Query("delete Product where productNum like ?1") + */ + @Test + public void test_deleteByProductNumLike() { + QueryByNameInfo info = ParseUtils.parseQueryByName("deleteByProductNumLike"); + info.setEntity("Product"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("delete Product where productNum like ?1", query); + Assertions.assertEquals(0, info.getOrderBy().size()); + + } + + /** Should produce + @Query("select count(this)>0 where thisCharacter = ?1") + */ + @Test + public void test_existsByThisCharacter() { + QueryByNameInfo info = ParseUtils.parseQueryByName("existsByThisCharacter"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("select count(this)>0 where thisCharacter = ?1", query); + Assertions.assertEquals(0, info.getOrderBy().size()); + } + + /** Should produce + @Query("select count(this) where hexadecimal is not null") + */ + @Test + public void test_countByHexadecimalNotNull() { + QueryByNameInfo info = ParseUtils.parseQueryByName("countByHexadecimalNotNull"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("select count(this) where hexadecimal is not null", query); + Assertions.assertEquals(0, info.getOrderBy().size()); + } + + + /** Should produce + @Query("select count(this) where id(this) < ?1") + */ + @Test + public void test_countByIdLessThan() { + QueryByNameInfo info = ParseUtils.parseQueryByName("countByIdLessThan"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("select count(this) where id(this) < ?1", query); + Assertions.assertEquals(0, info.getOrderBy().size()); + } + + /** Should produce + @Query("select count(this)>0 where id in ?1") + */ + @Test + public void test_existsByIdIn() { + QueryByNameInfo info = ParseUtils.parseQueryByName("existsByIdIn"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("select count(this)>0 where id in ?1", query); + Assertions.assertEquals(0, info.getOrderBy().size()); + } + + + /** Should produce + @Query("select count(this)>0 where id > ?1") + */ + @Test + public void test_existsByIdGreaterThan() { + QueryByNameInfo info = ParseUtils.parseQueryByName("existsByIdGreaterThan"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("select count(this)>0 where id > ?1", query); + Assertions.assertEquals(0, info.getOrderBy().size()); + } + } diff --git a/tools/src/test/java/qbyn/ST4RepoGenTest.java b/tools/src/test/java/qbyn/ST4RepoGenTest.java new file mode 100644 index 000000000..7aa1dc534 --- /dev/null +++ b/tools/src/test/java/qbyn/ST4RepoGenTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2024 Contributors to the Eclipse Foundation + * + * Licensed 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 + * + * SPDX-License-Identifier: Apache-2.0 + */ +package qbyn; + +import ee.jakarta.tck.data.tools.annp.RepositoryInfo; +import ee.jakarta.tck.data.tools.qbyn.QueryByNameInfo; +import org.junit.jupiter.api.Test; +import org.stringtemplate.v4.ST; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class ST4RepoGenTest { + static String REPO_TEMPLATE = """ + import jakarta.annotation.Generated; + import jakarta.data.repository.OrderBy; + import jakarta.data.repository.Query; + import jakarta.data.repository.Repository; + import #repo.fqn#; + + @Repository(dataStore = "#repo.dataStore#") + @Generated("ee.jakarta.tck.data.tools.annp.RespositoryProcessor") + public interface #repo.name#$ extends #repo.name# { + #repo.methods :{m | + @Override + @Query("#m.query#") + #m.orderBy :{o | @OrderBy(value="#o.property#", descending = #o.descending#)}# + public #m.returnType# #m.name# (#m.parameters: {p | #p#}; separator=", "#); + + } + # + } + """; + @Test + public void testSyntax() { + List methods = Arrays.asList("findByFloorOfSquareRootOrderByIdAsc", "findByHexadecimalIgnoreCase", + "findById", "findByIdBetween", "findByHexadecimalIgnoreCaseBetweenAndHexadecimalNotIn"); + ST s = new ST( " ();\n}>"); + s.add("methods", methods); + System.out.println(s.render()); + } + @Test + public void testRepoGen() { + RepositoryInfo repo = new RepositoryInfo(); + repo.setFqn("org.acme.BookRepository"); + repo.setName("BookRepository"); + repo.setDataStore("book"); + + RepositoryInfo.MethodInfo findByTitleLike = new RepositoryInfo.MethodInfo("findByTitleLike", "List", "from Book where title like :title", null); + findByTitleLike.addParameter("String title"); + repo.addMethod(findByTitleLike); + RepositoryInfo.MethodInfo findByNumericValue = new RepositoryInfo.MethodInfo("findByNumericValue", "Optional", + "from AsciiCharacter where numericValue = :numericValue", + Collections.singletonList(new QueryByNameInfo.OrderBy("numericValue", QueryByNameInfo.OrderBySortDirection.ASC))); + findByNumericValue.addParameter("int id"); + repo.addMethod(findByNumericValue); + ST st = new ST(REPO_TEMPLATE, '#', '#'); + st.add("repo", repo); + System.out.println(st.render()); + } +} From 9bfb04f93260e60af2041b77c10655f5cd9b809c Mon Sep 17 00:00:00 2001 From: Scott M Stark Date: Tue, 2 Apr 2024 23:05:51 -0600 Subject: [PATCH 3/4] Correct package and repo processor rerun check Signed-off-by: Scott M Stark --- .../jakarta/tck/data/tools/annp/AnnProcUtils.java | 1 + .../jakarta/tck/data/tools/annp/RepositoryInfo.java | 13 +++++++++++++ .../tck/data/tools/annp/RespositoryProcessor.java | 3 ++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/tools/src/main/java/ee/jakarta/tck/data/tools/annp/AnnProcUtils.java b/tools/src/main/java/ee/jakarta/tck/data/tools/annp/AnnProcUtils.java index 38932f347..a4c3abdc4 100644 --- a/tools/src/main/java/ee/jakarta/tck/data/tools/annp/AnnProcUtils.java +++ b/tools/src/main/java/ee/jakarta/tck/data/tools/annp/AnnProcUtils.java @@ -35,6 +35,7 @@ public class AnnProcUtils { static String REPO_TEMPLATE = """ + package #repo.pkg#; import jakarta.annotation.Generated; import jakarta.data.repository.OrderBy; import jakarta.data.repository.Query; diff --git a/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RepositoryInfo.java b/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RepositoryInfo.java index 0af03d388..43aa8c0c5 100644 --- a/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RepositoryInfo.java +++ b/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RepositoryInfo.java @@ -73,6 +73,7 @@ public List getOrderBy() { } private Element repositoryElement; private String fqn; + private String pkg; private String name; private String dataStore = ""; private ArrayList methods = new ArrayList<>(); @@ -97,6 +98,18 @@ public String getFqn() { public void setFqn(String fqn) { this.fqn = fqn; + int index = fqn.lastIndexOf("."); + if(index > 0) { + setPkg(fqn.substring(0, index)); + } + } + + public String getPkg() { + return pkg; + } + + public void setPkg(String pkg) { + this.pkg = pkg; } public String getName() { diff --git a/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RespositoryProcessor.java b/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RespositoryProcessor.java index 28e2af519..08e795bf8 100644 --- a/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RespositoryProcessor.java +++ b/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RespositoryProcessor.java @@ -52,6 +52,7 @@ public boolean process(Set annotations, RoundEnvironment Set repositories = roundEnv.getElementsAnnotatedWith(Repository.class); for (Element repository : repositories) { String fqn = AnnProcUtils.getFullyQualifiedName(repository); + System.out.printf("Processing repository %s\n", fqn); if(repoInfoMap.containsKey(fqn) || repoInfoMap.containsKey(fqn.substring(0, fqn.length()-1))) { System.out.printf("Repository(%s) already processed\n", fqn); continue; @@ -70,7 +71,7 @@ public boolean process(Set annotations, RoundEnvironment continue; } // - newRepos = checkRespositoryForQBN(repository, entityType); + newRepos |= checkRespositoryForQBN(repository, entityType); } // Generate repository interfaces for QBN methods From 39569309378acbbb176a46364d140a11952793de Mon Sep 17 00:00:00 2001 From: Scott M Stark Date: Wed, 17 Apr 2024 23:25:49 -0600 Subject: [PATCH 4/4] Updated to latest QBN processor Signed-off-by: Scott M Stark --- tools/pom.xml | 66 ++++----- tools/src/main/antlr4/QBN.g4 | 32 +++-- .../tck/data/tools/annp/AnnProcUtils.java | 129 ++++++++++++------ .../tck/data/tools/annp/RepositoryInfo.java | 48 ++++++- .../data/tools/annp/RespositoryProcessor.java | 100 +++++++++----- .../tck/data/tools/qbyn/ParseUtils.java | 80 ++++++++--- .../tck/data/tools/qbyn/QueryByNameInfo.java | 19 ++- tools/src/main/resources/RepoTemplate.stg | 37 +++++ tools/src/test/java/qbyn/QBNParserTest.java | 107 +++++++++++++-- tools/src/test/java/qbyn/ST4RepoGenTest.java | 75 +++++++++- .../resources/org.acme.BookRepository_tck.stg | 11 ++ 11 files changed, 540 insertions(+), 164 deletions(-) create mode 100644 tools/src/main/resources/RepoTemplate.stg create mode 100644 tools/src/test/resources/org.acme.BookRepository_tck.stg diff --git a/tools/pom.xml b/tools/pom.xml index 1a5aa8b89..12ac1bc05 100644 --- a/tools/pom.xml +++ b/tools/pom.xml @@ -21,20 +21,39 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - - jakarta.data - jakarta.data-parent - 1.0.0-SNAPSHOT - + jakarta.data jakarta.data-tools + 1.0.0-SNAPSHOT Jakarta Data Tools - Jakarta Data :: Tools 4.13.1 + 1.8.0.Final + 5.10.2 + 17 + 17 + + + + org.junit + junit-bom + ${junit.version} + pom + import + + + org.jboss.arquillian + arquillian-bom + ${arquillian.version} + pom + import + + + + @@ -100,40 +119,7 @@ - - - org.apache.maven.plugins - maven-checkstyle-plugin - ${maven.checkstyle.plugin.version} - - true - - - - - org.apache.maven.plugins - maven-javadoc-plugin - - - target/generated-sources/** - true - - ee.jakarta.tck.data.tools.antlr - - - - attach-javadocs - - jar - - - - + diff --git a/tools/src/main/antlr4/QBN.g4 b/tools/src/main/antlr4/QBN.g4 index 25bd0640e..01936a7c4 100644 --- a/tools/src/main/antlr4/QBN.g4 +++ b/tools/src/main/antlr4/QBN.g4 @@ -21,11 +21,12 @@ grammar QBN; package ee.jakarta.tck.data.tools.antlr; } -query_method : subject predicate order_clause? ; +query_method : find_query | action_query ; -subject : (action | find find_expression) ignored_text? BY ; +find_query : find limit? ignored_text? restriction? order? ; +action_query : action ignored_text? restriction? ; -action : find | delete | update | count | exists ; +action : delete | update | count | exists ; find : 'find' ; delete : 'delete' ; @@ -33,7 +34,9 @@ update : 'update' ; count : 'count' ; exists : 'exists' ; -find_expression : FIRST INTEGER? ; +restriction : BY predicate ; + +limit : FIRST INTEGER? ; predicate : condition ( (AND | OR) condition )* ; @@ -41,12 +44,25 @@ condition : property ignore_case? not? operator? ; ignore_case : IGNORE_CASE ; not : NOT ; -operator : CONTAINS | ENDSWITH | STARTSWITH | LESSTHAN| LESSTHANEQUAL | GREATERTHAN | - GREATERTHANEQUAL | BETWEEN | EMPTY | LIKE | IN | NULL | - TRUE | FALSE ; +operator + : CONTAINS + | ENDSWITH + | STARTSWITH + | LESSTHAN + | LESSTHANEQUAL + | GREATERTHAN + | GREATERTHANEQUAL + | BETWEEN + | EMPTY + | LIKE + | IN + | NULL + | TRUE + | FALSE + ; property : (IDENTIFIER | IDENTIFIER '_' property)+ ; -order_clause : ORDER_BY ( order_item )* ( order_item | property ) ; +order : ORDER_BY ( property | order_item+) ; order_item : property ( ASC | DESC ) ; diff --git a/tools/src/main/java/ee/jakarta/tck/data/tools/annp/AnnProcUtils.java b/tools/src/main/java/ee/jakarta/tck/data/tools/annp/AnnProcUtils.java index a4c3abdc4..640116d24 100644 --- a/tools/src/main/java/ee/jakarta/tck/data/tools/annp/AnnProcUtils.java +++ b/tools/src/main/java/ee/jakarta/tck/data/tools/annp/AnnProcUtils.java @@ -13,7 +13,15 @@ import ee.jakarta.tck.data.tools.qbyn.ParseUtils; import ee.jakarta.tck.data.tools.qbyn.QueryByNameInfo; +import jakarta.data.repository.Delete; +import jakarta.data.repository.Find; +import jakarta.data.repository.Insert; +import jakarta.data.repository.Query; +import jakarta.data.repository.Save; +import jakarta.data.repository.Update; import org.stringtemplate.v4.ST; +import org.stringtemplate.v4.STGroup; +import org.stringtemplate.v4.STGroupFile; import javax.annotation.processing.Filer; import javax.annotation.processing.ProcessingEnvironment; @@ -21,50 +29,75 @@ import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; -import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.ExecutableType; -import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; -import javax.lang.model.type.TypeVariable; import javax.tools.JavaFileObject; import java.io.IOException; import java.io.Writer; +import java.net.URL; import java.util.ArrayList; import java.util.List; public class AnnProcUtils { - static String REPO_TEMPLATE = """ - package #repo.pkg#; - import jakarta.annotation.Generated; - import jakarta.data.repository.OrderBy; - import jakarta.data.repository.Query; - import jakarta.data.repository.Repository; - import #repo.fqn#; + // The name of the template for the TCK override imports + public static final String TCK_IMPORTS = "/tckImports"; + // The name of the template for the TCK overrides + public static final String TCK_OVERRIDES = "/tckOverrides"; - @Repository(dataStore = "#repo.dataStore#") - @Generated("ee.jakarta.tck.data.tools.annp.RespositoryProcessor") - public interface #repo.name#$ extends #repo.name# { - #repo.methods :{m | - @Override - @Query("#m.query#") - #m.orderBy :{o | @OrderBy(value="#o.property#", descending = #o.descending#)}# - public #m.returnType# #m.name# (#m.parameters: {p | #p#}; separator=", "#); - - } - # + /** + * Get a list of non-lifecycle methods in a type element. This will also process superinterfaces + * @param typeElement a repository interface + * @return a list of non-lifecycle methods as candidate repository methods + */ + public static List methodsIn(TypeElement typeElement) { + ArrayList methods = new ArrayList<>(); + List typeMethods = methodsIn(typeElement.getEnclosedElements()); + methods.addAll(typeMethods); + List superifaces = typeElement.getInterfaces(); + for (TypeMirror iface : superifaces) { + if(iface instanceof DeclaredType) { + DeclaredType dt = (DeclaredType) iface; + System.out.printf("Processing superinterface %s<%s>\n", dt.asElement(), dt.getTypeArguments()); + methods.addAll(methodsIn((TypeElement) dt.asElement())); } - """; + } + return methods; + } + /** + * Get a list of non-lifecycle methods in a list of repository elements + * @param elements - a list of repository elements + * @return possibly empty list of non-lifecycle methods + */ public static List methodsIn(Iterable elements) { ArrayList methods = new ArrayList<>(); for (Element e : elements) { if(e.getKind() == ElementKind.METHOD) { - methods.add((ExecutableElement) e); + ExecutableElement method = (ExecutableElement) e; + // Skip lifecycle methods + if(!isLifeCycleMethod(method)) { + methods.add(method); + } } } return methods; } + + /** + * Is a method annotated with a lifecycle or Query annotation + * @param method a repository method + * @return true if the method is a lifecycle method + */ + public static boolean isLifeCycleMethod(ExecutableElement method) { + boolean standardLifecycle = method.getAnnotation(Insert.class) != null + || method.getAnnotation(Find.class) != null + || method.getAnnotation(Update.class) != null + || method.getAnnotation(Save.class) != null + || method.getAnnotation(Delete.class) != null + || method.getAnnotation(Query.class) != null; + return standardLifecycle; + } + public static String getFullyQualifiedName(Element element) { if (element instanceof TypeElement) { return ((TypeElement) element).getQualifiedName().toString(); @@ -74,26 +107,46 @@ public static String getFullyQualifiedName(Element element) { public static QueryByNameInfo isQBN(ExecutableElement m) { - QueryByNameInfo info = null; String methodName = m.getSimpleName().toString(); - if(methodName.startsWith("findBy") || methodName.startsWith("deleteBy") || methodName.startsWith("updateBy") - || methodName.startsWith("countBy") || methodName.startsWith("existsBy") ) { - try { - info = ParseUtils.parseQueryByName(methodName); - } catch (Throwable e) { - System.out.printf("Failed to parse %s: %s\n", methodName, e.getMessage()); - return null; - } - return info; + try { + return ParseUtils.parseQueryByName(methodName); + } + catch (Throwable e) { + System.out.printf("Failed to parse %s: %s\n", methodName, e.getMessage()); } - return null; } + /** + * Write a repository interface to a source file using the {@linkplain RepositoryInfo}. This uses the + * RepoTemplate.stg template file to generate the source code. It also looks for a + * + * @param repo - parsed repository info + * @param processingEnv - the processing environment + * @throws IOException - if the file cannot be written + */ public static void writeRepositoryInterface(RepositoryInfo repo, ProcessingEnvironment processingEnv) throws IOException { - ST st = new ST(REPO_TEMPLATE, '#', '#'); - st.add("repo", repo); - String ifaceSrc = st.render(); + STGroup repoGroup = new STGroupFile("RepoTemplate.stg"); + ST genRepo = repoGroup.getInstanceOf("genRepo"); + try { + URL stgURL = AnnProcUtils.class.getResource("/"+repo.getFqn()+".stg"); + STGroup tckGroup = new STGroupFile(stgURL); + long count = tckGroup.getTemplateNames().stream().filter(t -> t.equals(TCK_IMPORTS) | t.equals(TCK_OVERRIDES)).count(); + if(count != 2) { + System.out.printf("No TCK overrides for %s\n", repo.getFqn()); + } else { + tckGroup.importTemplates(repoGroup); + System.out.printf("Found TCK overrides(%s) for %s\n", tckGroup.getRootDirURL(), repo.getFqn()); + System.out.printf("tckGroup: %s\n", tckGroup.show()); + genRepo = tckGroup.getInstanceOf("genRepo"); + } + } catch (IllegalArgumentException e) { + System.out.printf("No TCK overrides for %s\n", repo.getFqn()); + } + + genRepo.add("repo", repo); + + String ifaceSrc = genRepo.render(); String ifaceName = repo.getFqn() + "$"; Filer filer = processingEnv.getFiler(); JavaFileObject srcFile = filer.createSourceFile(ifaceName, repo.getRepositoryElement()); diff --git a/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RepositoryInfo.java b/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RepositoryInfo.java index 43aa8c0c5..1848c9f2e 100644 --- a/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RepositoryInfo.java +++ b/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RepositoryInfo.java @@ -18,10 +18,14 @@ import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.util.Types; import java.util.ArrayList; import java.util.List; + public class RepositoryInfo { public static class MethodInfo { String name; @@ -129,16 +133,52 @@ public void setDataStore(String dataStore) { } - public void addQBNMethod(ExecutableElement m, QueryByNameInfo info) { + /** + * Add a Query By Name method to the repository + * @param m - the method + * @param info - parsed QBN info + * @param types - annotation processing types utility + */ + public void addQBNMethod(ExecutableElement m, QueryByNameInfo info, Types types) { qbnMethods.add(m); - String query = ParseUtils.toQuery(info); - StringBuilder orderBy = new StringBuilder(); - MethodInfo mi = new MethodInfo(m.getSimpleName().toString(), m.getReturnType().toString(), query, info.getOrderBy()); + // Deal with generics + DeclaredType returnType = null; + if(m.getReturnType() instanceof DeclaredType) { + returnType = (DeclaredType) m.getReturnType(); + } + String returnTypeStr = returnType == null ? m.getReturnType().toString() : toString(returnType); + System.out.printf("addQBNMethod: %s, returnType: %s, returnTypeStr: %s\n", + m.getSimpleName().toString(), returnType, returnTypeStr); + ParseUtils.ToQueryOptions options = ParseUtils.ToQueryOptions.NONE; + String methodName = m.getSimpleName().toString(); + // Select the appropriate cast option if this is a countBy method + if(methodName.startsWith("count")) { + options = switch (returnTypeStr) { + case "long" -> ParseUtils.ToQueryOptions.CAST_LONG_TO_INTEGER; + case "int" -> ParseUtils.ToQueryOptions.CAST_COUNT_TO_INTEGER; + default -> ParseUtils.ToQueryOptions.NONE; + }; + } + // Build the query string + String query = ParseUtils.toQuery(info, options); + + MethodInfo mi = new MethodInfo(methodName, m.getReturnType().toString(), query, info.getOrderBy()); for (VariableElement p : m.getParameters()) { mi.addParameter(p.asType().toString() + " " + p.getSimpleName()); } addMethod(mi); } + public String toString(DeclaredType tm) { + StringBuilder buf = new StringBuilder(); + TypeElement returnTypeElement = (TypeElement) tm.asElement(); + buf.append(returnTypeElement.getQualifiedName()); + if (!tm.getTypeArguments().isEmpty()) { + buf.append('<'); + buf.append(tm.getTypeArguments().toString()); + buf.append(">"); + } + return buf.toString(); + } public List getQBNMethods() { return qbnMethods; } diff --git a/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RespositoryProcessor.java b/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RespositoryProcessor.java index 08e795bf8..cbd621bf8 100644 --- a/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RespositoryProcessor.java +++ b/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RespositoryProcessor.java @@ -12,11 +12,13 @@ package ee.jakarta.tck.data.tools.annp; import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedOptions; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.TypeElement; @@ -27,51 +29,62 @@ import java.util.Set; import ee.jakarta.tck.data.tools.qbyn.QueryByNameInfo; -import jakarta.data.repository.Delete; -import jakarta.data.repository.Insert; import jakarta.data.repository.Repository; -import jakarta.data.repository.Save; -import jakarta.data.repository.Update; import jakarta.persistence.Entity; import javax.lang.model.element.VariableElement; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; +/** + * Annotation processor for {@link Repository} annotations that creates sub-interfaces for repositories + * that use Query By Name (QBN) methods. + */ @SupportedAnnotationTypes("jakarta.data.repository.Repository") @SupportedSourceVersion(SourceVersion.RELEASE_17) -@SupportedOptions({"debug"}) +@SupportedOptions({"debug", "generatedSourcesDirectory"}) public class RespositoryProcessor extends AbstractProcessor { private Map repoInfoMap = new HashMap<>(); + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + processingEnv.getOptions(); + } + @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { System.out.printf("RespositoryProcessor: Processing repositories, over=%s\n", roundEnv.processingOver()); boolean newRepos = false; Set repositories = roundEnv.getElementsAnnotatedWith(Repository.class); for (Element repository : repositories) { - String fqn = AnnProcUtils.getFullyQualifiedName(repository); - System.out.printf("Processing repository %s\n", fqn); - if(repoInfoMap.containsKey(fqn) || repoInfoMap.containsKey(fqn.substring(0, fqn.length()-1))) { - System.out.printf("Repository(%s) already processed\n", fqn); - continue; - } + String provider = repository.getAnnotation(Repository.class).provider(); + if(provider.isEmpty() || provider.equalsIgnoreCase("hibernate")) { + String fqn = AnnProcUtils.getFullyQualifiedName(repository); + System.out.printf("Processing repository %s\n", fqn); + if(repoInfoMap.containsKey(fqn) || repoInfoMap.containsKey(fqn.substring(0, fqn.length()-1))) { + System.out.printf("Repository(%s) already processed\n", fqn); + continue; + } - System.out.printf("Repository(%s) as kind:%s\n", repository.asType(), repository.getKind()); - TypeElement entityType = null; - if(repository instanceof TypeElement) { - TypeElement typeElement = (TypeElement) repository; - entityType = getEntityType(typeElement); - System.out.printf("\tRepository(%s) entityType(%s)\n", repository, entityType); - } - // If there - if(entityType == null) { - System.out.printf("Repository(%s) does not have an JPA entity type\n", repository); - continue; + System.out.printf("Repository(%s) as kind:%s\n", repository.asType(), repository.getKind()); + TypeElement entityType = null; + TypeElement repositoryType = null; + if(repository instanceof TypeElement) { + repositoryType = (TypeElement) repository; + entityType = getEntityType(repositoryType); + System.out.printf("\tRepository(%s) entityType(%s)\n", repository, entityType); + } + // If there + if(entityType == null) { + System.out.printf("Repository(%s) does not have an JPA entity type\n", repository); + continue; + } + // + newRepos |= checkRespositoryForQBN(repositoryType, entityType, processingEnv.getTypeUtils()); } - // - newRepos |= checkRespositoryForQBN(repository, entityType); } // Generate repository interfaces for QBN methods @@ -90,6 +103,9 @@ public boolean process(Set annotations, RoundEnvironment } private TypeElement getEntityType(TypeElement repo) { + if(repo.getQualifiedName().toString().equals("ee.jakarta.tck.data.common.cdi.Directory")) { + System.out.println("Directory"); + } // Check super interfaces for Repository for (TypeMirror iface : repo.getInterfaces()) { System.out.printf("\tRepository(%s) interface(%s)\n", repo, iface); @@ -101,6 +117,18 @@ private TypeElement getEntityType(TypeElement repo) { if (entity != null) { System.out.printf("Repository(%s) entityType(%s)\n", repo, candidateType); return candidateType; + } else { + // Look for custom Entity types based on '*Entity' naming convention + // A qualifier annotation would be better, see https://github.com/jakartaee/data/issues/638 + List x = candidateType.getAnnotationMirrors(); + for (AnnotationMirror am : x) { + DeclaredType dt = am.getAnnotationType(); + String annotationName = dt.asElement().getSimpleName().toString(); + if(annotationName.endsWith("Entity")) { + System.out.printf("Repository(%s) entityType(%s) from custom annotation:(%s)\n", repo, candidateType, annotationName); + return candidateType; + } + } } } } @@ -109,7 +137,7 @@ private TypeElement getEntityType(TypeElement repo) { for (Element e : repo.getEnclosedElements()) { if (e instanceof ExecutableElement) { ExecutableElement ee = (ExecutableElement) e; - if (isLifeCycleMethod(ee)) { + if (AnnProcUtils.isLifeCycleMethod(ee)) { List params = ee.getParameters(); for (VariableElement parameter : params) { // Get the type of the parameter @@ -146,31 +174,35 @@ private TypeElement getEntityType(TypeElement repo) { return null; } - private boolean isLifeCycleMethod(ExecutableElement method) { - return method.getAnnotation(Insert.class) != null - || method.getAnnotation(Update.class) != null - || method.getAnnotation(Save.class) != null - || method.getAnnotation(Delete.class) != null; - } - private boolean checkRespositoryForQBN(Element repository, TypeElement entityType) { + + /** + * Check a repository for Query By Name methods, and create a {@link RepositoryInfo} object if found. + * @param repository a repository element + * @param entityType the entity type for the repository + * @return true if the repository has QBN methods + */ + private boolean checkRespositoryForQBN(TypeElement repository, TypeElement entityType, Types types) { System.out.println("RespositoryProcessor: Checking repository for Query By Name"); boolean addedRepo = false; String entityName = entityType.getQualifiedName().toString(); - List methods = AnnProcUtils.methodsIn(repository.getEnclosedElements()); + List methods = AnnProcUtils.methodsIn(repository); RepositoryInfo repoInfo = new RepositoryInfo(repository); for (ExecutableElement m : methods) { System.out.printf("\t%s\n", m.getSimpleName()); QueryByNameInfo qbn = AnnProcUtils.isQBN(m); if(qbn != null) { qbn.setEntity(entityName); - repoInfo.addQBNMethod(m, qbn); + repoInfo.addQBNMethod(m, qbn, types); } + } if(repoInfo.hasQBNMethods()) { System.out.printf("Repository(%s) has QBN(%d) methods\n", repository, repoInfo.qbnMethods.size()); repoInfoMap.put(AnnProcUtils.getFullyQualifiedName(repository), repoInfo); addedRepo = true; + } else { + System.out.printf("Repository(%s) has NO QBN methods\n", repository); } return addedRepo; } diff --git a/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/ParseUtils.java b/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/ParseUtils.java index b9dd785c9..e99ed71ea 100644 --- a/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/ParseUtils.java +++ b/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/ParseUtils.java @@ -22,10 +22,25 @@ import ee.jakarta.tck.data.tools.antlr.QBNParser; import ee.jakarta.tck.data.tools.antlr.QBNBaseListener; +import java.util.Arrays; +import java.util.HashSet; + /** * A utility class for parsing query by name method names using the Antlr4 generated parser */ public class ParseUtils { + /** + * Options for the toQuery method + */ + public enum ToQueryOptions { + INCLUDE_ORDER_BY, + // select cast(count(this) as Integer) + CAST_COUNT_TO_INTEGER, + // select count(this) as Integer + CAST_LONG_TO_INTEGER, + NONE + } + /** * Parse a query by name method name into a QueryByNameInfo object * @param queryByName the query by name method name @@ -73,25 +88,30 @@ public void exitPredicate(ee.jakarta.tck.data.tools.antlr.QBNParser.PredicateCon } @Override - public void exitSubject(ee.jakarta.tck.data.tools.antlr.QBNParser.SubjectContext ctx) { - if(ctx.find() != null) { - System.out.println("find: " + ctx.find().getText()); - System.out.println("find_expression.INTEGER: " + ctx.find_expression().INTEGER()); + public void exitAction_query(QBNParser.Action_queryContext ctx) { + QueryByNameInfo.Action action = QueryByNameInfo.Action.valueOf(ctx.action().getText().toUpperCase()); + info.setAction(action); + if(ctx.ignored_text() != null) { + info.setIgnoredText(ctx.ignored_text().getText()); + } + } + + @Override + public void exitFind_query(QBNParser.Find_queryContext ctx) { + if (ctx.limit() != null) { int findCount = 0; - if(ctx.find_expression().INTEGER() != null) { - findCount = Integer.parseInt(ctx.find_expression().INTEGER().getText()); + if (ctx.limit().INTEGER() != null) { + findCount = Integer.parseInt(ctx.limit().INTEGER().getText()); } info.setFindExpressionCount(findCount); - } else { - QueryByNameInfo.Action action = QueryByNameInfo.Action.valueOf(ctx.action().getText().toUpperCase()); - info.setAction(action); } if(ctx.ignored_text() != null) { info.setIgnoredText(ctx.ignored_text().getText()); } } + @Override - public void exitOrder_clause(ee.jakarta.tck.data.tools.antlr.QBNParser.Order_clauseContext ctx) { + public void exitOrder(ee.jakarta.tck.data.tools.antlr.QBNParser.OrderContext ctx) { int count = ctx.order_item().size(); if(ctx.property() != null) { String property = camelCase(ctx.property().getText()); @@ -111,6 +131,11 @@ public void exitOrder_clause(ee.jakarta.tck.data.tools.antlr.QBNParser.Order_cla return info; } + /** + * Simple function to transfer the first character of a string to lower case + * @param s - phrase + * @return camel case version of s + */ public static String camelCase(String s) { return s.substring(0, 1).toLowerCase() + s.substring(1); } @@ -119,18 +144,20 @@ public static String camelCase(String s) { * Convert a QueryByNameInfo object into a JDQL query string * @param info - parse QBN info * @return toQuery(info, false) - * @see #toQuery(QueryByNameInfo, boolean) + * @see #toQuery(QueryByNameInfo, ToQueryOptions...) */ public static String toQuery(QueryByNameInfo info) { - return toQuery(info, false); + return toQuery(info, ToQueryOptions.NONE); } /** * Convert a QueryByNameInfo object into a JDQL query string * @param info - parse QBN info - * @param includeOrderBy - if the order by clause should be included in the query + * @param options - * @return the JDQL query string */ - public static String toQuery(QueryByNameInfo info, boolean includeOrderBy) { + public static String toQuery(QueryByNameInfo info, ToQueryOptions... options) { + // Collect the options into a set + HashSet optionsSet = new HashSet<>(Arrays.asList(options)); StringBuilder sb = new StringBuilder(); int paramIdx = 1; QueryByNameInfo.Action action = info.getAction(); @@ -138,13 +165,19 @@ public static String toQuery(QueryByNameInfo info, boolean includeOrderBy) { case FIND: break; case DELETE: - sb.append("delete ").append(info.getEntity()).append(' '); + sb.append("delete ").append(info.getSimpleName()).append(' '); break; case UPDATE: - sb.append("update ").append(info.getEntity()).append(' '); + sb.append("update ").append(info.getSimpleName()).append(' '); break; case COUNT: - sb.append("select count(this) "); + if(optionsSet.contains(ToQueryOptions.CAST_COUNT_TO_INTEGER)) { + sb.append("select cast(count(this) as Integer) "); + } else if(optionsSet.contains(ToQueryOptions.CAST_LONG_TO_INTEGER)) { + sb.append("select count(this) as Integer "); + } else { + sb.append("select count(this) "); + } break; case EXISTS: sb.append("select count(this)>0 "); @@ -152,7 +185,7 @@ public static String toQuery(QueryByNameInfo info, boolean includeOrderBy) { } // if(info.getPredicates().isEmpty()) { - return sb.toString(); + return sb.toString().trim(); } sb.append("where "); @@ -250,15 +283,22 @@ else if(c.operator == QueryByNameInfo.Operator.EMPTY) { } // If there is an orderBy clause, add it to query - if(includeOrderBy && !info.getOrderBy().isEmpty()) { + int limit = info.getFindExpressionCount() == 0 ? 1 : info.getFindExpressionCount(); + if(optionsSet.contains(ToQueryOptions.INCLUDE_ORDER_BY) && !info.getOrderBy().isEmpty()) { for (QueryByNameInfo.OrderBy ob : info.getOrderBy()) { sb.append(" order by ").append(ob.property).append(' '); if(ob.direction != QueryByNameInfo.OrderBySortDirection.NONE) { sb.append(ob.direction.name().toLowerCase()); } } + // We pass the find expression count as the limit + if(limit > 0) { + sb.append(" limit ").append(limit); + } + } else if(limit > 0) { + sb.append(" order by '' limit ").append(limit); } - return sb.toString(); + return sb.toString().trim(); } } diff --git a/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/QueryByNameInfo.java b/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/QueryByNameInfo.java index 4857f0882..dea1c9ef6 100644 --- a/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/QueryByNameInfo.java +++ b/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/QueryByNameInfo.java @@ -94,12 +94,16 @@ public boolean isDescending() { private Action action = Action.NONE; private List predicates = new ArrayList<>(); private List orderBy = new ArrayList<>(); - // >= 0 means find expression exists + // > 0 means find expression exists int findExpressionCount = -1; String ignoredText; - // The entity name + // The entity FQN name String entity; + /** + * The entity FQN + * @return entity FQN + */ public String getEntity() { return entity; } @@ -108,6 +112,15 @@ public void setEntity(String entity) { this.entity = entity; } + public String getSimpleName() { + String simpleName = entity; + int lastDot = entity.lastIndexOf('.'); + if(lastDot >= 0) { + simpleName = entity.substring(lastDot + 1); + } + return simpleName; + } + public Action getAction() { return action; } @@ -190,10 +203,10 @@ public String toString() { if(ignoredText != null && !ignoredText.isEmpty()) { sb.append(ignoredText); } - sb.append("By"); // Predicates boolean first = true; if(!predicates.isEmpty()) { + sb.append("By"); for(Condition c : predicates) { // Add the join condition if(!first) { diff --git a/tools/src/main/resources/RepoTemplate.stg b/tools/src/main/resources/RepoTemplate.stg new file mode 100644 index 000000000..e5fff9375 --- /dev/null +++ b/tools/src/main/resources/RepoTemplate.stg @@ -0,0 +1,37 @@ +// +delimiters "#", "#" + +/* The base template for creating a repository subinterface +@repo is a ee.jakarta.tck.data.tools.annp.RepositoryInfo object +*/ +genRepo(repo) ::= << +package #repo.pkg#; +import jakarta.annotation.Generated; +import jakarta.data.repository.OrderBy; +import jakarta.data.repository.Query; +import jakarta.data.repository.Repository; +import #repo.fqn#; +#tckImports()# + +@Repository(dataStore = "#repo.dataStore#") +@Generated("ee.jakarta.tck.data.tools.annp.RespositoryProcessor") +interface #repo.name#$ extends #repo.name# { + #repo.methods :{m | + @Override + @Query("#m.query#") + #m.orderBy :{o | @OrderBy(value="#o.property#", descending = #o.descending#)}# + public #m.returnType# #m.name# (#m.parameters: {p | #p#}; separator=", "#); + + } + # + + #tckOverrides()# +} +>> + +/* This is an extension point for adding TCK overrides. Create a subtemplate + group and include the tckOverrides that generates the overrides. +*/ +tckOverrides() ::= "// TODO; Implement TCK overrides" + +tckImports() ::= "" \ No newline at end of file diff --git a/tools/src/test/java/qbyn/QBNParserTest.java b/tools/src/test/java/qbyn/QBNParserTest.java index d30e2dd6b..d576885e7 100644 --- a/tools/src/test/java/qbyn/QBNParserTest.java +++ b/tools/src/test/java/qbyn/QBNParserTest.java @@ -18,6 +18,7 @@ import org.antlr.v4.runtime.CommonTokenStream; import org.antlr.v4.runtime.tree.ParseTree; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import ee.jakarta.tck.data.tools.antlr.QBNLexer; @@ -81,8 +82,6 @@ public void testQueryByNameExamples() throws IOException { QBNParser parser = new QBNParser(tokens); QueryByNameInfo info = new QueryByNameInfo(); parser.addParseListener(new QBNBaseListener() { - private StringBuffer property = new StringBuffer(); - @Override public void exitPredicate(QBNParser.PredicateContext ctx) { int count = ctx.condition().size(); @@ -106,25 +105,32 @@ public void exitPredicate(QBNParser.PredicateContext ctx) { } @Override - public void exitSubject(QBNParser.SubjectContext ctx) { - if(ctx.find() != null) { - System.out.println("find: " + ctx.find().getText()); - System.out.println("find_expression.INTEGER: " + ctx.find_expression().INTEGER()); + public void exitFind_query(QBNParser.Find_queryContext ctx) { + System.out.println("find: " + ctx.find().getText()); + if(ctx.limit() != null) { + System.out.println("find_expression.INTEGER: " + ctx.limit().INTEGER()); int findCount = 0; - if(ctx.find_expression().INTEGER() != null) { - findCount = Integer.parseInt(ctx.find_expression().INTEGER().getText()); + if(ctx.limit().INTEGER() != null) { + findCount = Integer.parseInt(ctx.limit().INTEGER().getText()); } info.setFindExpressionCount(findCount); - } else { - QueryByNameInfo.Action action = QueryByNameInfo.Action.valueOf(ctx.action().getText().toUpperCase()); - info.setAction(action); + if(ctx.ignored_text() != null) { + info.setIgnoredText(ctx.ignored_text().getText()); + } } + } + + @Override + public void exitAction_query(QBNParser.Action_queryContext ctx) { + QueryByNameInfo.Action action = QueryByNameInfo.Action.valueOf(ctx.action().getText().toUpperCase()); + info.setAction(action); if(ctx.ignored_text() != null) { info.setIgnoredText(ctx.ignored_text().getText()); } } + @Override - public void exitOrder_clause(QBNParser.Order_clauseContext ctx) { + public void exitOrder(QBNParser.OrderContext ctx) { int count = ctx.order_item().size(); if(ctx.property() != null) { String property = ctx.property().getText(); @@ -206,7 +212,7 @@ public void test_findByIsOddFalseAndIdBetween() { @Test public void test_findByNumTypeInOrderByIdAsc() { QueryByNameInfo info = ParseUtils.parseQueryByName("findByNumTypeInOrderByIdAsc"); - String query = ParseUtils.toQuery(info, true); + String query = ParseUtils.toQuery(info, ParseUtils.ToQueryOptions.INCLUDE_ORDER_BY); System.out.println(query); Assertions.assertEquals("where numType in ?1 order by id asc", query); Assertions.assertEquals(1, info.getOrderBy().size()); @@ -294,7 +300,7 @@ public void test_findByNumericValueGreaterThanEqualAndHexadecimalEndsWith() { @Test public void test_findByHexadecimalStartsWithAndIsControlOrderByIdAsc() { QueryByNameInfo info = ParseUtils.parseQueryByName("findByHexadecimalStartsWithAndIsControlOrderByIdAsc"); - String query = ParseUtils.toQuery(info, true); + String query = ParseUtils.toQuery(info, ParseUtils.ToQueryOptions.INCLUDE_ORDER_BY); System.out.println(query); Assertions.assertEquals("where left(hexadecimal, length(?1)) = ?1 and isControl = ?2 order by id asc", query); Assertions.assertEquals(1, info.getOrderBy().size()); @@ -383,6 +389,19 @@ public void test_deleteByProductNumLike() { Assertions.assertEquals("delete Product where productNum like ?1", query); Assertions.assertEquals(0, info.getOrderBy().size()); + } + /** Should produce + @Query("delete Product where productNum like ?1") + */ + @Test + public void test_deleteByProductNumLikeNoFQN() { + QueryByNameInfo info = ParseUtils.parseQueryByName("deleteByProductNumLike"); + info.setEntity("com.example.Product"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("delete Product where productNum like ?1", query); + Assertions.assertEquals(0, info.getOrderBy().size()); + } /** Should produce @@ -414,6 +433,7 @@ public void test_countByHexadecimalNotNull() { @Query("select count(this) where id(this) < ?1") */ @Test + @Disabled("Disabled until id refs are fixed") public void test_countByIdLessThan() { QueryByNameInfo info = ParseUtils.parseQueryByName("countByIdLessThan"); String query = ParseUtils.toQuery(info); @@ -447,4 +467,63 @@ public void test_existsByIdGreaterThan() { Assertions.assertEquals(0, info.getOrderBy().size()); } + @Test + public void test_findFirstNameByIdInOrderByAgeDesc() { + QueryByNameInfo info = ParseUtils.parseQueryByName("findFirstXxxxxByIdInOrderByAgeDesc"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("where id in ?1 order by '' limit 1", query); + Assertions.assertEquals(1, info.getOrderBy().size()); + } + + @Test + public void test_findFirst3ByNumericValueGreaterThanEqualAndHexadecimalEndsWith() { + QueryByNameInfo info = ParseUtils.parseQueryByName("findFirst3ByNumericValueGreaterThanEqualAndHexadecimalEndsWith"); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("where numericValue >= ?1 and right(hexadecimal, length(?2)) = ?2 order by '' limit 3", query); + Assertions.assertEquals(0, info.getOrderBy().size()); + } + + @Test + public void test_countByByHand() { + QueryByNameInfo info = new QueryByNameInfo(); + info.setAction(QueryByNameInfo.Action.COUNT); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("select count(this)", query); + } + + /** + * Test the countBy method with an int return type is cast to an integer + */ + @Test + public void test_countByByHandIntReturn() { + QueryByNameInfo info = new QueryByNameInfo(); + info.setAction(QueryByNameInfo.Action.COUNT); + String query = ParseUtils.toQuery(info, ParseUtils.ToQueryOptions.CAST_COUNT_TO_INTEGER); + System.out.println(query); + Assertions.assertEquals("select cast(count(this) as Integer)", query); + } + + /** + * Test the countBy method with a long return type is cast to an integer + */ + @Test + public void test_countByByHandLongReturn() { + QueryByNameInfo info = new QueryByNameInfo(); + info.setAction(QueryByNameInfo.Action.COUNT); + String query = ParseUtils.toQuery(info, ParseUtils.ToQueryOptions.CAST_LONG_TO_INTEGER); + System.out.println(query); + Assertions.assertEquals("select count(this) as Integer", query); + } + + @Test + public void testExistsBy() { + QueryByNameInfo info = new QueryByNameInfo(); + info.setAction(QueryByNameInfo.Action.EXISTS); + String query = ParseUtils.toQuery(info); + System.out.println(query); + Assertions.assertEquals("select count(this)>0", query); + } } diff --git a/tools/src/test/java/qbyn/ST4RepoGenTest.java b/tools/src/test/java/qbyn/ST4RepoGenTest.java index 7aa1dc534..e5af046ef 100644 --- a/tools/src/test/java/qbyn/ST4RepoGenTest.java +++ b/tools/src/test/java/qbyn/ST4RepoGenTest.java @@ -12,14 +12,21 @@ package qbyn; import ee.jakarta.tck.data.tools.annp.RepositoryInfo; +import ee.jakarta.tck.data.tools.qbyn.ParseUtils; import ee.jakarta.tck.data.tools.qbyn.QueryByNameInfo; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.stringtemplate.v4.ST; +import org.stringtemplate.v4.STGroup; +import org.stringtemplate.v4.STGroupFile; import java.util.Arrays; import java.util.Collections; import java.util.List; +import static ee.jakarta.tck.data.tools.annp.AnnProcUtils.TCK_IMPORTS; +import static ee.jakarta.tck.data.tools.annp.AnnProcUtils.TCK_OVERRIDES; + public class ST4RepoGenTest { static String REPO_TEMPLATE = """ import jakarta.annotation.Generated; @@ -49,8 +56,8 @@ public void testSyntax() { s.add("methods", methods); System.out.println(s.render()); } - @Test - public void testRepoGen() { + + private RepositoryInfo createRepositoryInfo() { RepositoryInfo repo = new RepositoryInfo(); repo.setFqn("org.acme.BookRepository"); repo.setName("BookRepository"); @@ -61,11 +68,73 @@ public void testRepoGen() { repo.addMethod(findByTitleLike); RepositoryInfo.MethodInfo findByNumericValue = new RepositoryInfo.MethodInfo("findByNumericValue", "Optional", "from AsciiCharacter where numericValue = :numericValue", - Collections.singletonList(new QueryByNameInfo.OrderBy("numericValue", QueryByNameInfo.OrderBySortDirection.ASC))); + Collections.singletonList(new QueryByNameInfo.OrderBy("numericValue", QueryByNameInfo.OrderBySortDirection.ASC))); findByNumericValue.addParameter("int id"); repo.addMethod(findByNumericValue); + return repo; + } + @Test + public void testRepoGen() { + RepositoryInfo repo = createRepositoryInfo(); ST st = new ST(REPO_TEMPLATE, '#', '#'); st.add("repo", repo); System.out.println(st.render()); } + + @Test + public void testRepoGenViaGroupFiles() { + STGroup repoGroup = new STGroupFile("RepoTemplate.stg"); + ST genRepo = repoGroup.getInstanceOf("genRepo"); + RepositoryInfo repo = createRepositoryInfo(); + genRepo.add("repo", repo); + String classSrc = genRepo.render(); + System.out.println(classSrc); + Assertions.assertTrue(classSrc.contains("interface BookRepository$")); + Assertions.assertTrue(classSrc.contains("// TODO; Implement TCK overrides")); + } + + @Test + public void testRepoGenWithTckOverride() { + STGroup repoGroup = new STGroupFile("RepoTemplate.stg"); + repoGroup.defineTemplate("tckImports", "import jakarta.data.Delete;\n"); + repoGroup.defineTemplate("tckOverrides", "@Delete\nvoid deleteAllBy();\n"); + ST genRepo = repoGroup.getInstanceOf("genRepo"); + RepositoryInfo repo = createRepositoryInfo(); + genRepo.add("repo", repo); + String classSrc = genRepo.render(); + System.out.println(classSrc); + Assertions.assertTrue(classSrc.contains("interface BookRepository$")); + Assertions.assertTrue(!classSrc.contains("// TODO; Implement TCK overrides")); + Assertions.assertTrue(classSrc.contains("void deleteAllBy();")); + Assertions.assertTrue(classSrc.contains("import jakarta.data.Delete;")); + } + + @Test + public void testRepoGenWithTckOverrideFromImport() { + STGroup repoGroup = new STGroupFile("RepoTemplate.stg"); + STGroup tckGroup = new STGroupFile("org.acme.BookRepository_tck.stg"); + tckGroup.importTemplates(repoGroup); + ST genRepo = tckGroup.getInstanceOf("genRepo"); + long count = tckGroup.getTemplateNames().stream().filter(t -> t.equals(TCK_IMPORTS) | t.equals(TCK_OVERRIDES)).count(); + System.out.printf("tckGroup.templates(%d) %s\n", count, tckGroup.getTemplateNames()); + System.out.printf("tckGroup: %s\n", tckGroup.show()); + + RepositoryInfo repo = createRepositoryInfo(); + genRepo.add("repo", repo); + String classSrc = genRepo.render(); + System.out.println(classSrc); + Assertions.assertTrue(classSrc.contains("interface BookRepository$")); + Assertions.assertTrue(!classSrc.contains("// TODO; Implement TCK overrides")); + Assertions.assertTrue(classSrc.contains("void deleteAllBy();")); + Assertions.assertTrue(classSrc.contains("import jakarta.data.Delete;")); + } + + @Test + public void testMissingGroupTemplate() { + IllegalArgumentException ex = Assertions.assertThrows(IllegalArgumentException.class, () -> { + STGroup repoGroup = new STGroupFile("Rectangles_tck.stg"); + repoGroup.getTemplateNames(); + }); + Assertions.assertNotNull(ex, "Load of Rectangles_tck should fail"); + } } diff --git a/tools/src/test/resources/org.acme.BookRepository_tck.stg b/tools/src/test/resources/org.acme.BookRepository_tck.stg new file mode 100644 index 000000000..8bbf199fa --- /dev/null +++ b/tools/src/test/resources/org.acme.BookRepository_tck.stg @@ -0,0 +1,11 @@ +// +delimiters "#", "#" +tckOverrides() ::= << + @Override + @Delete + public void deleteAllBy(); +>> + +tckImports() ::= << +import jakarta.data.Delete; +>> \ No newline at end of file