From a82926a4ef2677fb7f1022df6412aeac16e78657 Mon Sep 17 00:00:00 2001 From: Brian Stansberry Date: Mon, 23 Sep 2024 16:19:14 -0500 Subject: [PATCH] [Issue_183][WFLY-19397] Add a Jakarta Data TCK runner --- .gitignore | 3 + data/README.md | 15 + data/pom.xml | 216 +++++++ data/testjar/pom.xml | 194 +++++++ .../javax.annotation.processing.Processor | 1 + ...rta.tck.data.web.validation.Rectangles.stg | 0 data/tools/pom.xml | 135 +++++ data/tools/src/main/antlr4/QBN.g4 | 98 ++++ .../tck/data/tools/annp/AnnProcUtils.java | 159 ++++++ .../tck/data/tools/annp/RepositoryInfo.java | 196 +++++++ .../data/tools/annp/RespositoryProcessor.java | 217 +++++++ .../tck/data/tools/qbyn/ParseUtils.java | 304 ++++++++++ .../tck/data/tools/qbyn/QueryByNameInfo.java | 250 +++++++++ .../tools/src/main/resources/RepoTemplate.stg | 37 ++ .../src/test/java/qbyn/QBNParserTest.java | 529 ++++++++++++++++++ .../src/test/java/qbyn/ST4RepoGenTest.java | 140 +++++ .../resources/org.acme.BookRepository_tck.stg | 11 + data/wildfly-runner/pom.xml | 308 ++++++++++ .../src/test/java/embedded/CdiTests.java | 55 ++ .../test/java/embedded/GreetingService.java | 39 ++ .../tck/ext/HibernateLoadableExtension.java | 14 + .../hibernate/data/tck/ext/JPAProcessor.java | 74 +++ ...boss.arquillian.core.spi.LoadableExtension | 1 + .../src/test/resources/arquillian.xml | 33 ++ .../test/resources/enable-jakarta-data.cli | 17 + .../src/test/resources/logging.properties | 40 ++ 26 files changed, 3086 insertions(+) create mode 100644 data/README.md create mode 100644 data/pom.xml create mode 100644 data/testjar/pom.xml create mode 100644 data/testjar/src/main/resources/META-INF/services/javax.annotation.processing.Processor create mode 100644 data/testjar/src/main/resources/ee.jakarta.tck.data.web.validation.Rectangles.stg create mode 100644 data/tools/pom.xml create mode 100644 data/tools/src/main/antlr4/QBN.g4 create mode 100644 data/tools/src/main/java/ee/jakarta/tck/data/tools/annp/AnnProcUtils.java create mode 100644 data/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RepositoryInfo.java create mode 100644 data/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RespositoryProcessor.java create mode 100644 data/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/ParseUtils.java create mode 100644 data/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/QueryByNameInfo.java create mode 100644 data/tools/src/main/resources/RepoTemplate.stg create mode 100644 data/tools/src/test/java/qbyn/QBNParserTest.java create mode 100644 data/tools/src/test/java/qbyn/ST4RepoGenTest.java create mode 100644 data/tools/src/test/resources/org.acme.BookRepository_tck.stg create mode 100644 data/wildfly-runner/pom.xml create mode 100644 data/wildfly-runner/src/test/java/embedded/CdiTests.java create mode 100644 data/wildfly-runner/src/test/java/embedded/GreetingService.java create mode 100644 data/wildfly-runner/src/test/java/org/hibernate/data/tck/ext/HibernateLoadableExtension.java create mode 100644 data/wildfly-runner/src/test/java/org/hibernate/data/tck/ext/JPAProcessor.java create mode 100644 data/wildfly-runner/src/test/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension create mode 100644 data/wildfly-runner/src/test/resources/arquillian.xml create mode 100644 data/wildfly-runner/src/test/resources/enable-jakarta-data.cli create mode 100644 data/wildfly-runner/src/test/resources/logging.properties diff --git a/.gitignore b/.gitignore index c7b1c2a..94bc314 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,6 @@ build.metadata # Derby logs derby.log /bin/ + +# Jakarta Data TCK logs +*DataTCK*.log diff --git a/data/README.md b/data/README.md new file mode 100644 index 0000000..2bc363c --- /dev/null +++ b/data/README.md @@ -0,0 +1,15 @@ +# Jakarta Data TCK Runner for Hibernate + WildFly + +This project is a Jakarta Data TCK runner for Hibernate + WildFly. It is a simple project that demonstrates how to run +the Jakarta Data TCK tests against Hibernate on WildFly. + +## Initialize the WildFly server +There is a install-wildfly profile that will configure a local target/wildfly install with patched org.hibernate, +jakarta.data.api and org.jboss.as.jpa modules. To use it, run: + +```shell +mvn -Pstaging -Pinstall-wildfly clean process-sources +``` + +## Run the ValidationTests +Load the ee.jakarta.tck.data.web.validation.ValidationTests class in IntelliJ and run the tests. diff --git a/data/pom.xml b/data/pom.xml new file mode 100644 index 0000000..0fca66e --- /dev/null +++ b/data/pom.xml @@ -0,0 +1,216 @@ + + + + 4.0.0 + + + org.jboss + jboss-parent + 46 + + + org.wildfly.data.tck + data-tck-parent + 1.0.0-SNAPSHOT + pom + + WildFly Jakarta Data TCK Runner Parent + + + + 1.0.0 + 1.0.0 + 4.1.0 + 6.1.0 + 3.1.0 + 6.6.1.Final + 1.9.1.Final + 10.0.0.Final + 3.6.1.Final + 1.2.6 + 5.10.2 + + [17,) + ${project.build.directory}/wildfly + wildfly-preview-feature-pack + preview + + + + + + + org.junit + junit-bom + ${version.org.junit} + pom + import + + + org.jboss.arquillian + arquillian-bom + ${version.org.jboss.arquillian.core} + pom + import + + + org.jboss.arquillian.jakarta + arquillian-jakarta-bom + ${version.org.jboss.arquillian.jakarta} + pom + import + + + + + jakarta.data + jakarta.data-api + ${version.jakarta.data.jakarta-data-api} + + + + jakarta.servlet + jakarta.servlet-api + ${version.jakarta.servlet.jakarta-servlet-api} + + + jakarta.enterprise + jakarta.enterprise.cdi-api + ${version.jakarta.enterprise} + + + jakarta.validation + jakarta.validation-api + ${version.jakarta.validation.jakarta-validation-api} + provided + + + org.jboss.logging + jboss-logging + ${version.org.jboss.logging.jboss-logging} + + + + org.jboss.shrinkwrap + shrinkwrap-api + ${version.org.jboss.shrinkwrap.shrinkwrap} + + + + ${project.groupId} + hibernate-data-tck-tests + ${project.version} + + + + jakarta.data + jakarta.data-tools + ${project.version} + + + + + + + org.jboss.shrinkwrap + shrinkwrap-api + + + org.jboss.arquillian.junit5 + arquillian-junit5-core + + + + + tools + testjar + wildfly-runner + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + + + + + org.apache.maven.plugins + maven-enforcer-plugin + ${version.enforcer.plugin} + + + require-java17-build + + enforce + + compile + + + + ${required.java.build.version} + + + + + + + + + + + + + staging + + false + + + + sonatype-nexus-staging + Sonatype Nexus Staging + https://jakarta.oss.sonatype.org/content/repositories/staging/ + + true + + + false + + + + + + sonatype-nexus-staging + Sonatype Nexus Staging + https://jakarta.oss.sonatype.org/content/repositories/staging/ + + true + + + false + + + + + + diff --git a/data/testjar/pom.xml b/data/testjar/pom.xml new file mode 100644 index 0000000..eb1c9cd --- /dev/null +++ b/data/testjar/pom.xml @@ -0,0 +1,194 @@ + + + + 4.0.0 + + + org.wildfly.data.tck + data-tck-parent + 1.0.0-SNAPSHOT + ../pom.xml + + + hibernate-data-tck-tests + + Hibernate Jakarta Data TCK Test Jar + + 3.2.0 + 2.0.1 + + + + + + + jakarta.data + jakarta.data-tck + ${version.jakarta.data.tck} + + + org.apache.maven.plugins + maven-surefire-plugin + + + + + jakarta.data + jakarta.data-tck + ${version.jakarta.data.tck} + sources + + + + jakarta.data + jakarta.data-api + + + + jakarta.nosql + nosql-core + 1.0.0-b7 + provided + + + + + + org.junit.jupiter + junit-jupiter + + + jakarta.servlet + jakarta.servlet-api + + + jakarta.enterprise + jakarta.enterprise.cdi-api + + + jakarta.validation + jakarta.validation-api + + + jakarta.transaction + jakarta.transaction-api + ${version.jakarta.transaction.jakarta-transaction-api} + provided + + + jakarta.persistence + jakarta.persistence-api + ${version.jakarta.persistence} + provided + + + jakarta.data + jakarta.data-tools + + + org.hibernate.orm + hibernate-jpamodelgen + ${version.org.hibernate.orm} + + + + + + + src/main/resources + + + ${project.build.directory}/tck-sources + + ee/jakarta/tck/data/framework/signature/jakarta.data.sig_17 + ee/jakarta/tck/data/framework/signature/jakarta.data.sig_21 + ee/jakarta/tck/data/framework/signature/sig-test.map + ee/jakarta/tck/data/framework/signature/sig-test-pkg-list.txt + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + src-dependencies + initialize + + unpack-dependencies + + + jakarta.data + jakarta.data-tck + sources + true + **/_AsciiChar.java,**/_AsciiCharacter.java + true + ${project.build.directory}/tck-sources + true + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + -parameters + -XprintRounds + + + ${project.basedir}/src/main/java + ${project.build.directory}/tck-sources + ${project.build.directory}/generated-source/annotations + + ${project.build.directory}/tck-tool-sources + 17 + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + true + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + + \ No newline at end of file diff --git a/data/testjar/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/data/testjar/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000..c2ae1dd --- /dev/null +++ b/data/testjar/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +ee.jakarta.tck.data.tools.annp.RespositoryProcessor \ No newline at end of file diff --git a/data/testjar/src/main/resources/ee.jakarta.tck.data.web.validation.Rectangles.stg b/data/testjar/src/main/resources/ee.jakarta.tck.data.web.validation.Rectangles.stg new file mode 100644 index 0000000..e69de29 diff --git a/data/tools/pom.xml b/data/tools/pom.xml new file mode 100644 index 0000000..fa28465 --- /dev/null +++ b/data/tools/pom.xml @@ -0,0 +1,135 @@ + + + + + 4.0.0 + + + jakarta.data + jakarta.data-tools + 1.0.0-SNAPSHOT + 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 + + + + + + + + jakarta.data + jakarta.data-api + 1.0.0-RC1 + + + 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.apache.maven.plugins + maven-deploy-plugin + + + true + + + + org.antlr + antlr4-maven-plugin + ${antlr.version} + + true + true + ${project.build.directory}/generated-sources/ee/jakarta/tck/data/tools/antlr + + + + + antlr + + antlr4 + + + + + + + + diff --git a/data/tools/src/main/antlr4/QBN.g4 b/data/tools/src/main/antlr4/QBN.g4 new file mode 100644 index 0000000..01936a7 --- /dev/null +++ b/data/tools/src/main/antlr4/QBN.g4 @@ -0,0 +1,98 @@ +/* + * 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 : find_query | action_query ; + +find_query : find limit? ignored_text? restriction? order? ; +action_query : action ignored_text? restriction? ; + +action : delete | update | count | exists ; + +find : 'find' ; +delete : 'delete' ; +update : 'update' ; +count : 'count' ; +exists : 'exists' ; + +restriction : BY predicate ; + +limit : 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 : ORDER_BY ( property | order_item+) ; + +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/data/tools/src/main/java/ee/jakarta/tck/data/tools/annp/AnnProcUtils.java b/data/tools/src/main/java/ee/jakarta/tck/data/tools/annp/AnnProcUtils.java new file mode 100644 index 0000000..640116d --- /dev/null +++ b/data/tools/src/main/java/ee/jakarta/tck/data/tools/annp/AnnProcUtils.java @@ -0,0 +1,159 @@ +/* + * 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 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; +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.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +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 { + // 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"; + + /** + * 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) { + 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(); + } + return null; + } + + + public static QueryByNameInfo isQBN(ExecutableElement m) { + String methodName = m.getSimpleName().toString(); + 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 { + 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()); + try(Writer writer = srcFile.openWriter()) { + writer.write(ifaceSrc); + writer.flush(); + } + System.out.printf("Wrote %s, to: %s\n", ifaceName, srcFile.toUri()); + } +} diff --git a/data/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RepositoryInfo.java b/data/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RepositoryInfo.java new file mode 100644 index 0000000..1848c9f --- /dev/null +++ b/data/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RepositoryInfo.java @@ -0,0 +1,196 @@ +/* + * 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.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; + 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 pkg; + 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; + 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() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDataStore() { + return dataStore; + } + + public void setDataStore(String dataStore) { + this.dataStore = dataStore; + } + + + /** + * 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); + // 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; + } + public boolean hasQBNMethods() { + return !qbnMethods.isEmpty(); + } + + public ArrayList getMethods() { + return methods; + } + + public void addMethod(MethodInfo m) { + methods.add(m); + } +} diff --git a/data/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RespositoryProcessor.java b/data/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RespositoryProcessor.java new file mode 100644 index 0000000..cbd621b --- /dev/null +++ b/data/tools/src/main/java/ee/jakarta/tck/data/tools/annp/RespositoryProcessor.java @@ -0,0 +1,217 @@ +/* + * 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.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; +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.Repository; +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", "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 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; + 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()); + } + } + + // 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) { + 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); + 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; + } 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; + } + } + } + } + } + } + // Look for lifecycle methods + for (Element e : repo.getEnclosedElements()) { + if (e instanceof ExecutableElement) { + ExecutableElement ee = (ExecutableElement) e; + if (AnnProcUtils.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; + } + + + /** + * 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); + 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, 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; + } + + 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/data/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/ParseUtils.java b/data/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/ParseUtils.java new file mode 100644 index 0000000..e99ed71 --- /dev/null +++ b/data/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/ParseUtils.java @@ -0,0 +1,304 @@ +/* + * 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.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; +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 + * @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.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() { + @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) { + 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.EQUAL; + 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 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.limit().INTEGER() != null) { + findCount = Integer.parseInt(ctx.limit().INTEGER().getText()); + } + info.setFindExpressionCount(findCount); + } + if(ctx.ignored_text() != null) { + info.setIgnoredText(ctx.ignored_text().getText()); + } + } + + @Override + 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()); + 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 = camelCase(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; + } + + /** + * 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); + } + + /** + * Convert a QueryByNameInfo object into a JDQL query string + * @param info - parse QBN info + * @return toQuery(info, false) + * @see #toQuery(QueryByNameInfo, ToQueryOptions...) + */ + public static String toQuery(QueryByNameInfo info) { + return toQuery(info, ToQueryOptions.NONE); + } + /** + * Convert a QueryByNameInfo object into a JDQL query string + * @param info - parse QBN info + * @param options - + * @return the JDQL query string + */ + 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(); + switch (action) { + case FIND: + break; + case DELETE: + sb.append("delete ").append(info.getSimpleName()).append(' '); + break; + case UPDATE: + sb.append("update ").append(info.getSimpleName()).append(' '); + break; + case COUNT: + 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 "); + break; + } + // + if(info.getPredicates().isEmpty()) { + return sb.toString().trim(); + } + + 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 + 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().trim(); + } +} diff --git a/data/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/QueryByNameInfo.java b/data/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/QueryByNameInfo.java new file mode 100644 index 0000000..dea1c9e --- /dev/null +++ b/data/tools/src/main/java/ee/jakarta/tck/data/tools/qbyn/QueryByNameInfo.java @@ -0,0 +1,250 @@ +/* + * 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("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 + } + + /** + * 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.EQUAL; + // 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 + public String property; + // the direction to sort the property + 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<>(); + private List orderBy = new ArrayList<>(); + // > 0 means find expression exists + int findExpressionCount = -1; + String ignoredText; + // The entity FQN name + String entity; + + /** + * The entity FQN + * @return entity FQN + */ + public String getEntity() { + return entity; + } + + 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; + } + + 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); + } + // Predicates + boolean first = true; + if(!predicates.isEmpty()) { + sb.append("By"); + for(Condition c : predicates) { + // Add the join condition + 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.EQUAL) { + 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/data/tools/src/main/resources/RepoTemplate.stg b/data/tools/src/main/resources/RepoTemplate.stg new file mode 100644 index 0000000..e5fff93 --- /dev/null +++ b/data/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/data/tools/src/test/java/qbyn/QBNParserTest.java b/data/tools/src/test/java/qbyn/QBNParserTest.java new file mode 100644 index 0000000..d576885 --- /dev/null +++ b/data/tools/src/test/java/qbyn/QBNParserTest.java @@ -0,0 +1,529 @@ +/* + * 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.Assertions; +import org.junit.jupiter.api.Disabled; +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() { + @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.EQUAL; + 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 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.limit().INTEGER() != null) { + findCount = Integer.parseInt(ctx.limit().INTEGER().getText()); + } + info.setFindExpressionCount(findCount); + 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(QBNParser.OrderContext 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); + } + } + + @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, 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()); + 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, 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()); + } + + /** 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("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 + @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 + @Disabled("Disabled until id refs are fixed") + 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()); + } + + @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/data/tools/src/test/java/qbyn/ST4RepoGenTest.java b/data/tools/src/test/java/qbyn/ST4RepoGenTest.java new file mode 100644 index 0000000..e5af046 --- /dev/null +++ b/data/tools/src/test/java/qbyn/ST4RepoGenTest.java @@ -0,0 +1,140 @@ +/* + * 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.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; + 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()); + } + + private RepositoryInfo createRepositoryInfo() { + 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); + 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/data/tools/src/test/resources/org.acme.BookRepository_tck.stg b/data/tools/src/test/resources/org.acme.BookRepository_tck.stg new file mode 100644 index 0000000..8bbf199 --- /dev/null +++ b/data/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 diff --git a/data/wildfly-runner/pom.xml b/data/wildfly-runner/pom.xml new file mode 100644 index 0000000..8aabdd4 --- /dev/null +++ b/data/wildfly-runner/pom.xml @@ -0,0 +1,308 @@ + + + + 4.0.0 + + + org.wildfly.data.tck + data-tck-parent + 1.0.0-SNAPSHOT + ../pom.xml + + + data-tck + + WildFly Jakarta Data TCK Web Runner + + + 5.1.3.Final + 4.0.2.Final + 5.1.0.Beta4 + 5.0.1.Final + 34.0.0.Beta1 + 2.2 + + ${project.build.directory}/wildfly + wildfly-preview-feature-pack + preview + + + + + + + ${project.groupId} + hibernate-data-tck-tests + test + + + + + jakarta.data + jakarta.data-api + + + + org.hibernate.orm + hibernate-core + ${version.org.hibernate.orm} + + + + org.jboss.weld + weld-junit5 + ${version.org.jboss.weld.weld-junit} + + + org.jboss.weld + weld-lite-extension-translator + ${version.org.jboss.weld.weld} + + + + org.junit.jupiter + junit-jupiter + + + org.junit.jupiter + junit-jupiter-engine + + + + jakarta.tck + sigtest-maven-plugin + ${version.sigtest.maven.plugin} + + + jakarta.servlet + jakarta.servlet-api + + + jakarta.enterprise + jakarta.enterprise.cdi-api + + + jakarta.validation + jakarta.validation-api + provided + + + org.jboss.logging + jboss-logging + + + org.wildfly.arquillian + wildfly-arquillian-container-managed + ${version.org.wildfly.arquillian} + test + + + org.jboss.arquillian.protocol + arquillian-protocol-rest-jakarta + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy + process-sources + + copy + + + + + + + jakarta.data + jakarta.data-api + ${version.jakarta.data.jakarta-data-api} + jar + false + target + jakarta.data-api.jar + + + true + + + + + org.apache.maven.plugins + maven-surefire-plugin + + 1 + + org.wildfly.data.tck:hibernate-data-tck-tests + + + + core + + target/jimage + core + ${project.build.directory}/jakarta.data-api.jar + + target/test-classes/logging.properties + + ${settings.localRepository} + ${jboss.home} + wildfly-core-profile + false + + + + + + ${jboss.home} + + + + + + + + + + install-wildfly + + + !jboss.home + + + + + + org.wildfly.plugins + wildfly-maven-plugin + ${version.org.wildfly.maven.plugin} + + + install-wildfly + initialize + + provision + + + + + ${jboss.home} + true + + + org.wildfly + ${wildfly.feature.pack.artifactId} + ${version.org.wildfly.wildfly} + + + + internal-standalone-profile + jakarta-data + + + messaging-activemq + + + + + + + + + + configure-jakarta-data + + + jboss.home + + + + + + org.wildfly.plugins + wildfly-maven-plugin + ${version.org.wildfly.maven.plugin} + + + configure-jakarta-data + initialize + + execute-commands + + + + + true + + + + ${jboss.home} + ${project.build.directory}/wildfly-plugin.log + + ${settings.localRepository} + ${jboss.home}/modules + ${embedded.server.stability} + + + + + + + + + staging + + false + + + + sonatype-nexus-staging + Sonatype Nexus Staging + https://jakarta.oss.sonatype.org/content/repositories/staging/ + + true + + + false + + + + + + sonatype-nexus-staging + Sonatype Nexus Staging + https://jakarta.oss.sonatype.org/content/repositories/staging/ + + true + + + false + + + + + + diff --git a/data/wildfly-runner/src/test/java/embedded/CdiTests.java b/data/wildfly-runner/src/test/java/embedded/CdiTests.java new file mode 100644 index 0000000..2b95f2a --- /dev/null +++ b/data/wildfly-runner/src/test/java/embedded/CdiTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2015 Red Hat, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package embedded; + +import jakarta.inject.Inject; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.junit5.ArquillianExtension; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.Test; + +/** + * Tests CDI and injection + * + * @author Andrew Lee Rubinger + */ +@ExtendWith(ArquillianExtension.class) +@Tag("core") +public class CdiTests { + + @Deployment + public static JavaArchive create() { + final JavaArchive archive = ShrinkWrap.create(JavaArchive.class).addClass(GreetingService.class); + archive.addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml"); + return archive; + } + + @Inject + private GreetingService service; + + @Test + public void shouldBeAbleToInject() throws Exception { + Assertions.assertNotNull(service); + System.out.println("shouldBeAbleToInject, have service"); + final String name = "ALR"; + Assertions.assertEquals(GreetingService.GREETING_PREPENDED + name, service.greet(name)); + } +} \ No newline at end of file diff --git a/data/wildfly-runner/src/test/java/embedded/GreetingService.java b/data/wildfly-runner/src/test/java/embedded/GreetingService.java new file mode 100644 index 0000000..e2f8630 --- /dev/null +++ b/data/wildfly-runner/src/test/java/embedded/GreetingService.java @@ -0,0 +1,39 @@ +/* + * Copyright 2015 Red Hat, Inc. + * + * 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 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package embedded; + +import jakarta.enterprise.context.ApplicationScoped; + +/** + * Test CDI Bean + * + * @author Andrew Lee Rubinger + */ +@ApplicationScoped +public class GreetingService { + + public static final String GREETING_PREPENDED = "Hello, "; + + /** + * Prepends the specified name with {@link GreetingService#GREETING_PREPENDED} + * + * @param name + * @return + */ + public String greet(final String name) { + return GREETING_PREPENDED + name; + } +} diff --git a/data/wildfly-runner/src/test/java/org/hibernate/data/tck/ext/HibernateLoadableExtension.java b/data/wildfly-runner/src/test/java/org/hibernate/data/tck/ext/HibernateLoadableExtension.java new file mode 100644 index 0000000..e6c3073 --- /dev/null +++ b/data/wildfly-runner/src/test/java/org/hibernate/data/tck/ext/HibernateLoadableExtension.java @@ -0,0 +1,14 @@ +package org.hibernate.data.tck.ext; + +import org.jboss.arquillian.container.test.spi.client.deployment.ApplicationArchiveProcessor; +import org.jboss.arquillian.container.test.spi.client.deployment.AuxiliaryArchiveAppender; +import org.jboss.arquillian.core.spi.LoadableExtension; + +public class HibernateLoadableExtension implements LoadableExtension { + + @Override + public void register(ExtensionBuilder builder) { + builder.service(ApplicationArchiveProcessor.class, JPAProcessor.class); + //builder.service(AuxiliaryArchiveAppender.class, TCKFrameworkAppender.class); + } +} diff --git a/data/wildfly-runner/src/test/java/org/hibernate/data/tck/ext/JPAProcessor.java b/data/wildfly-runner/src/test/java/org/hibernate/data/tck/ext/JPAProcessor.java new file mode 100644 index 0000000..eace0c6 --- /dev/null +++ b/data/wildfly-runner/src/test/java/org/hibernate/data/tck/ext/JPAProcessor.java @@ -0,0 +1,74 @@ +package org.hibernate.data.tck.ext; + +import org.jboss.arquillian.container.test.spi.client.deployment.ApplicationArchiveProcessor; +import org.jboss.arquillian.test.spi.TestClass; +import org.jboss.shrinkwrap.api.Archive; +import org.jboss.shrinkwrap.api.ArchivePath; +import org.jboss.shrinkwrap.api.Node; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.jboss.shrinkwrap.api.asset.StringAsset; +import org.jboss.shrinkwrap.api.spec.WebArchive; + +import java.util.Map; + +/** + * Creates and adds a persistence.xml for HibernatePersistenceProvider, an emtpy beans.xml, and the annotation processor + * generated classes to the deployment archive. + */ +public class JPAProcessor implements ApplicationArchiveProcessor { + static final String PERSISTENCE_XML = """ + + + + + Hibernate Entity Manager for Jakarta Data TCK + org.hibernate.jpa.HibernatePersistenceProvider + + + + + + + + + + + + + + + + + + + """; + + @Override + public void process(Archive archive, TestClass testClass) { + System.out.printf("Processing archive %s, test=%s\n", archive.getName(), testClass.getName()); + if(archive instanceof WebArchive) { + WebArchive webArchive = (WebArchive) archive; + webArchive.addAsWebInfResource(new StringAsset(PERSISTENCE_XML), "classes/META-INF/persistence.xml"); + webArchive.addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml"); + for (Map.Entry e : webArchive.getContent().entrySet()) { + String path = e.getKey().get(); + if (path.endsWith(".class")) { + // Look for X_.class + String className = path.substring("/WEB-INF/classes/".length(), path.length() - ".class".length()) + .replace('/', '.'); + try { + webArchive.addClass(className + "_"); + System.out.printf("Added %s_\n", className); + } catch (IllegalArgumentException ex) { + // Ignore + } + } + } + } + } +} diff --git a/data/wildfly-runner/src/test/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension b/data/wildfly-runner/src/test/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension new file mode 100644 index 0000000..5aa3219 --- /dev/null +++ b/data/wildfly-runner/src/test/resources/META-INF/services/org.jboss.arquillian.core.spi.LoadableExtension @@ -0,0 +1 @@ +org.hibernate.data.tck.ext.HibernateLoadableExtension \ No newline at end of file diff --git a/data/wildfly-runner/src/test/resources/arquillian.xml b/data/wildfly-runner/src/test/resources/arquillian.xml new file mode 100644 index 0000000..7aad875 --- /dev/null +++ b/data/wildfly-runner/src/test/resources/arquillian.xml @@ -0,0 +1,33 @@ + + + + + + + + target/deployments + + + + + ${jboss.home} + ${additional.vm.args:} + standalone.xml + + + diff --git a/data/wildfly-runner/src/test/resources/enable-jakarta-data.cli b/data/wildfly-runner/src/test/resources/enable-jakarta-data.cli new file mode 100644 index 0000000..811abe2 --- /dev/null +++ b/data/wildfly-runner/src/test/resources/enable-jakarta-data.cli @@ -0,0 +1,17 @@ +# This CLI script allows to enable Jakarta Data for the standalone configurations. +# By default, standalone.xml is updated. +# Run it from JBOSS_HOME as: +# bin/jboss-cli.sh --file=docs/examples/enable-microprofile.cli [-Dconfig=] + +embed-server --server-config=${config:standalone.xml} + +if (outcome != success) of /subsystem=jakarta-data:read-resource + /extension=org.wildfly.extension.jakarta.data:add + /subsystem=jakarta-data:add +else + echo INFO: jakarta-data already in configuration, subsystem not added. +end-if + +echo INFO: Configuration done. + +stop-embedded-server diff --git a/data/wildfly-runner/src/test/resources/logging.properties b/data/wildfly-runner/src/test/resources/logging.properties new file mode 100644 index 0000000..b459c6b --- /dev/null +++ b/data/wildfly-runner/src/test/resources/logging.properties @@ -0,0 +1,40 @@ +# Ensure that both your client and sever JVMs point to this file using the java.util.logging property +# -Djava.util.logging.config.file=/path/to/logging.properties + +#Handlers we plan to use +handlers=java.util.logging.FileHandler,java.util.logging.ConsoleHandler + +.level=ALL + +org.junit.level=FINEST +#Jakarta Data TCK logger - By default log everything for ee.jakarta.tck.data +ee.jakarta.tck.data.level=ALL + +#Formatting for the simple formatter +java.util.logging.SimpleFormatter.class.log=true +java.util.logging.SimpleFormatter.class.full=false +java.util.logging.SimpleFormatter.class.length=10 + +java.util.logging.SimpleFormatter.level.log=true + +java.util.logging.SimpleFormatter.method.log=true +java.util.logging.SimpleFormatter.method.length=30 + +java.util.logging.SimpleFormatter.thread.log=true +java.util.logging.SimpleFormatter.thread.length=3 + +java.util.logging.SimpleFormatter.time.log=true +java.util.logging.SimpleFormatter.time.format=[MM/dd/yyyy HH:mm:ss:SSS z] + +java.util.logging.SimpleFormatter.format=[%1$tF %1$tT] %4$.1s %3$s %5$s %n + +# Log warnings to console +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.ConsoleHandler.level=WARNING + +# Log everything else to file +java.util.logging.FileHandler.pattern=DataTCK%g%u.log +java.util.logging.FileHandler.limit = 500000 +java.util.logging.FileHandler.count = 5 +java.util.logging.FileHandler.formatter=java.util.logging.SimpleFormatter +java.util.logging.FileHandler.level=ALL \ No newline at end of file