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..bb5cc63
--- /dev/null
+++ b/data/README.md
@@ -0,0 +1,18 @@
+# Jakarta Data TCK Runner for WildFly
+
+This project is a Jakarta Data TCK runner for WildFly. It allows execution of
+the Jakarta Data TCK tests against WildFly.
+
+## Running against a WildFly provisioned by the TCK runner
+
+By default, the runner will provision a WildFly instance that includes the `jakarta-data` layer.
+
+Use the `wildfly.feature.pack.artifactId`, `wildfly.feature.pack.artifactId` and `version.org.wildfly.wildfly` system properties to control the GAV of the feature pack used to provision the installation. All have default values; see `wildfly-runner/pom.xml` for the current defaults.
+
+## Running against an externally provisioned WildFly
+
+To disable local provisioning and instead run against a WildFly installation that was provisioned externally, use the `jboss-home` system property to point at your installation.
+
+`mvn clean verify -Djboss.home=/home/developer/my-wildfly`
+
+The TCK runner will execute a CLI script to attempt to add the `jakarta-data` subsystem if it's not already present in the `standalone.xml` configuration. (If it is present, the script will do nothing.)
\ No newline at end of file
diff --git a/data/pom.xml b/data/pom.xml
new file mode 100644
index 0000000..9f00449
--- /dev/null
+++ b/data/pom.xml
@@ -0,0 +1,228 @@
+
+
+
+ 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-deploy-plugin
+
+
+ true
+
+
+
+ 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..8fef907
--- /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-install-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 extends TypeMirror> 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 extends Element> 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 extends TypeElement> annotations, RoundEnvironment roundEnv) {
+ System.out.printf("RespositoryProcessor: Processing repositories, over=%s\n", roundEnv.processingOver());
+ boolean newRepos = false;
+ Set extends Element> 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 extends AnnotationMirror> 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 extends VariableElement> 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 extends TypeMirror> 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..60bc854
--- /dev/null
+++ b/data/wildfly-runner/pom.xml
@@ -0,0 +1,309 @@
+
+
+
+ 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
+ org.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
+
+
+ ${wildfly.feature.pack.groupId}
+ ${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