From 038db797370eb8c4b76d66abc972e2121fb3abad Mon Sep 17 00:00:00 2001 From: rob bygrave Date: Fri, 9 Oct 2020 00:02:09 +1300 Subject: [PATCH] #2069 - Add ebean-querybean, querybean-generator and kotlin-querybean-generator as modules --- ebean-querybean/README.md | 4 + ebean-querybean/pom.xml | 92 + .../typequery/AlreadyEnhancedMarker.java | 18 + .../main/java/io/ebean/typequery/PArray.java | 98 + .../io/ebean/typequery/PBaseCompareable.java | 237 ++ .../java/io/ebean/typequery/PBaseDate.java | 50 + .../java/io/ebean/typequery/PBaseNumber.java | 109 + .../java/io/ebean/typequery/PBaseString.java | 173 ++ .../io/ebean/typequery/PBaseValueEqual.java | 227 ++ .../java/io/ebean/typequery/PBigDecimal.java | 28 + .../java/io/ebean/typequery/PBigInteger.java | 29 + .../java/io/ebean/typequery/PBoolean.java | 70 + .../java/io/ebean/typequery/PCalendar.java | 28 + .../main/java/io/ebean/typequery/PCidr.java | 28 + .../main/java/io/ebean/typequery/PClass.java | 27 + .../java/io/ebean/typequery/PCurrency.java | 29 + .../java/io/ebean/typequery/PDayOfWeek.java | 30 + .../main/java/io/ebean/typequery/PDouble.java | 27 + .../java/io/ebean/typequery/PDuration.java | 30 + .../main/java/io/ebean/typequery/PEnum.java | 28 + .../main/java/io/ebean/typequery/PFile.java | 31 + .../main/java/io/ebean/typequery/PFloat.java | 27 + .../main/java/io/ebean/typequery/PInet.java | 28 + .../java/io/ebean/typequery/PInetAddress.java | 29 + .../java/io/ebean/typequery/PInstant.java | 30 + .../java/io/ebean/typequery/PInteger.java | 27 + .../io/ebean/typequery/PJodaDateMidnight.java | 28 + .../io/ebean/typequery/PJodaDateTime.java | 28 + .../io/ebean/typequery/PJodaLocalDate.java | 28 + .../ebean/typequery/PJodaLocalDateTime.java | 28 + .../io/ebean/typequery/PJodaLocalTime.java | 30 + .../main/java/io/ebean/typequery/PJson.java | 157 ++ .../java/io/ebean/typequery/PLocalDate.java | 28 + .../io/ebean/typequery/PLocalDateTime.java | 28 + .../java/io/ebean/typequery/PLocalTime.java | 30 + .../main/java/io/ebean/typequery/PLocale.java | 29 + .../main/java/io/ebean/typequery/PLong.java | 27 + .../main/java/io/ebean/typequery/PMonth.java | 28 + .../java/io/ebean/typequery/PMonthDay.java | 28 + .../io/ebean/typequery/POffsetDateTime.java | 29 + .../java/io/ebean/typequery/POffsetTime.java | 29 + .../main/java/io/ebean/typequery/PScalar.java | 29 + .../io/ebean/typequery/PScalarComparable.java | 30 + .../main/java/io/ebean/typequery/PShort.java | 27 + .../java/io/ebean/typequery/PSqlDate.java | 29 + .../main/java/io/ebean/typequery/PString.java | 149 ++ .../main/java/io/ebean/typequery/PTime.java | 30 + .../java/io/ebean/typequery/PTimeZone.java | 29 + .../java/io/ebean/typequery/PTimestamp.java | 28 + .../main/java/io/ebean/typequery/PUri.java | 29 + .../main/java/io/ebean/typequery/PUrl.java | 29 + .../java/io/ebean/typequery/PUtilDate.java | 28 + .../main/java/io/ebean/typequery/PUuid.java | 29 + .../main/java/io/ebean/typequery/PYear.java | 28 + .../java/io/ebean/typequery/PYearMonth.java | 28 + .../main/java/io/ebean/typequery/PZoneId.java | 29 + .../java/io/ebean/typequery/PZoneOffset.java | 29 + .../java/io/ebean/typequery/TQAssocBean.java | 251 ++ .../main/java/io/ebean/typequery/TQPath.java | 14 + .../java/io/ebean/typequery/TQProperty.java | 68 + .../io/ebean/typequery/TQPropertyBase.java | 43 + .../java/io/ebean/typequery/TQRootBean.java | 2093 +++++++++++++++++ .../io/ebean/typequery/TypeQueryBean.java | 22 + .../java/io/ebean/typequery/package-info.java | 37 + .../java/io/ebean/typequery/PBooleanTest.java | 92 + .../test/java/org/example/domain/ACat.java | 16 + .../test/java/org/example/domain/ADog.java | 26 + .../java/org/example/domain/AWildCat.java | 16 + .../test/java/org/example/domain/Address.java | 100 + .../test/java/org/example/domain/Animal.java | 49 + .../java/org/example/domain/BaseModel.java | 69 + .../test/java/org/example/domain/Contact.java | 110 + .../java/org/example/domain/ContactNote.java | 46 + .../test/java/org/example/domain/Country.java | 59 + .../java/org/example/domain/Customer.java | 172 ++ .../test/java/org/example/domain/Order.java | 137 ++ .../java/org/example/domain/OrderDetail.java | 99 + .../test/java/org/example/domain/Product.java | 47 + .../java/org/example/domain/SomePojo.java | 19 + .../example/domain/finder/CustomerFinder.java | 18 + .../example/domain/otherpackage/Email.java | 4 + .../otherpackage/PhoneAttributeConverter.java | 17 + .../domain/otherpackage/PhoneNumber.java | 13 + .../domain/otherpackage/ValidEmail.java | 18 + .../ValidEmailAttributeConverter.java | 17 + .../org/querytest/QCustomerAndOrTest.java | 62 + .../QCustomerSimpleLikeQueryTest.java | 37 + .../java/org/querytest/QCustomerTest.java | 842 +++++++ .../test/java/org/querytest/QOrderTest.java | 44 + ebean-querybean/src/test/resources/ebean.mf | 2 + .../src/test/resources/ebean.properties | 12 + .../src/test/resources/logback-test.xml | 35 + kotlin-querybean-generator/README.md | 9 + kotlin-querybean-generator/pom.xml | 42 + .../io/ebean/querybean/generator/Append.java | 51 + .../ebean/querybean/generator/Constants.java | 37 + .../ebean/querybean/generator/FindDbName.java | 55 + .../generator/KotlinLangAdapter.java | 117 + .../querybean/generator/LangAdapter.java | 19 + .../ebean/querybean/generator/ModuleMeta.java | 21 + .../generator/ProcessingContext.java | 576 +++++ .../ebean/querybean/generator/Processor.java | 117 + .../querybean/generator/PropertyMeta.java | 37 + .../querybean/generator/PropertyType.java | 51 + .../generator/PropertyTypeArray.java | 37 + .../generator/PropertyTypeAssoc.java | 31 + .../querybean/generator/PropertyTypeEnum.java | 37 + .../querybean/generator/PropertyTypeMap.java | 120 + .../generator/PropertyTypeScalar.java | 48 + .../PropertyTypeScalarComparable.java | 48 + .../querybean/generator/ReadModuleInfo.java | 68 + .../generator/SimpleModuleInfoWriter.java | 204 ++ .../generator/SimpleQueryBeanWriter.java | 361 +++ .../io/ebean/querybean/generator/Split.java | 44 + .../javax.annotation.processing.Processor | 1 + .../ebean/querybean/generator/SplitTest.java | 40 + pom.xml | 3 + querybean-generator/README.md | 4 + querybean-generator/pom.xml | 43 + .../io/ebean/querybean/generator/Append.java | 51 + .../ebean/querybean/generator/Constants.java | 38 + .../ebean/querybean/generator/FindDbName.java | 55 + .../ebean/querybean/generator/ModuleMeta.java | 21 + .../generator/ProcessingContext.java | 558 +++++ .../ebean/querybean/generator/Processor.java | 108 + .../querybean/generator/PropertyMeta.java | 46 + .../querybean/generator/PropertyType.java | 50 + .../generator/PropertyTypeArray.java | 37 + .../generator/PropertyTypeAssoc.java | 34 + .../querybean/generator/PropertyTypeEnum.java | 37 + .../querybean/generator/PropertyTypeMap.java | 119 + .../generator/PropertyTypeScalar.java | 48 + .../PropertyTypeScalarComparable.java | 48 + .../querybean/generator/ReadModuleInfo.java | 69 + .../generator/SimpleModuleInfoWriter.java | 202 ++ .../generator/SimpleQueryBeanWriter.java | 395 ++++ .../io/ebean/querybean/generator/Split.java | 34 + .../gradle/incremental.annotation.processors | 1 + .../javax.annotation.processing.Processor | 1 + .../ebean/querybean/generator/SplitTest.java | 35 + 140 files changed, 11583 insertions(+) create mode 100644 ebean-querybean/README.md create mode 100644 ebean-querybean/pom.xml create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/AlreadyEnhancedMarker.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PArray.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PBaseCompareable.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PBaseDate.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PBaseNumber.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PBaseString.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PBaseValueEqual.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PBigDecimal.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PBigInteger.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PBoolean.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PCalendar.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PCidr.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PClass.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PCurrency.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PDayOfWeek.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PDouble.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PDuration.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PEnum.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PFile.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PFloat.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PInet.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PInetAddress.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PInstant.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PInteger.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PJodaDateMidnight.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PJodaDateTime.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PJodaLocalDate.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PJodaLocalDateTime.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PJodaLocalTime.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PJson.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PLocalDate.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PLocalDateTime.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PLocalTime.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PLocale.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PLong.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PMonth.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PMonthDay.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/POffsetDateTime.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/POffsetTime.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PScalar.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PScalarComparable.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PShort.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PSqlDate.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PString.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PTime.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PTimeZone.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PTimestamp.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PUri.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PUrl.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PUtilDate.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PUuid.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PYear.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PYearMonth.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PZoneId.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/PZoneOffset.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/TQAssocBean.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/TQPath.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/TQProperty.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/TQPropertyBase.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/TypeQueryBean.java create mode 100644 ebean-querybean/src/main/java/io/ebean/typequery/package-info.java create mode 100644 ebean-querybean/src/test/java/io/ebean/typequery/PBooleanTest.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/ACat.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/ADog.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/AWildCat.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/Address.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/Animal.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/BaseModel.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/Contact.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/ContactNote.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/Country.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/Customer.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/Order.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/OrderDetail.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/Product.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/SomePojo.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/finder/CustomerFinder.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/otherpackage/Email.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/otherpackage/PhoneAttributeConverter.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/otherpackage/PhoneNumber.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/otherpackage/ValidEmail.java create mode 100644 ebean-querybean/src/test/java/org/example/domain/otherpackage/ValidEmailAttributeConverter.java create mode 100644 ebean-querybean/src/test/java/org/querytest/QCustomerAndOrTest.java create mode 100644 ebean-querybean/src/test/java/org/querytest/QCustomerSimpleLikeQueryTest.java create mode 100644 ebean-querybean/src/test/java/org/querytest/QCustomerTest.java create mode 100644 ebean-querybean/src/test/java/org/querytest/QOrderTest.java create mode 100644 ebean-querybean/src/test/resources/ebean.mf create mode 100644 ebean-querybean/src/test/resources/ebean.properties create mode 100644 ebean-querybean/src/test/resources/logback-test.xml create mode 100644 kotlin-querybean-generator/README.md create mode 100644 kotlin-querybean-generator/pom.xml create mode 100644 kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/Append.java create mode 100644 kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/Constants.java create mode 100644 kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/FindDbName.java create mode 100644 kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/KotlinLangAdapter.java create mode 100644 kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/LangAdapter.java create mode 100644 kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/ModuleMeta.java create mode 100644 kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/ProcessingContext.java create mode 100644 kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/Processor.java create mode 100644 kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyMeta.java create mode 100644 kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyType.java create mode 100644 kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeArray.java create mode 100644 kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeAssoc.java create mode 100644 kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeEnum.java create mode 100644 kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeMap.java create mode 100644 kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeScalar.java create mode 100644 kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeScalarComparable.java create mode 100644 kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/ReadModuleInfo.java create mode 100644 kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleModuleInfoWriter.java create mode 100644 kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleQueryBeanWriter.java create mode 100644 kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/Split.java create mode 100644 kotlin-querybean-generator/src/main/resources/META-INF/services/javax.annotation.processing.Processor create mode 100644 kotlin-querybean-generator/src/test/java/io/ebean/querybean/generator/SplitTest.java create mode 100644 querybean-generator/README.md create mode 100644 querybean-generator/pom.xml create mode 100644 querybean-generator/src/main/java/io/ebean/querybean/generator/Append.java create mode 100644 querybean-generator/src/main/java/io/ebean/querybean/generator/Constants.java create mode 100644 querybean-generator/src/main/java/io/ebean/querybean/generator/FindDbName.java create mode 100644 querybean-generator/src/main/java/io/ebean/querybean/generator/ModuleMeta.java create mode 100644 querybean-generator/src/main/java/io/ebean/querybean/generator/ProcessingContext.java create mode 100644 querybean-generator/src/main/java/io/ebean/querybean/generator/Processor.java create mode 100644 querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyMeta.java create mode 100644 querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyType.java create mode 100644 querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeArray.java create mode 100644 querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeAssoc.java create mode 100644 querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeEnum.java create mode 100644 querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeMap.java create mode 100644 querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeScalar.java create mode 100644 querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeScalarComparable.java create mode 100644 querybean-generator/src/main/java/io/ebean/querybean/generator/ReadModuleInfo.java create mode 100644 querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleModuleInfoWriter.java create mode 100644 querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleQueryBeanWriter.java create mode 100644 querybean-generator/src/main/java/io/ebean/querybean/generator/Split.java create mode 100644 querybean-generator/src/main/resources/META-INF/gradle/incremental.annotation.processors create mode 100644 querybean-generator/src/main/resources/META-INF/services/javax.annotation.processing.Processor create mode 100644 querybean-generator/src/test/java/io/ebean/querybean/generator/SplitTest.java diff --git a/ebean-querybean/README.md b/ebean-querybean/README.md new file mode 100644 index 000000000..772855af0 --- /dev/null +++ b/ebean-querybean/README.md @@ -0,0 +1,4 @@ +# ebean-querybean +Type safe query extension for Ebean ORM + +Refer to the documentation at https://ebean.io/docs/query/query-beans diff --git a/ebean-querybean/pom.xml b/ebean-querybean/pom.xml new file mode 100644 index 000000000..702fa3f55 --- /dev/null +++ b/ebean-querybean/pom.xml @@ -0,0 +1,92 @@ + + + + ebean-parent + io.ebean + 12.4.3-SNAPSHOT + + 4.0.0 + + ebean-querybean + + + + + + + io.ebean + ebean + ${project.version} + provided + + + + io.ebean + querybean-generator + ${project.version} + provided + + + + + io.avaje + avaje-jsr305 + 1.0 + provided + + + + joda-time + joda-time + 1.6 + true + + + + javax.validation + validation-api + 1.0.0.GA + true + + + + org.avaje.composite + junit + 1.1 + test + + + + org.avaje.composite + logback + 1.1 + test + + + + + + + + + + io.repaint.maven + tiles-maven-plugin + 2.17 + true + + + io.ebean.tile:enhancement:12.4.2 + + + + + + + + diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/AlreadyEnhancedMarker.java b/ebean-querybean/src/main/java/io/ebean/typequery/AlreadyEnhancedMarker.java new file mode 100644 index 000000000..cc107a7bb --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/AlreadyEnhancedMarker.java @@ -0,0 +1,18 @@ +package io.ebean.typequery; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used to denote a query bean that has already been enhanced. + *

+ * Used by the agent to detect already enhanced type query beans to skip enhancement processing. + *

+ */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface AlreadyEnhancedMarker { + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PArray.java b/ebean-querybean/src/main/java/io/ebean/typequery/PArray.java new file mode 100644 index 000000000..61d077636 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PArray.java @@ -0,0 +1,98 @@ +package io.ebean.typequery; + +/** + * Array property with E as the element type. + * + * @param the root query bean type + * @param the element type of the DbArray + */ +public class PArray extends TQPropertyBase { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PArray(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PArray(String name, R root, String prefix) { + super(name, root, prefix); + } + + /** + * ARRAY contains the values. + *

+ *

{@code
+   *
+   *   new QContact()
+   *    .phoneNumbers.contains("4321")
+   *    .findList();
+   *
+   * }
+ * + * @param values The values that should be contained in the array + */ + @SafeVarargs + public final R contains(E... values) { + expr().arrayContains(_name, (Object[]) values); + return _root; + } + + /** + * ARRAY does not contain the values. + *

+ *

{@code
+   *
+   *   new QContact()
+   *    .phoneNumbers.notContains("4321")
+   *    .findList();
+   *
+   * }
+ * + * @param values The values that should not be contained in the array + */ + @SafeVarargs + public final R notContains(E... values) { + expr().arrayNotContains(_name, (Object[]) values); + return _root; + } + + /** + * ARRAY is empty. + *

+ *

{@code
+   *
+   *   new QContact()
+   *    .phoneNumbers.isEmpty()
+   *    .findList();
+   *
+   * }
+ */ + public R isEmpty() { + expr().arrayIsEmpty(_name); + return _root; + } + + /** + * ARRAY is not empty. + *

+ *

{@code
+   *
+   *   new QContact()
+   *    .phoneNumbers.isNotEmpty()
+   *    .findList();
+   *
+   * }
+ */ + public R isNotEmpty() { + expr().arrayIsNotEmpty(_name); + return _root; + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PBaseCompareable.java b/ebean-querybean/src/main/java/io/ebean/typequery/PBaseCompareable.java new file mode 100644 index 000000000..c9488f63c --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PBaseCompareable.java @@ -0,0 +1,237 @@ +package io.ebean.typequery; + + +/** + * Base property for all comparable types. + * + * @param the root query bean type + * @param the type of the scalar property + */ +@SuppressWarnings("rawtypes") +public class PBaseCompareable extends PBaseValueEqual { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PBaseCompareable(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PBaseCompareable(String name, R root, String prefix) { + super(name, root, prefix); + } + + // ---- range comparisons ------- + + /** + * Greater than. + * + * @param value the bind value + * @return the root query bean instance + */ + public final R gt(T value) { + expr().gt(_name, value); + return _root; + } + + /** + * Greater than OR Null. + * + * @param value the bind value + * @return the root query bean instance + */ + public final R gtOrNull(T value) { + expr().gtOrNull(_name, value); + return _root; + } + + /** + * Greater than or Equal to. + * + * @param value the bind value + * @return the root query bean instance + */ + public final R ge(T value) { + expr().ge(_name, value); + return _root; + } + + /** + * Greater than or Equal to OR Null. + * + * @param value the bind value + * @return the root query bean instance + */ + public final R geOrNull(T value) { + expr().geOrNull(_name, value); + return _root; + } + + /** + * Less than. + * + * @param value the bind value + * @return the root query bean instance + */ + public final R lt(T value) { + expr().lt(_name, value); + return _root; + } + + /** + * Less than OR Null. + * + * @param value the bind value + * @return the root query bean instance + */ + public final R ltOrNull(T value) { + expr().ltOrNull(_name, value); + return _root; + } + + /** + * Less than or Equal to. + * + * @param value the bind value + * @return the root query bean instance + */ + public final R le(T value) { + expr().le(_name, value); + return _root; + } + + /** + * Less than or Equal to. + * + * @param value the bind value + * @return the root query bean instance + */ + public final R leOrNull(T value) { + expr().leOrNull(_name, value); + return _root; + } + + /** + * Greater or equal to lower value and strictly less than upper value. + *

+ * This is generally preferable over Between for date and datetime types + * as SQL Between is inclusive on the upper bound (<=) and generally we + * need the upper bound to be exclusive (<). + *

+ * + * @param lower the lower bind value (>=) + * @param upper the upper bind value (<) + * @return the root query bean instance + */ + public final R inRange(T lower, T upper) { + expr().inRange(_name, lower, upper); + return _root; + } + + /** + * Value in Range between 2 properties. + * + *
{@code
+   *
+   *    .startDate.inRangeWith(endDate, now)
+   *
+   *    // which equates to
+   *    startDate <= now and (endDate > now or endDate is null)
+   *
+   * }
+ * + *

+ * This is a convenience expression combining a number of simple expressions. + * The most common use of this could be called "effective dating" where 2 date or + * timestamp columns represent the date range in which + */ + public final R inRangeWith(TQProperty highProperty, T value) { + expr().inRangeWith(_name, highProperty._name, value); + return _root; + } + + /** + * Between lower and upper values. + * + * @param lower the lower bind value + * @param upper the upper bind value + * @return the root query bean instance + */ + public final R between(T lower, T upper) { + expr().between(_name, lower, upper); + return _root; + } + + /** + * Greater than. + * + * @param value the bind value + * @return the root query bean instance + */ + public final R greaterThan(T value) { + expr().gt(_name, value); + return _root; + } + + /** + * Greater than or Null. + * + * @param value the bind value + * @return the root query bean instance + */ + public final R greaterThanOrNull(T value) { + expr().gtOrNull(_name, value); + return _root; + } + + /** + * Greater than or Equal to. + * + * @param value the bind value + * @return the root query bean instance + */ + public final R greaterOrEqualTo(T value) { + expr().ge(_name, value); + return _root; + } + + /** + * Less than. + * + * @param value the bind value + * @return the root query bean instance + */ + public final R lessThan(T value) { + expr().lt(_name, value); + return _root; + } + + /** + * Less than or Null. + * + * @param value the bind value + * @return the root query bean instance + */ + public final R lessThanOrNull(T value) { + expr().ltOrNull(_name, value); + return _root; + } + + /** + * Less than or Equal to. + * + * @param value the bind value + * @return the root query bean instance + */ + public final R lessOrEqualTo(T value) { + expr().le(_name, value); + return _root; + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PBaseDate.java b/ebean-querybean/src/main/java/io/ebean/typequery/PBaseDate.java new file mode 100644 index 000000000..fa4631247 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PBaseDate.java @@ -0,0 +1,50 @@ +package io.ebean.typequery; + +/** + * Base property for date and date time types. + * + * @param the root query bean type + * @param the date time type + */ +@SuppressWarnings("rawtypes") +public abstract class PBaseDate extends PBaseCompareable { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PBaseDate(String name, R root) { + super(name , root); + } + + /** + * Construct with additional path prefix. + */ + public PBaseDate(String name, R root, String prefix) { + super(name, root, prefix); + } + + /** + * Same as greater than. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R after(D value) { + expr().gt(_name, value); + return _root; + } + + /** + * Same as less than. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R before(D value) { + expr().lt(_name, value); + return _root; + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PBaseNumber.java b/ebean-querybean/src/main/java/io/ebean/typequery/PBaseNumber.java new file mode 100644 index 000000000..2c54bb6db --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PBaseNumber.java @@ -0,0 +1,109 @@ +package io.ebean.typequery; + +/** + * Base property for number types. + * + * @param the root query bean type + * @param the number type + */ +@SuppressWarnings("rawtypes") +public abstract class PBaseNumber extends PBaseCompareable { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PBaseNumber(String name, R root) { + super(name , root); + } + + /** + * Construct with additional path prefix. + */ + public PBaseNumber(String name, R root, String prefix) { + super(name, root, prefix); + } + + // Additional int versions -- seems the right thing to do + + /** + * Is equal to. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R equalTo(int value) { + expr().eq(_name, value); + return _root; + } + + /** + * Greater than. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R greaterThan(int value) { + expr().gt(_name, value); + return _root; + } + + /** + * Less than. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R lessThan(int value) { + expr().lt(_name, value); + return _root; + } + + + /** + * Is equal to. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R eq(int value) { + expr().eq(_name, value); + return _root; + } + + /** + * Greater than. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R gt(int value) { + expr().gt(_name, value); + return _root; + } + + /** + * Less than. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R lt(int value) { + expr().lt(_name, value); + return _root; + } + + /** + * Between lower and upper values. + * + * @param lower the lower bind value + * @param upper the upper bind value + * @return the root query bean instance + */ + public R between(int lower, int upper) { + expr().between(_name, lower, upper); + return _root; + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PBaseString.java b/ebean-querybean/src/main/java/io/ebean/typequery/PBaseString.java new file mode 100644 index 000000000..b985842e4 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PBaseString.java @@ -0,0 +1,173 @@ +package io.ebean.typequery; + +/** + * Base for property types that store as String Varchar types. + * + * @param the root query bean type + */ +public abstract class PBaseString extends PBaseCompareable { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + PBaseString(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + PBaseString(String name, R root, String prefix) { + super(name, root, prefix); + } + + /** + * Is equal to. The same as eq but uses the strong type as argument rather than String. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public final R equalToType(T value) { + expr().eq(_name, value); + return _root; + } + + /** + * Is not equal to. The same as ne but uses the strong type as argument rather than String. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public final R notEqualToType(T value) { + expr().ne(_name, value); + return _root; + } + + // common string / expressions ------------ + + /** + * Case insensitive is equal to. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R ieq(String value) { + expr().ieq(_name, value); + return _root; + } + + /** + * Case insensitive is equal to. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R iequalTo(String value) { + expr().ieq(_name, value); + return _root; + } + + /** + * Like - include '%' and '_' placeholders as necessary. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R like(String value) { + expr().like(_name, value); + return _root; + } + + /** + * Starts with - uses a like with '%' wildcard added to the end. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R startsWith(String value) { + expr().startsWith(_name, value); + return _root; + } + + /** + * Ends with - uses a like with '%' wildcard added to the beginning. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R endsWith(String value) { + expr().endsWith(_name, value); + return _root; + } + + /** + * Contains - uses a like with '%' wildcard added to the beginning and end. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R contains(String value) { + expr().contains(_name, value); + return _root; + } + + /** + * Case insensitive like. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R ilike(String value) { + expr().ilike(_name, value); + return _root; + } + + /** + * Case insensitive starts with. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R istartsWith(String value) { + expr().istartsWith(_name, value); + return _root; + } + + /** + * Case insensitive ends with. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R iendsWith(String value) { + expr().iendsWith(_name, value); + return _root; + } + + /** + * Case insensitive contains. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R icontains(String value) { + expr().icontains(_name, value); + return _root; + } + + /** + * Add a full text "Match" expression. + *

+ * This means the query will automatically execute against the document store (ElasticSearch). + *

+ * + * @param value the match expression + */ + public R match(String value) { + expr().match(_name, value); + return _root; + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PBaseValueEqual.java b/ebean-querybean/src/main/java/io/ebean/typequery/PBaseValueEqual.java new file mode 100644 index 000000000..c102d9991 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PBaseValueEqual.java @@ -0,0 +1,227 @@ +package io.ebean.typequery; + +import java.util.Collection; + +/** + * Base property for types that primarily have equal to. + * + * @param the root query bean type + * @param the number type + */ +public abstract class PBaseValueEqual extends TQPropertyBase { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PBaseValueEqual(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PBaseValueEqual(String name, R root, String prefix) { + super(name, root, prefix); + } + + /** + * Set the property as the map key for a findMap query. + * + *
{@code
+   *
+   *   Map map =
+   *     new QCustomer()
+   *       .organisation.id.equalTo(42)
+   *       .email.asMapKey() // email property as map key
+   *       .findMap();
+   *
+   * }
+ * + * @return the root query bean instance + */ + public final R asMapKey() { + expr().setMapKey(_name); + return _root; + } + + /** + * Is equal to or Null. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public final R equalToOrNull(T value) { + expr().eqOrNull(_name, value); + return _root; + } + + /** + * Is equal to. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public final R equalTo(T value) { + expr().eq(_name, value); + return _root; + } + + /** + * Is equal to. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public final R eq(T value) { + expr().eq(_name, value); + return _root; + } + + /** + * Is equal to or Null. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public final R eqOrNull(T value) { + expr().eqOrNull(_name, value); + return _root; + } + + /** + * Is not equal to. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public final R notEqualTo(T value) { + expr().ne(_name, value); + return _root; + } + + /** + * Is not equal to. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public final R ne(T value) { + expr().ne(_name, value); + return _root; + } + + /** + * Is in a list of values. + * + * @param values the list of values for the predicate + * @return the root query bean instance + */ + @SafeVarargs + public final R in(T... values) { + expr().in(_name, (Object[]) values); + return _root; + } + + /** + * In where null or empty values means that no predicate is added to the query. + *

+ * That is, only add the IN predicate if the values are not null or empty. + *

+ * Without this we typically need to code an if block to only add + * the IN predicate if the collection is not empty like: + *

+ * + *

Without inOrEmpty()

+ *
{@code
+   *
+   *   List names = Arrays.asList("foo", "bar");
+   *
+   *   QCustomer query = new QCustomer()
+   *       .registered.before(LocalDate.now())
+   *
+   *   // conditionally add the IN expression to the query
+   *   if (names != null && !names.isEmpty()) {
+   *       query.name.in(names)
+   *   }
+   *
+   *   query.findList();
+   *
+   * }
+ * + *

Using inOrEmpty()

+ *
{@code
+   *
+   *   List names = Arrays.asList("foo", "bar");
+   *
+   *   new QCustomer()
+   *       .registered.before(LocalDate.now())
+   *       .name.inOrEmpty(names)
+   *       .findList();
+   *
+   * }
+ */ + public final R inOrEmpty(Collection values) { + expr().inOrEmpty(_name, values); + return _root; + } + + /** + * Is NOT in a list of values. + * + * @param values the list of values for the predicate + * @return the root query bean instance + */ + @SafeVarargs + public final R notIn(T... values) { + expr().notIn(_name, (Object[]) values); + return _root; + } + + /** + * Is in a list of values. Synonym for in(). + * + * @param values the list of values for the predicate + * @return the root query bean instance + */ + @SafeVarargs + public final R isIn(T... values) { + expr().in(_name, (Object[]) values); + return _root; + } + + /** + * Is in a list of values. + * + * @param values the list of values for the predicate + * @return the root query bean instance + */ + public final R in(Collection values) { + expr().in(_name, values); + return _root; + } + + /** + * Is NOT in a list of values. + * + * @param values the list of values for the predicate + * @return the root query bean instance + */ + public final R notIn(Collection values) { + expr().notIn(_name, values); + return _root; + } + + /** + * Is in a list of values. Synonym for in(). + * + * @param values the list of values for the predicate + * @return the root query bean instance + */ + public final R isIn(Collection values) { + expr().in(_name, values); + return _root; + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PBigDecimal.java b/ebean-querybean/src/main/java/io/ebean/typequery/PBigDecimal.java new file mode 100644 index 000000000..c2ab97572 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PBigDecimal.java @@ -0,0 +1,28 @@ +package io.ebean.typequery; + +import java.math.BigDecimal; + +/** + * BigDecimal property. + * @param the root query bean type + */ +public class PBigDecimal extends PBaseNumber { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PBigDecimal(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PBigDecimal(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PBigInteger.java b/ebean-querybean/src/main/java/io/ebean/typequery/PBigInteger.java new file mode 100644 index 000000000..fd96a13dd --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PBigInteger.java @@ -0,0 +1,29 @@ +package io.ebean.typequery; + +import java.math.BigInteger; + +/** + * BigInteger property. + * + * @param the root query bean type + */ +public class PBigInteger extends PBaseNumber { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PBigInteger(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PBigInteger(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PBoolean.java b/ebean-querybean/src/main/java/io/ebean/typequery/PBoolean.java new file mode 100644 index 000000000..bd0f1608a --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PBoolean.java @@ -0,0 +1,70 @@ +package io.ebean.typequery; + +/** + * Boolean property. + * + * @param the root query bean type + */ +public class PBoolean extends PBaseValueEqual { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PBoolean(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PBoolean(String name, R root, String prefix) { + super(name, root, prefix); + } + + /** + * Is true. + * + * @return the root query bean instance + */ + public R isTrue() { + expr().eq(_name, Boolean.TRUE); + return _root; + } + + /** + * Is false. + * + * @return the root query bean instance + */ + public R isFalse() { + expr().eq(_name, Boolean.FALSE); + return _root; + } + + /** + * Is true or false based on the bind value. + * + * @param value the equal to bind value + * + * @return the root query bean instance + */ + public R is(boolean value) { + expr().eq(_name, value); + return _root; + } + + /** + * Is true or false based on the bind value. + * + * @param value the equal to bind value + * + * @return the root query bean instance + */ + public R eq(boolean value) { + expr().eq(_name, value); + return _root; + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PCalendar.java b/ebean-querybean/src/main/java/io/ebean/typequery/PCalendar.java new file mode 100644 index 000000000..d23acb089 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PCalendar.java @@ -0,0 +1,28 @@ +package io.ebean.typequery; + +import java.util.Calendar; + +/** + * Calendar property. + * + * @param the root query bean type + */ +public class PCalendar extends PBaseDate { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PCalendar(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PCalendar(String name, R root, String prefix) { + super(name, root, prefix); + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PCidr.java b/ebean-querybean/src/main/java/io/ebean/typequery/PCidr.java new file mode 100644 index 000000000..78d64fa40 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PCidr.java @@ -0,0 +1,28 @@ +package io.ebean.typequery; + +import io.ebean.types.Cidr; + +/** + * Cidr property. + * + * @param the root query bean type + */ +public class PCidr extends PBaseValueEqual { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PCidr(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PCidr(String name, R root, String prefix) { + super(name, root, prefix); + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PClass.java b/ebean-querybean/src/main/java/io/ebean/typequery/PClass.java new file mode 100644 index 000000000..a372e4004 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PClass.java @@ -0,0 +1,27 @@ +package io.ebean.typequery; + +/** + * Class property. + * + * @param the root query bean type + */ +public class PClass extends PBaseString { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PClass(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PClass(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PCurrency.java b/ebean-querybean/src/main/java/io/ebean/typequery/PCurrency.java new file mode 100644 index 000000000..4eec7eacb --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PCurrency.java @@ -0,0 +1,29 @@ +package io.ebean.typequery; + +import java.util.Currency; + +/** + * Currency property. + * + * @param the root query bean type + */ +public class PCurrency extends PBaseValueEqual { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PCurrency(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PCurrency(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PDayOfWeek.java b/ebean-querybean/src/main/java/io/ebean/typequery/PDayOfWeek.java new file mode 100644 index 000000000..2c9589831 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PDayOfWeek.java @@ -0,0 +1,30 @@ +package io.ebean.typequery; + + +import java.time.DayOfWeek; + +/** + * DayOfWeek property. + * + * @param the root query bean type + */ +public class PDayOfWeek extends PBaseNumber { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PDayOfWeek(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PDayOfWeek(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PDouble.java b/ebean-querybean/src/main/java/io/ebean/typequery/PDouble.java new file mode 100644 index 000000000..0b51dc44b --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PDouble.java @@ -0,0 +1,27 @@ +package io.ebean.typequery; + +/** + * Double property. + * + * @param the root query bean type + */ +public class PDouble extends PBaseNumber { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PDouble(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PDouble(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PDuration.java b/ebean-querybean/src/main/java/io/ebean/typequery/PDuration.java new file mode 100644 index 000000000..2eedd97a8 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PDuration.java @@ -0,0 +1,30 @@ +package io.ebean.typequery; + + +import java.time.Duration; + +/** + * Duration property. + * + * @param the root query bean type + */ +public class PDuration extends PBaseNumber { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PDuration(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PDuration(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PEnum.java b/ebean-querybean/src/main/java/io/ebean/typequery/PEnum.java new file mode 100644 index 000000000..9c5d53fdd --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PEnum.java @@ -0,0 +1,28 @@ +package io.ebean.typequery; + +/** + * BigDecimal property. + * + * @param the enum specific type + * @param the root query bean type + */ +public class PEnum extends PBaseValueEqual { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PEnum(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PEnum(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PFile.java b/ebean-querybean/src/main/java/io/ebean/typequery/PFile.java new file mode 100644 index 000000000..5c78fbac7 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PFile.java @@ -0,0 +1,31 @@ +package io.ebean.typequery; + + +/** + * File property. + *

+ * This is a placeholder in the sense that currently it has no supported expressions. + *

+ * + * @param the root query bean type + */ +public class PFile extends TQPropertyBase { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PFile(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PFile(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PFloat.java b/ebean-querybean/src/main/java/io/ebean/typequery/PFloat.java new file mode 100644 index 000000000..29f41a32e --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PFloat.java @@ -0,0 +1,27 @@ +package io.ebean.typequery; + +/** + * Float property. + * + * @param the root query bean type + */ +public class PFloat extends PBaseNumber { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PFloat(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PFloat(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PInet.java b/ebean-querybean/src/main/java/io/ebean/typequery/PInet.java new file mode 100644 index 000000000..d9be07066 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PInet.java @@ -0,0 +1,28 @@ +package io.ebean.typequery; + +import io.ebean.types.Inet; + +/** + * Inet property. + * + * @param the root query bean type + */ +public class PInet extends PBaseValueEqual { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PInet(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PInet(String name, R root, String prefix) { + super(name, root, prefix); + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PInetAddress.java b/ebean-querybean/src/main/java/io/ebean/typequery/PInetAddress.java new file mode 100644 index 000000000..9a7d958b4 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PInetAddress.java @@ -0,0 +1,29 @@ +package io.ebean.typequery; + +import java.net.InetAddress; + +/** + * InetAddress property. + * + * @param the root query bean type + */ + +public class PInetAddress extends PBaseValueEqual { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PInetAddress(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PInetAddress(String name, R root, String prefix) { + super(name, root, prefix); + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PInstant.java b/ebean-querybean/src/main/java/io/ebean/typequery/PInstant.java new file mode 100644 index 000000000..371f1bd0a --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PInstant.java @@ -0,0 +1,30 @@ +package io.ebean.typequery; + + +import java.time.Instant; + +/** + * Instant property. + * + * @param the root query bean type + */ +public class PInstant extends PBaseDate { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PInstant(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PInstant(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PInteger.java b/ebean-querybean/src/main/java/io/ebean/typequery/PInteger.java new file mode 100644 index 000000000..3aefa8f46 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PInteger.java @@ -0,0 +1,27 @@ +package io.ebean.typequery; + +/** + * Integer property. + * + * @param the root query bean type + */ +public class PInteger extends PBaseNumber { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PInteger(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PInteger(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PJodaDateMidnight.java b/ebean-querybean/src/main/java/io/ebean/typequery/PJodaDateMidnight.java new file mode 100644 index 000000000..a2dc96e52 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PJodaDateMidnight.java @@ -0,0 +1,28 @@ +package io.ebean.typequery; + +import org.joda.time.DateMidnight; + +/** + * Joda DateMidnight property. + * + * @param the root query bean type + */ +public class PJodaDateMidnight extends PBaseDate { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PJodaDateMidnight(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PJodaDateMidnight(String name, R root, String prefix) { + super(name, root, prefix); + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PJodaDateTime.java b/ebean-querybean/src/main/java/io/ebean/typequery/PJodaDateTime.java new file mode 100644 index 000000000..acda95c92 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PJodaDateTime.java @@ -0,0 +1,28 @@ +package io.ebean.typequery; + +import org.joda.time.DateTime; + +/** + * Joda DateTime property. + * + * @param the root query bean type + */ +public class PJodaDateTime extends PBaseDate { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PJodaDateTime(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PJodaDateTime(String name, R root, String prefix) { + super(name, root, prefix); + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PJodaLocalDate.java b/ebean-querybean/src/main/java/io/ebean/typequery/PJodaLocalDate.java new file mode 100644 index 000000000..784832c0f --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PJodaLocalDate.java @@ -0,0 +1,28 @@ +package io.ebean.typequery; + +import org.joda.time.LocalDate; + +/** + * Joda LocalDate property. + * + * @param the root query bean type + */ +public class PJodaLocalDate extends PBaseDate { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PJodaLocalDate(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PJodaLocalDate(String name, R root, String prefix) { + super(name, root, prefix); + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PJodaLocalDateTime.java b/ebean-querybean/src/main/java/io/ebean/typequery/PJodaLocalDateTime.java new file mode 100644 index 000000000..bfac78c04 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PJodaLocalDateTime.java @@ -0,0 +1,28 @@ +package io.ebean.typequery; + +import org.joda.time.LocalDateTime; + +/** + * Joda LocalDateTime property. + * + * @param the root query bean type + */ +public class PJodaLocalDateTime extends PBaseDate { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PJodaLocalDateTime(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PJodaLocalDateTime(String name, R root, String prefix) { + super(name, root, prefix); + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PJodaLocalTime.java b/ebean-querybean/src/main/java/io/ebean/typequery/PJodaLocalTime.java new file mode 100644 index 000000000..5c14ffea9 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PJodaLocalTime.java @@ -0,0 +1,30 @@ +package io.ebean.typequery; + + +import org.joda.time.LocalTime; + +/** + * Joda LocalTime property. + * + * @param the root query bean type + */ +public class PJodaLocalTime extends PBaseNumber { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PJodaLocalTime(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PJodaLocalTime(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PJson.java b/ebean-querybean/src/main/java/io/ebean/typequery/PJson.java new file mode 100644 index 000000000..3601811fb --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PJson.java @@ -0,0 +1,157 @@ +package io.ebean.typequery; + + +/** + * JSON document type. + *

+ * Type that is JSON content mapped to database types such as Postgres JSON/JSONB and otherwise Varchar,Clob and Blob. + *

+ *

+ * The expressions on this type are valid of Postgres and Oracle. + *

+ * + *

+ * The path can reference a nested property in the JSON document using dot notation - + * for example "documentMeta.score" where "score" is an embedded attribute of "documentMeta" + *

+ * + * @param the root query bean type + */ +public class PJson extends TQPropertyBase { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PJson(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PJson(String name, R root, String prefix) { + super(name, root, prefix); + } + + /** + * Path exists - for the given path in a JSON document. + * + *
{@code
+   *
+   *   new QSimpleDoc()
+   *    .content.jsonExists("meta.title")
+   *    .findList();
+   *
+   * }
+ * + * @param path the nested path in the JSON document in dot notation + */ + public R jsonExists(String path) { + expr().jsonExists(_name, path); + return _root; + } + + /** + * Path does not exist - for the given path in a JSON document. + * + *
{@code
+   *
+   *   new QSimpleDoc()
+   *    .content.jsonNotExists("meta.title")
+   *    .findList();
+   *
+   * }
+ * + * @param path the nested path in the JSON document in dot notation + */ + public R jsonNotExists(String path) { + expr().jsonNotExists(_name, path); + return _root; + } + + /** + * Value at the given JSON path is equal to the given value. + * + * + *
{@code
+   *
+   *   new QSimpleDoc()
+   *    .content.jsonEqualTo("title", "Rob JSON in the DB")
+   *    .findList();
+   *
+   * }
+ * + *
{@code
+   *
+   *   new QSimpleDoc()
+   *    .content.jsonEqualTo("path.other", 34)
+   *    .findList();
+   *
+   * }
+ * + * @param path the dot notation path in the JSON document + * @param value the equal to bind value + */ + public R jsonEqualTo(String path, Object value) { + expr().jsonEqualTo(_name, path, value); + return _root; + } + + /** + * Not Equal to - for the given path in a JSON document. + * + * @param path the nested path in the JSON document in dot notation + * @param value the value used to test equality against the document path's value + */ + public R jsonNotEqualTo(String path, Object value) { + expr().jsonNotEqualTo(_name, path, value); + return _root; + } + + /** + * Greater than - for the given path in a JSON document. + * + * @param path the nested path in the JSON document in dot notation + * @param value the value used to test against the document path's value + */ + public R jsonGreaterThan(String path, Object value) { + expr().jsonGreaterThan(_name, path, value); + return _root; + } + + /** + * Greater than or equal to - for the given path in a JSON document. + * + * @param path the nested path in the JSON document in dot notation + * @param value the value used to test against the document path's value + */ + public R jsonGreaterOrEqual(String path, Object value) { + expr().jsonGreaterOrEqual(_name, path, value); + return _root; + } + + /** + * Less than - for the given path in a JSON document. + * + * @param path the nested path in the JSON document in dot notation + * @param value the value used to test against the document path's value + */ + public R jsonLessThan(String path, Object value) { + expr().jsonLessThan(_name, path, value); + return _root; + } + + /** + * Less than or equal to - for the given path in a JSON document. + * + * @param path the nested path in the JSON document in dot notation + * @param value the value used to test against the document path's value + */ + public R jsonLessOrEqualTo(String path, Object value) { + expr().jsonLessOrEqualTo(_name, path, value); + return _root; + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PLocalDate.java b/ebean-querybean/src/main/java/io/ebean/typequery/PLocalDate.java new file mode 100644 index 000000000..6ffd1285a --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PLocalDate.java @@ -0,0 +1,28 @@ +package io.ebean.typequery; + +import java.time.LocalDate; + +/** + * LocalDate property. + * + * @param the root query bean type + */ +public class PLocalDate extends PBaseDate { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PLocalDate(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PLocalDate(String name, R root, String prefix) { + super(name, root, prefix); + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PLocalDateTime.java b/ebean-querybean/src/main/java/io/ebean/typequery/PLocalDateTime.java new file mode 100644 index 000000000..eea2346dc --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PLocalDateTime.java @@ -0,0 +1,28 @@ +package io.ebean.typequery; + +import java.time.LocalDateTime; + +/** + * LocalDateTime property. + * + * @param the root query bean type + */ +public class PLocalDateTime extends PBaseDate { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PLocalDateTime(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PLocalDateTime(String name, R root, String prefix) { + super(name, root, prefix); + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PLocalTime.java b/ebean-querybean/src/main/java/io/ebean/typequery/PLocalTime.java new file mode 100644 index 000000000..8470518f2 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PLocalTime.java @@ -0,0 +1,30 @@ +package io.ebean.typequery; + + +import java.time.LocalTime; + +/** + * LocalTime property. + * + * @param the root query bean type + */ +public class PLocalTime extends PBaseNumber { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PLocalTime(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PLocalTime(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PLocale.java b/ebean-querybean/src/main/java/io/ebean/typequery/PLocale.java new file mode 100644 index 000000000..f6a17e024 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PLocale.java @@ -0,0 +1,29 @@ +package io.ebean.typequery; + +import java.util.Locale; + +/** + * Locale property. + * + * @param the root query bean type + */ +public class PLocale extends PBaseString { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PLocale(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PLocale(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PLong.java b/ebean-querybean/src/main/java/io/ebean/typequery/PLong.java new file mode 100644 index 000000000..e5869682c --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PLong.java @@ -0,0 +1,27 @@ +package io.ebean.typequery; + +/** + * Long property. + * + * @param the root query bean type + */ +public class PLong extends PBaseNumber { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PLong(String name, R root) { + super(name , root); + } + + /** + * Construct with additional path prefix. + */ + public PLong(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PMonth.java b/ebean-querybean/src/main/java/io/ebean/typequery/PMonth.java new file mode 100644 index 000000000..be11e6a2c --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PMonth.java @@ -0,0 +1,28 @@ +package io.ebean.typequery; + +import java.time.Month; + +/** + * LocalDateTime property. + * + * @param the root query bean type + */ +public class PMonth extends PBaseDate { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PMonth(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PMonth(String name, R root, String prefix) { + super(name, root, prefix); + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PMonthDay.java b/ebean-querybean/src/main/java/io/ebean/typequery/PMonthDay.java new file mode 100644 index 000000000..5c614edca --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PMonthDay.java @@ -0,0 +1,28 @@ +package io.ebean.typequery; + +import java.time.MonthDay; + +/** + * MonthDay property. + * + * @param the root query bean type + */ +public class PMonthDay extends PBaseDate { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PMonthDay(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PMonthDay(String name, R root, String prefix) { + super(name, root, prefix); + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/POffsetDateTime.java b/ebean-querybean/src/main/java/io/ebean/typequery/POffsetDateTime.java new file mode 100644 index 000000000..5a32f07e9 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/POffsetDateTime.java @@ -0,0 +1,29 @@ +package io.ebean.typequery; + +import java.time.OffsetDateTime; + +/** + * OffsetDateTime property. + * + * @param the root query bean type + */ +public class POffsetDateTime extends PBaseNumber { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public POffsetDateTime(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public POffsetDateTime(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/POffsetTime.java b/ebean-querybean/src/main/java/io/ebean/typequery/POffsetTime.java new file mode 100644 index 000000000..675b9895a --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/POffsetTime.java @@ -0,0 +1,29 @@ +package io.ebean.typequery; + +import java.time.OffsetTime; + +/** + * OffsetTime property. + * + * @param the root query bean type + */ +public class POffsetTime extends PBaseNumber { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public POffsetTime(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public POffsetTime(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PScalar.java b/ebean-querybean/src/main/java/io/ebean/typequery/PScalar.java new file mode 100644 index 000000000..4e479bc61 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PScalar.java @@ -0,0 +1,29 @@ +package io.ebean.typequery; + +/** + * Property for classes that are serialized/deserialized by + * ScalarType/AttributeConverter. + * + * @param the root query bean type + * @param the scalar type + */ +public class PScalar extends PBaseValueEqual { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PScalar(String name, R root) { + super(name , root); + } + + /** + * Construct with additional path prefix. + */ + public PScalar(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PScalarComparable.java b/ebean-querybean/src/main/java/io/ebean/typequery/PScalarComparable.java new file mode 100644 index 000000000..9364f9a8f --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PScalarComparable.java @@ -0,0 +1,30 @@ +package io.ebean.typequery; + +/** + * Property for classes that are serialized/deserialized by + * ScalarType/AttributeConverter. If the classes are comparable, + * it is assumed that the database can compare the serialized values too. + * + * @param the root query bean type + * @param the scalar type + */ +public class PScalarComparable> extends PBaseCompareable { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PScalarComparable(String name, R root) { + super(name , root); + } + + /** + * Construct with additional path prefix. + */ + public PScalarComparable(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PShort.java b/ebean-querybean/src/main/java/io/ebean/typequery/PShort.java new file mode 100644 index 000000000..440a5ab4c --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PShort.java @@ -0,0 +1,27 @@ +package io.ebean.typequery; + +/** + * Short property. + * + * @param the root query bean type + */ +public class PShort extends PBaseNumber { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PShort(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PShort(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PSqlDate.java b/ebean-querybean/src/main/java/io/ebean/typequery/PSqlDate.java new file mode 100644 index 000000000..5e12b2c66 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PSqlDate.java @@ -0,0 +1,29 @@ +package io.ebean.typequery; + +import java.sql.Date; + +/** + * Java sql Date property. + * + * @param the root query bean type + */ + +public class PSqlDate extends PBaseDate { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PSqlDate(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PSqlDate(String name, R root, String prefix) { + super(name, root, prefix); + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PString.java b/ebean-querybean/src/main/java/io/ebean/typequery/PString.java new file mode 100644 index 000000000..5efdd346b --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PString.java @@ -0,0 +1,149 @@ +package io.ebean.typequery; + +/** + * String property. + * + * @param the root query bean type + */ +public class PString extends PBaseCompareable { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PString(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PString(String name, R root, String prefix) { + super(name, root, prefix); + } + + /** + * Case insensitive is equal to. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R ieq(String value) { + expr().ieq(_name, value); + return _root; + } + + /** + * Case insensitive is equal to. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R iequalTo(String value) { + expr().ieq(_name, value); + return _root; + } + + /** + * Like - include '%' and '_' placeholders as necessary. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R like(String value) { + expr().like(_name, value); + return _root; + } + + /** + * Starts with - uses a like with '%' wildcard added to the end. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R startsWith(String value) { + expr().startsWith(_name, value); + return _root; + } + + /** + * Ends with - uses a like with '%' wildcard added to the beginning. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R endsWith(String value) { + expr().endsWith(_name, value); + return _root; + } + + /** + * Contains - uses a like with '%' wildcard added to the beginning and end. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R contains(String value) { + expr().contains(_name, value); + return _root; + } + + /** + * Case insensitive like. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R ilike(String value) { + expr().ilike(_name, value); + return _root; + } + + /** + * Case insensitive starts with. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R istartsWith(String value) { + expr().istartsWith(_name, value); + return _root; + } + + /** + * Case insensitive ends with. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R iendsWith(String value) { + expr().iendsWith(_name, value); + return _root; + } + + /** + * Case insensitive contains. + * + * @param value the equal to bind value + * @return the root query bean instance + */ + public R icontains(String value) { + expr().icontains(_name, value); + return _root; + } + + /** + * Add a full text "Match" expression. + *

+ * This means the query will automatically execute against the document store (ElasticSearch). + *

+ * + * @param value the match expression + */ + public R match(String value) { + expr().match(_name, value); + return _root; + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PTime.java b/ebean-querybean/src/main/java/io/ebean/typequery/PTime.java new file mode 100644 index 000000000..56f07eaf3 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PTime.java @@ -0,0 +1,30 @@ +package io.ebean.typequery; + + +import java.sql.Time; + +/** + * Time property. + * + * @param the root query bean type + */ +public class PTime extends PBaseNumber { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PTime(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PTime(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PTimeZone.java b/ebean-querybean/src/main/java/io/ebean/typequery/PTimeZone.java new file mode 100644 index 000000000..9257b7555 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PTimeZone.java @@ -0,0 +1,29 @@ +package io.ebean.typequery; + +import java.util.TimeZone; + +/** + * TimeZone property. + * + * @param the root query bean type + */ +public class PTimeZone extends PBaseValueEqual { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PTimeZone(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PTimeZone(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PTimestamp.java b/ebean-querybean/src/main/java/io/ebean/typequery/PTimestamp.java new file mode 100644 index 000000000..31ecbca8a --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PTimestamp.java @@ -0,0 +1,28 @@ +package io.ebean.typequery; + +import java.sql.Timestamp; + +/** + * Property for java sql Timestamp. + * + * @param the root query bean type + */ +public class PTimestamp extends PBaseDate { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PTimestamp(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PTimestamp(String name, R root, String prefix) { + super(name, root, prefix); + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PUri.java b/ebean-querybean/src/main/java/io/ebean/typequery/PUri.java new file mode 100644 index 000000000..37f369515 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PUri.java @@ -0,0 +1,29 @@ +package io.ebean.typequery; + +import java.net.URI; + +/** + * URI property. + * + * @param the root query bean type + */ +public class PUri extends PBaseString { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PUri(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PUri(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PUrl.java b/ebean-querybean/src/main/java/io/ebean/typequery/PUrl.java new file mode 100644 index 000000000..f2a1c8c24 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PUrl.java @@ -0,0 +1,29 @@ +package io.ebean.typequery; + +import java.net.URL; + +/** + * URL property. + * + * @param the root query bean type + */ +public class PUrl extends PBaseString { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PUrl(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PUrl(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PUtilDate.java b/ebean-querybean/src/main/java/io/ebean/typequery/PUtilDate.java new file mode 100644 index 000000000..a63e1311b --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PUtilDate.java @@ -0,0 +1,28 @@ +package io.ebean.typequery; + +import java.util.Date; + +/** + * Java util Date property. + * + * @param the root query bean type + */ +public class PUtilDate extends PBaseDate { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PUtilDate(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PUtilDate(String name, R root, String prefix) { + super(name, root, prefix); + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PUuid.java b/ebean-querybean/src/main/java/io/ebean/typequery/PUuid.java new file mode 100644 index 000000000..e6a4e134f --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PUuid.java @@ -0,0 +1,29 @@ +package io.ebean.typequery; + +import java.util.UUID; + +/** + * UUID property. + * + * @param the root query bean type + */ +public class PUuid extends PBaseValueEqual { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PUuid(String name, R root) { + super(name , root); + } + + /** + * Construct with additional path prefix. + */ + public PUuid(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PYear.java b/ebean-querybean/src/main/java/io/ebean/typequery/PYear.java new file mode 100644 index 000000000..760e26e5f --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PYear.java @@ -0,0 +1,28 @@ +package io.ebean.typequery; + +import java.time.Year; + +/** + * Year property. + * + * @param the root query bean type + */ +public class PYear extends PBaseNumber { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PYear(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PYear(String name, R root, String prefix) { + super(name, root, prefix); + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PYearMonth.java b/ebean-querybean/src/main/java/io/ebean/typequery/PYearMonth.java new file mode 100644 index 000000000..e4b5634bc --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PYearMonth.java @@ -0,0 +1,28 @@ +package io.ebean.typequery; + +import java.time.YearMonth; + +/** + * YearMonth property. + * + * @param the root query bean type + */ +public class PYearMonth extends PBaseDate { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PYearMonth(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PYearMonth(String name, R root, String prefix) { + super(name, root, prefix); + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PZoneId.java b/ebean-querybean/src/main/java/io/ebean/typequery/PZoneId.java new file mode 100644 index 000000000..b10fb0fe6 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PZoneId.java @@ -0,0 +1,29 @@ +package io.ebean.typequery; + +import java.time.ZoneId; + +/** + * ZoneId property. + * + * @param the root query bean type + */ +public class PZoneId extends PBaseValueEqual { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PZoneId(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PZoneId(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/PZoneOffset.java b/ebean-querybean/src/main/java/io/ebean/typequery/PZoneOffset.java new file mode 100644 index 000000000..063e32d81 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/PZoneOffset.java @@ -0,0 +1,29 @@ +package io.ebean.typequery; + +import java.time.ZoneOffset; + +/** + * ZoneOffset property. + * + * @param the root query bean type + */ +public class PZoneOffset extends PBaseValueEqual { + + /** + * Construct with a property name and root instance. + * + * @param name property name + * @param root the root query bean instance + */ + public PZoneOffset(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public PZoneOffset(String name, R root, String prefix) { + super(name, root, prefix); + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/TQAssocBean.java b/ebean-querybean/src/main/java/io/ebean/typequery/TQAssocBean.java new file mode 100644 index 000000000..9f56b6b13 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/TQAssocBean.java @@ -0,0 +1,251 @@ +package io.ebean.typequery; + +import io.ebean.ExpressionList; + +/** + * Base type for associated beans. + * + * @param the entity bean type (normal entity bean type e.g. Customer) + * @param the specific root query bean type (e.g. QCustomer) + */ +@SuppressWarnings("rawtypes") +public abstract class TQAssocBean extends TQProperty { + + /** + * Construct with a property name and root instance. + * + * @param name the name of the property + * @param root the root query bean instance + */ + public TQAssocBean(String name, R root) { + this(name, root, null); + } + + /** + * Construct with additional path prefix. + */ + public TQAssocBean(String name, R root, String prefix) { + super(name, root, prefix); + } + + /** + * Eagerly fetch this association fetching all the properties. + */ + public R fetch() { + ((TQRootBean) _root).query().fetch(_name); + return _root; + } + + /** + * Eagerly fetch this association using a "query join". + */ + public R fetchQuery() { + ((TQRootBean) _root).query().fetchQuery(_name); + return _root; + } + + /** + * Eagerly fetch this association using L2 bean cache. + * Cache misses are populated via fetchQuery(). + */ + public R fetchCache() { + ((TQRootBean) _root).query().fetchCache(_name); + return _root; + } + + /** + * Use lazy loading for fetching this association. + */ + public R fetchLazy() { + ((TQRootBean) _root).query().fetchLazy(_name); + return _root; + } + + /** + * Eagerly fetch this association with the properties specified. + */ + public R fetch(String properties) { + ((TQRootBean) _root).query().fetch(_name, properties); + return _root; + } + + /** + * Eagerly fetch this association using a "query join" with the properties specified. + */ + public R fetchQuery(String properties) { + ((TQRootBean) _root).query().fetchQuery(_name, properties); + return _root; + } + + /** + * Eagerly fetch this association using L2 cache with the properties specified. + * Cache misses are populated via fetchQuery(). + */ + public R fetchCache(String properties) { + ((TQRootBean) _root).query().fetchCache(_name, properties); + return _root; + } + + /** + * Deprecated in favor of fetch(). + * + * @deprecated + */ + public R fetchAll() { + return fetch(); + } + + /** + * Eagerly fetch this association fetching some of the properties. + */ + @SafeVarargs + protected final R fetchProperties(TQProperty... props) { + ((TQRootBean) _root).query().fetch(_name, properties(props)); + return _root; + } + + /** + * Eagerly fetch query this association fetching some of the properties. + */ + @SafeVarargs + protected final R fetchQueryProperties(TQProperty... props) { + ((TQRootBean) _root).query().fetchQuery(_name, properties(props)); + return _root; + } + + /** + * Eagerly fetch this association using L2 bean cache. + */ + @SafeVarargs + protected final R fetchCacheProperties(TQProperty... props) { + ((TQRootBean) _root).query().fetchCache(_name, properties(props)); + return _root; + } + + /** + * Eagerly fetch query this association fetching some of the properties. + */ + @SafeVarargs + protected final R fetchLazyProperties(TQProperty... props) { + ((TQRootBean) _root).query().fetchLazy(_name, properties(props)); + return _root; + } + + /** + * Append the properties as a comma delimited string. + */ + @SafeVarargs + protected final String properties(TQProperty... props) { + StringBuilder selectProps = new StringBuilder(50); + for (int i = 0; i < props.length; i++) { + if (i > 0) { + selectProps.append(","); + } + selectProps.append(props[i].propertyName()); + } + return selectProps.toString(); + } + + /** + * Is equal to by ID property. + */ + public R eq(T other) { + expr().eq(_name, other); + return _root; + } + + /** + * Is equal to by ID property. + */ + public R equalTo(T other) { + return eq(other); + } + + /** + * Is not equal to by ID property. + */ + public R ne(T other) { + expr().ne(_name, other); + return _root; + } + + /** + * Is not equal to by ID property. + */ + public R notEqualTo(T other) { + return ne(other); + } + + /** + * Apply a filter when fetching these beans. + */ + public R filterMany(ExpressionList filter) { + + @SuppressWarnings("unchecked") + ExpressionList expressionList = (ExpressionList) expr().filterMany(_name); + expressionList.addAll(filter); + return _root; + } + + /** + * Apply a filter when fetching these beans. + *

+ * The expressions can use any valid Ebean expression and contain + * placeholders for bind values using ? or ?1 style. + *

+ * + *
{@code
+   *
+   *     new QCustomer()
+   *       .name.startsWith("Postgres")
+   *       .contacts.filterMany("firstName istartsWith ?", "Rob")
+   *       .findList();
+   *
+   * }
+ * + *
{@code
+   *
+   *     new QCustomer()
+   *       .name.startsWith("Postgres")
+   *       .contacts.filterMany("whenCreated inRange ? to ?", startDate, endDate)
+   *       .findList();
+   *
+   * }
+ * + * @param expressions The expressions including and, or, not etc with ? and ?1 bind params. + * @param params The bind parameter values + */ + public R filterMany(String expressions, Object... params) { + expr().filterMany(_name, expressions, params); + return _root; + } + + /** + * Is empty for a collection property. + *

+ * This effectively adds a not exists sub-query on the collection property. + *

+ *

+ * This expression only works on OneToMany and ManyToMany properties. + *

+ */ + public R isEmpty() { + expr().isEmpty(_name); + return _root; + } + + /** + * Is not empty for a collection property. + *

+ * This effectively adds an exists sub-query on the collection property. + *

+ *

+ * This expression only works on OneToMany and ManyToMany properties. + *

+ */ + public R isNotEmpty() { + expr().isNotEmpty(_name); + return _root; + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/TQPath.java b/ebean-querybean/src/main/java/io/ebean/typequery/TQPath.java new file mode 100644 index 000000000..6dde7e1c4 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/TQPath.java @@ -0,0 +1,14 @@ +package io.ebean.typequery; + +/** + * Helper for adding a path prefix to a property. + */ +public class TQPath { + + /** + * Return the full path by adding the prefix to the property name (null safe). + */ + public static String add(String prefix, String name) { + return (prefix == null) ? name : prefix+"."+name; + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/TQProperty.java b/ebean-querybean/src/main/java/io/ebean/typequery/TQProperty.java new file mode 100644 index 000000000..fac9efa56 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/TQProperty.java @@ -0,0 +1,68 @@ +package io.ebean.typequery; + +import io.ebean.ExpressionList; + +/** + * A property used in type query. + * + * @param The type of the owning root bean + */ +public class TQProperty { + + protected final String _name; + + protected final R _root; + + /** + * Construct with a property name and root instance. + * + * @param name the name of the property + * @param root the root query bean instance + */ + public TQProperty(String name, R root) { + this(name, root, null); + } + + /** + * Construct with additional path prefix. + */ + public TQProperty(String name, R root, String prefix) { + this._root = root; + this._name = TQPath.add(prefix, name); + } + + public String toString() { + return _name; + } + + /** + * Internal method to return the underlying expression list. + */ + protected ExpressionList expr() { + return ((TQRootBean) _root).peekExprList(); + } + + /** + * Return the property name. + */ + protected String propertyName() { + return _name; + } + + /** + * Is null. + */ + public R isNull() { + expr().isNull(_name); + return _root; + } + + /** + * Is not null. + */ + public R isNotNull() { + expr().isNotNull(_name); + return _root; + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/TQPropertyBase.java b/ebean-querybean/src/main/java/io/ebean/typequery/TQPropertyBase.java new file mode 100644 index 000000000..a6b8030a0 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/TQPropertyBase.java @@ -0,0 +1,43 @@ +package io.ebean.typequery; + +/** + * Base scalar property. + * + * @param The type of the owning root bean + */ +public class TQPropertyBase extends TQProperty { + + /** + * Construct with a property name and root instance. + * + * @param name the name of the property + * @param root the root query bean instance + */ + public TQPropertyBase(String name, R root) { + super(name, root); + } + + /** + * Construct with additional path prefix. + */ + public TQPropertyBase(String name, R root, String prefix) { + super(name, root, prefix); + } + + /** + * Order by ascending on this property. + */ + public R asc() { + expr().order().asc(_name); + return _root; + } + + /** + * Order by descending on this property. + */ + public R desc() { + expr().order().desc(_name); + return _root; + } + +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java b/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java new file mode 100644 index 000000000..3e68d6ada --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/TQRootBean.java @@ -0,0 +1,2093 @@ +package io.ebean.typequery; + +import io.ebean.CacheMode; +import io.ebean.DB; +import io.ebean.Database; +import io.ebean.DtoQuery; +import io.ebean.ExpressionList; +import io.ebean.FetchConfig; +import io.ebean.FetchGroup; +import io.ebean.FutureIds; +import io.ebean.FutureList; +import io.ebean.FutureRowCount; +import io.ebean.PagedList; +import io.ebean.PersistenceContextScope; +import io.ebean.ProfileLocation; +import io.ebean.Query; +import io.ebean.QueryIterator; +import io.ebean.RawSql; +import io.ebean.Transaction; +import io.ebean.UpdateQuery; +import io.ebean.Version; +import io.ebean.search.MultiMatch; +import io.ebean.search.TextCommonTerms; +import io.ebean.search.TextQueryString; +import io.ebean.search.TextSimple; +import io.ebean.service.SpiFetchGroupQuery; +import io.ebean.text.PathProperties; +import io.ebeaninternal.server.util.ArrayStack; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.sql.Connection; +import java.sql.Timestamp; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Stream; + +/** + * Base root query bean. + *

+ * With code generation for each entity bean type a query bean is created that extends this. + *

+ * Provides common features for all root query beans + *

+ *

+ *

Example - QCustomer extends TQRootBean

+ *

+ * These 'query beans' like QCustomer are generated using the avaje-ebeanorm-typequery-generator. + *

+ *
{@code
+ *
+ *   public class QCustomer extends TQRootBean {
+ *
+ *     // properties
+ *     public PLong id;
+ *
+ *     public PString name;
+ *     ...
+ *
+ * }
+ *

+ *

Example - usage of QCustomer

+ *
{@code
+ *
+ *    Date fiveDaysAgo = ...
+ *
+ *    List customers =
+ *        new QCustomer()
+ *          .name.ilike("rob")
+ *          .status.equalTo(Customer.Status.GOOD)
+ *          .registered.after(fiveDaysAgo)
+ *          .contacts.email.endsWith("@foo.com")
+ *          .orderBy()
+ *            .name.asc()
+ *            .registered.desc()
+ *          .findList();
+ *
+ * }
+ *

+ *

Resulting SQL where

+ *

+ *

{@code sql
+ *
+ *     where lower(t0.name) like ?  and t0.status = ?  and t0.registered > ?  and u1.email like ?
+ *     order by t0.name, t0.registered desc;
+ *
+ *     --bind(rob,GOOD,Mon Jul 27 12:05:37 NZST 2015,%@foo.com)
+ * }
+ * + * @param the entity bean type (normal entity bean type e.g. Customer) + * @param the specific root query bean type (e.g. QCustomer) + */ +public abstract class TQRootBean { + + /** + * The underlying query. + */ + private final Query query; + + /** + * The underlying expression lists held as a stack. Pushed and popped based on and/or (conjunction/disjunction). + */ + private ArrayStack> whereStack; + + /** + * Stack of Text expressions ("query" section of ElasticSearch query rather than "filter" section). + */ + private ArrayStack> textStack; + + /** + * When true expressions should be added to the "text" stack - ElasticSearch "query" section + * rather than the "where" stack. + */ + private boolean textMode; + + /** + * The root query bean instance. Used to provide fluid query construction. + */ + private R root; + + /** + * Construct using the type of bean to query on and the default database. + */ + public TQRootBean(Class beanType) { + this(beanType, DB.getDefault()); + } + + /** + * Construct using the type of bean to query on and a given database. + */ + public TQRootBean(Class beanType, Database database) { + this(database.find(beanType)); + } + + /** + * Construct with a transaction. + */ + protected TQRootBean(Class beanType, Transaction transaction) { + this(beanType); + query.usingTransaction(transaction); + } + + /** + * Construct with a database and transaction. + */ + protected TQRootBean(Class beanType, Database database, Transaction transaction) { + this(beanType, database); + query.usingTransaction(transaction); + } + + /** + * Construct using a query. + */ + @SuppressWarnings("unchecked") + public TQRootBean(Query query) { + this.query = query; + this.root = (R) this; + } + + /** + * Construct for using as an 'Alias' to use the properties as known string + * values for select() and fetch(). + */ + public TQRootBean(boolean aliasDummy) { + this.query = null; + } + + /** + * Return the fetch group. + */ + public FetchGroup buildFetchGroup() { + return ((SpiFetchGroupQuery) query()).buildFetchGroup(); + } + + /** + * Sets the root query bean instance. Used to provide fluid query construction. + */ + protected void setRoot(R root) { + this.root = root; + } + + /** + * Return the underlying query. + *

+ * Generally it is not expected that you will need to do this but typically use + * the find methods available on this 'root query bean' instance like findList(). + *

+ */ + @Nonnull + public Query query() { + return query; + } + + /** + * Explicitly set a comma delimited list of the properties to fetch on the + * 'main' root level entity bean (aka partial object). Note that '*' means all + * properties. + *

+ * You use {@link #fetch(String, String)} to specify specific properties to fetch + * on other non-root level paths of the object graph. + *

+ *

+ *

{@code
+   *
+   * List customers =
+   *     new QCustomer()
+   *     // Only fetch the customer id, name and status.
+   *     // This is described as a "Partial Object"
+   *     .select("name, status")
+   *     .name.ilike("rob%")
+   *     .findList();
+   *
+   * }
+ * + * @param properties the properties to fetch for this bean (* = all properties). + */ + public R select(String properties) { + query.select(properties); + return root; + } + + /** + * Set a FetchGroup to control what part of the object graph is loaded. + *

+ * This is an alternative to using select() and fetch() providing a nice clean separation + * between what a query should load and the query predicates. + *

+ * + *
{@code
+   *
+   * FetchGroup fetchGroup = FetchGroup.of(Customer.class)
+   *   .select("name, status")
+   *   .fetch("contacts", "firstName, lastName, email")
+   *   .build();
+   *
+   * List customers =
+   *
+   *   new QCustomer()
+   *   .select(fetchGroup)
+   *   .findList();
+   *
+   * }
+ */ + public R select(FetchGroup fetchGroup) { + query.select(fetchGroup); + return root; + } + + /** + * Tune the query by specifying the properties to be loaded on the + * 'main' root level entity bean (aka partial object). + *
{@code
+   *
+   *   // alias for the customer properties in select()
+   *   QCustomer cust = QCustomer.alias();
+   *
+   *   // alias for the contact properties in contacts.fetch()
+   *   QContact contact = QContact.alias();
+   *
+   *   List customers =
+   *     new QCustomer()
+   *       // tune query
+   *       .select(cust.id, cust.name)
+   *       .contacts.fetch(contact.firstName, contact.lastName, contact.email)
+   *
+   *       // predicates
+   *       .id.greaterThan(1)
+   *       .findList();
+   *
+   * }
+ * + * @param properties the list of properties to fetch + */ + @SafeVarargs + public final R select(TQProperty... properties) { + StringBuilder selectProps = new StringBuilder(50); + for (int i = 0; i < properties.length; i++) { + if (i > 0) { + selectProps.append(","); + } + selectProps.append(properties[i].propertyName()); + } + query.select(selectProps.toString()); + return root; + } + + /** + * Specify a path to load including all its properties. + *

+ * The same as {@link #fetch(String, String)} with the fetchProperties as "*". + *

+ *
{@code
+   *
+   * List customers =
+   *     new QCustomer()
+   *     // eager fetch the contacts
+   *     .fetch("contacts")
+   *     .findList();
+   *
+   * }
+ * + * @param path the property path of an associated (OneToOne, OneToMany, ManyToOne or ManyToMany) bean. + */ + public R fetch(String path) { + query.fetch(path); + return root; + } + + /** + * Specify a path to load including all its properties using a "query join". + * + *
{@code
+   *
+   * List customers =
+   *     new QCustomer()
+   *     // eager fetch the contacts using a "query join"
+   *     .fetchQuery("contacts")
+   *     .findList();
+   *
+   * }
+ * + * @param path the property path of an associated (OneToOne, OneToMany, ManyToOne or ManyToMany) bean. + */ + public R fetchQuery(String path) { + query.fetchQuery(path); + return root; + } + + /** + * Specify a path to load from L2 cache including all its properties. + * + *
{@code
+   *
+   * List orders =
+   *     new QOrder()
+   *     // eager fetch the customer using L2 cache
+   *     .fetchCache("customer")
+   *     .findList();
+   *
+   * }
+ * + * @param path the property path to load from L2 cache. + */ + public R fetchCache(String path) { + query.fetchCache(path); + return root; + } + + /** + * Specify a path and properties to load using a "query join". + * + *
{@code
+   *
+   * List customers =
+   *     new QCustomer()
+   *     // eager fetch contacts using a "query join"
+   *     .fetchQuery("contacts", "email, firstName, lastName")
+   *     .findList();
+   *
+   * }
+ * + * @param path the property path of an associated (OneToOne, OneToMany, ManyToOne or ManyToMany) bean. + * @param properties the properties to load for this path. + */ + public R fetchQuery(String path, String properties) { + query.fetchQuery(path, properties); + return root; + } + + /** + * Specify a path and properties to load from L2 cache. + * + *
{@code
+   *
+   * List orders =
+   *     new QOrder()
+   *     // eager fetch the customer using L2 cache
+   *     .fetchCache("customer", "name,status")
+   *     .findList();
+   *
+   * }
+ * + * @param path the property path to load from L2 cache. + * @param properties the properties to load for this path. + */ + public R fetchCache(String path, String properties) { + query.fetchCache(path, properties); + return root; + } + + /** + * Specify a path to fetch with its specific properties to include + * (aka partial object). + *

+ * When you specify a join this means that property (associated bean(s)) will + * be fetched and populated. If you specify "*" then all the properties of the + * associated bean will be fetched and populated. You can specify a comma + * delimited list of the properties of that associated bean which means that + * only those properties are fetched and populated resulting in a + * "Partial Object" - a bean that only has some of its properties populated. + *

+ *

+ *

{@code
+   *
+   * // query orders...
+   * List orders =
+   *     new QOrder()
+   *       .fetch("customer", "name, phoneNumber")
+   *       .fetch("customer.billingAddress", "*")
+   *       .findList();
+   *
+   * }
+ *

+ * If columns is null or "*" then all columns/properties for that path are fetched. + *

+ * + *
{@code
+   *
+   * List customers =
+   *     new QCustomer()
+   *     .select("name, status")
+   *     .fetch("contacts", "firstName,lastName,email")
+   *     .findList();
+   *
+   * }
+ * + * @param path the path of an associated (OneToOne, OneToMany, ManyToOne or ManyToMany) bean. + * @param properties properties of the associated bean that you want to include in the + * fetch (* means all properties, null also means all properties). + */ + public R fetch(String path, String properties) { + query.fetch(path, properties); + return root; + } + + /** + * Additionally specify a FetchConfig to use a separate query or lazy loading + * to load this path. + *

+ *

{@code
+   *
+   * // fetch customers (their id, name and status)
+   * List customers =
+   *     new QCustomer()
+   *     .select("name, status")
+   *     .fetch("contacts", "firstName,lastName,email", new FetchConfig().lazy(10))
+   *     .findList();
+   *
+   * }
+ */ + public R fetch(String path, String properties, FetchConfig fetchConfig) { + query.fetch(path, properties, fetchConfig); + return root; + } + + /** + * Additionally specify a FetchConfig to specify a "query join" and or define + * the lazy loading query. + *

+ *

{@code
+   *
+   * // fetch customers (their id, name and status)
+   * List customers =
+   *     new QCustomer()
+   *       // lazy fetch contacts with a batch size of 100
+   *       .fetch("contacts", new FetchConfig().lazy(100))
+   *       .findList();
+   *
+   * }
+ */ + public R fetch(String path, FetchConfig fetchConfig) { + query.fetch(path, fetchConfig); + return root; + } + + /** + * Apply the path properties replacing the select and fetch clauses. + *

+ * This is typically used when the PathProperties is applied to both the query and the JSON output. + *

+ */ + public R apply(PathProperties pathProperties) { + query.apply(pathProperties); + return root; + } + + /** + * Perform an 'As of' query using history tables to return the object graph + * as of a time in the past. + *

+ * To perform this query the DB must have underlying history tables. + *

+ * + * @param asOf the date time in the past at which you want to view the data + */ + public R asOf(Timestamp asOf) { + query.asOf(asOf); + return root; + } + + /** + * Execute the query against the draft set of tables. + */ + public R asDraft() { + query.asDraft(); + return root; + } + + /** + * Execute the query including soft deleted rows. + */ + public R setIncludeSoftDeletes() { + query.setIncludeSoftDeletes(); + return root; + } + + /** + * Set root table alias. + */ + public R alias(String alias) { + query.alias(alias); + return root; + } + + /** + * Set the maximum number of rows to return in the query. + * + * @param maxRows the maximum number of rows to return in the query. + */ + public R setMaxRows(int maxRows) { + query.setMaxRows(maxRows); + return root; + } + + /** + * Set the first row to return for this query. + * + * @param firstRow the first row to include in the query result. + */ + public R setFirstRow(int firstRow) { + query.setFirstRow(firstRow); + return root; + } + + /** + * Execute the query allowing properties with invalid JSON to be collected and not fail the query. + *
{@code
+   *
+   *   // fetch a bean with JSON content
+   *   EBasicJsonList bean= new QEBasicJsonList()
+   *       .id.equalTo(42)
+   *       .setAllowLoadErrors()  // collect errors into bean state if we have invalid JSON
+   *       .findOne();
+   *
+   *
+   *   // get the invalid JSON errors from the bean state
+   *   Map errors = server().getBeanState(bean).getLoadErrors();
+   *
+   *   // If this map is not empty tell we have invalid JSON
+   *   // and should try and fix the JSON content or inform the user
+   *
+   * }
+ */ + public R setAllowLoadErrors() { + query.setAllowLoadErrors(); + return root; + } + + /** + * Explicitly specify whether to use AutoTune for this query. + *

+ * If you do not call this method on a query the "Implicit AutoTune mode" is + * used to determine if AutoTune should be used for a given query. + *

+ *

+ * AutoTune can add additional fetch paths to the query and specify which + * properties are included for each path. If you have explicitly defined some + * fetch paths AutoTune will not remove them. + *

+ */ + public R setAutoTune(boolean autoTune) { + query.setAutoTune(autoTune); + return root; + } + + /** + * A hint which for JDBC translates to the Statement.fetchSize(). + *

+ * Gives the JDBC driver a hint as to the number of rows that should be + * fetched from the database when more rows are needed for ResultSet. + *

+ */ + public R setBufferFetchSizeHint(int fetchSize) { + query.setBufferFetchSizeHint(fetchSize); + return root; + } + + /** + * Set whether this query uses DISTINCT. + */ + public R setDistinct(boolean distinct) { + query.setDistinct(distinct); + return root; + } + + /** + * Set the index(es) to search for a document store which uses partitions. + *

+ * For example, when executing a query against ElasticSearch with daily indexes we can + * explicitly specify the indexes to search against. + *

+ *
{@code
+   *
+   *   // explicitly specify the indexes to search
+   *   query.setDocIndexName("logstash-2016.11.5,logstash-2016.11.6")
+   *
+   *   // search today's index
+   *   query.setDocIndexName("$today")
+   *
+   *   // search the last 3 days
+   *   query.setDocIndexName("$last-3")
+   *
+   * }
+ *

+ * If the indexName is specified with ${daily} e.g. "logstash-${daily}" ... then we can use + * $today and $last-x as the search docIndexName like the examples below. + *

+ *
{@code
+   *
+   *   // search today's index
+   *   query.setDocIndexName("$today")
+   *
+   *   // search the last 3 days
+   *   query.setDocIndexName("$last-3")
+   *
+   * }
+ * + * @param indexName The index or indexes to search against + * @return This query + */ + public R setDocIndexName(String indexName) { + query.setDocIndexName(indexName); + return root; + } + + /** + * Restrict the query to only return subtypes of the given inherit type. + *
{@code
+   *
+   *   List animals =
+   *     new QAnimal()
+   *       .name.startsWith("Fluffy")
+   *       .setInheritType(Cat.class)
+   *       .findList();
+   *
+   * }
+ */ + public R setInheritType(Class type) { + query.setInheritType(type); + return root; + } + + /** + * Set the base table to use for this query. + *

+ * Typically this is used when a table has partitioning and we wish to specify a specific + * partition/table to query against. + *

+ *
{@code
+   *
+   *   QOrder()
+   *   .setBaseTable("order_2019_05")
+   *   .status.equalTo(Status.NEW)
+   *   .findList();
+   *
+   * }
+ */ + public R setBaseTable(String baseTable) { + query.setBaseTable(baseTable); + return root; + } + + /** + * executed the select with "for update" which should lock the record "on read" + */ + public R forUpdate() { + query.forUpdate(); + return root; + } + + /** + * Execute using "for update" clause with "no wait" option. + *

+ * This is typically a Postgres and Oracle only option at this stage. + *

+ */ + public R forUpdateNoWait() { + query.forUpdateNoWait(); + return root; + } + + /** + * Execute using "for update" clause with "skip locked" option. + *

+ * This is typically a Postgres and Oracle only option at this stage. + *

+ */ + public R forUpdateSkipLocked() { + query.forUpdateSkipLocked(); + return root; + } + + /** + * Return this query as an UpdateQuery. + * + *
{@code
+   *
+   *   int rows =
+   *     new QCustomer()
+   *     .name.startsWith("Rob")
+   *     .organisation.id.equalTo(42)
+   *     .asUpdate()
+   *       .set("active", false)
+   *       .update()
+   *
+   * }
+ * + * @return This query as an UpdateQuery + */ + public UpdateQuery asUpdate() { + return query.asUpdate(); + } + + /** + * Convert the query to a DTO bean query. + *

+ * We effectively use the underlying ORM query to build the SQL and then execute + * and map it into DTO beans. + */ + public DtoQuery asDto(Class dtoClass) { + return query.asDto(dtoClass); + } + + /** + * Set the Id value to query. This is used with findOne(). + *

+ * You can use this to have further control over the query. For example adding + * fetch joins. + *

+ *

+ *

{@code
+   *
+   * Order order =
+   *   new QOrder()
+   *     .setId(1)
+   *     .fetch("details")
+   *     .findOne();
+   *
+   * // the order details were eagerly fetched
+   * List details = order.getDetails();
+   *
+   * }
+ */ + public R setId(Object id) { + query.setId(id); + return root; + } + + /** + * Set a list of Id values to match. + *

+ *

{@code
+   *
+   * List orders =
+   *   new QOrder()
+   *     .setIdIn(42, 43, 44)
+   *     .findList();
+   *
+   * // the order details were eagerly fetched
+   * List details = order.getDetails();
+   *
+   * }
+ */ + public R setIdIn(Object... ids) { + query.where().idIn(ids); + return root; + } + + /** + * Set a label on the query. + *

+ * This label can be used to help identify query performance metrics but we can also use + * profile location enhancement on Finders so for some that would be a better option. + *

+ */ + public R setLabel(String label) { + query.setLabel(label); + return root; + } + + /** + * Set the profile location. + *

+ * This is typically set automatically via enhancement when profile location enhancement + * is turned on. It is generally not set by application code. + *

+ */ + public R setProfileLocation(ProfileLocation profileLocation) { + query.setProfileLocation(profileLocation); + return root; + } + + /** + * Set the default lazy loading batch size to use. + *

+ * When lazy loading is invoked on beans loaded by this query then this sets the + * batch size used to load those beans. + * + * @param lazyLoadBatchSize the number of beans to lazy load in a single batch + */ + public R setLazyLoadBatchSize(int lazyLoadBatchSize) { + query.setLazyLoadBatchSize(lazyLoadBatchSize); + return root; + } + + /** + * When set to true all the beans from this query are loaded into the bean + * cache. + */ + public R setLoadBeanCache(boolean loadBeanCache) { + query.setLoadBeanCache(loadBeanCache); + return root; + } + + /** + * Set the property to use as keys for a map. + *

+ * If no property is set then the id property is used. + *

+ *

+ *

{@code
+   *
+   * // Assuming sku is unique for products...
+   *
+   * Map productMap =
+   *     new QProduct()
+   *     // use sku for keys...
+   *     .setMapKey("sku")
+   *     .findMap();
+   *
+   * }
+ * + * @param mapKey the property to use as keys for a map. + */ + public R setMapKey(String mapKey) { + query.setMapKey(mapKey); + return root; + } + + /** + * Specify the PersistenceContextScope to use for this query. + *

+ * When this is not set the 'default' configured on {@link io.ebean.config.ServerConfig#setPersistenceContextScope(PersistenceContextScope)} + * is used - this value defaults to {@link io.ebean.PersistenceContextScope#TRANSACTION}. + *

+ * Note that the same persistence Context is used for subsequent lazy loading and query join queries. + *

+ * Note that #findEach uses a 'per object graph' PersistenceContext so this scope is ignored for + * queries executed as #findIterate, #findEach, #findEachWhile. + * + * @param scope The scope to use for this query and subsequent lazy loading. + */ + public R setPersistenceContextScope(PersistenceContextScope scope) { + query.setPersistenceContextScope(scope); + return root; + } + + /** + * Set RawSql to use for this query. + */ + public R setRawSql(RawSql rawSql) { + query.setRawSql(rawSql); + return root; + } + + /** + * When set to true when you want the returned beans to be read only. + */ + public R setReadOnly(boolean readOnly) { + query.setReadOnly(readOnly); + return root; + } + + /** + * Set this to true to use the bean cache. + *

+ * If the query result is in cache then by default this same instance is + * returned. In this sense it should be treated as a read only object graph. + *

+ */ + public R setUseCache(boolean useCache) { + query.setUseCache(useCache); + return root; + } + + + /** + * Set the mode to use the bean cache when executing this query. + *

+ * By default "find by id" and "find by natural key" will use the bean cache + * when bean caching is enabled. Setting this to false means that the query + * will not use the bean cache and instead hit the database. + *

+ *

+ * By default findList() with natural keys will not use the bean cache. In that + * case we need to explicitly use the bean cache. + *

+ */ + public R setBeanCacheMode(CacheMode beanCacheMode) { + query.setBeanCacheMode(beanCacheMode); + return root; + } + + /** + * Set to true if this query should execute against the doc store. + *

+ * When setting this you may also consider disabling lazy loading. + *

+ */ + public R setUseDocStore(boolean useDocStore) { + query.setUseDocStore(useDocStore); + return root; + } + + /** + * Set true if you want to disable lazy loading. + *

+ * That is, once the object graph is returned further lazy loading is disabled. + *

+ */ + public R setDisableLazyLoading(boolean disableLazyLoading) { + query.setDisableLazyLoading(disableLazyLoading); + return root; + } + + /** + * Disable read auditing for this query. + *

+ * This is intended to be used when the query is not a user initiated query and instead + * part of the internal processing in an application to load a cache or document store etc. + * In these cases we don't want the query to be part of read auditing. + *

+ */ + public R setDisableReadAuditing() { + query.setDisableReadAuditing(); + return root; + } + + /** + * Set this to true to use the query cache. + */ + public R setUseQueryCache(boolean useCache) { + query.setUseQueryCache(useCache); + return root; + } + + /** + * Set the {@link CacheMode} to use the query for executing this query. + */ + public R setUseQueryCache(CacheMode cacheMode) { + query.setUseQueryCache(cacheMode); + return root; + } + + /** + * Set a timeout on this query. + *

+ * This will typically result in a call to setQueryTimeout() on a + * preparedStatement. If the timeout occurs an exception will be thrown - this + * will be a SQLException wrapped up in a PersistenceException. + *

+ * + * @param secs the query timeout limit in seconds. Zero means there is no limit. + */ + public R setTimeout(int secs) { + query.setTimeout(secs); + return root; + } + + /** + * Returns the set of properties or paths that are unknown (do not map to known properties or paths). + *

+ * Validate the query checking the where and orderBy expression paths to confirm if + * they represent valid properties or paths for the given bean type. + *

+ */ + public Set validate() { + return query.validate(); + } + + /** + * Add raw expression with no parameters. + *

+ * When properties in the clause are fully qualified as table-column names + * then they are not translated. logical property name names (not fully + * qualified) will still be translated to their physical name. + *

+ *

+ *

{@code
+   *
+   *   raw("orderQty < shipQty")
+   *
+   * }
+ * + *

Subquery example:

+ *
{@code
+   *
+   *   .raw("t0.customer_id in (select customer_id from customer_group where group_id = any(?::uuid[]))", groupIds)
+   *
+   * }
+ */ + public R raw(String rawExpression) { + peekExprList().raw(rawExpression); + return root; + } + + /** + * Add raw expression with an array of parameters. + *

+ * The raw expression should contain the same number of ? as there are + * parameters. + *

+ *

+ * When properties in the clause are fully qualified as table-column names + * then they are not translated. logical property name names (not fully + * qualified) will still be translated to their physical name. + *

+ */ + public R raw(String rawExpression, Object... bindValues) { + peekExprList().raw(rawExpression, bindValues); + return root; + } + + /** + * Only add the raw expression if the values is not null or empty. + *

+ * This is a pure convenience expression to make it nicer to deal with the pattern where we use + * raw() expression with a subquery and only want to add the subquery predicate when the collection + * of values is not empty. + *

+ *

Without inOrEmpty()

+ *
{@code
+   *
+   *   QCustomer query = new QCustomer() // add some predicates
+   *     .status.equalTo(Status.NEW);
+   *
+   *   // common pattern - we can use rawOrEmpty() instead
+   *   if (orderIds != null && !orderIds.isEmpty()) {
+   *     query.raw("t0.customer_id in (select o.customer_id from orders o where o.id in (?1))", orderIds);
+   *   }
+   *
+   *   query.findList();
+   *
+   * }
+ * + *

Using rawOrEmpty()

+ * Note that in the example below we use the ?1 bind parameter to get "parameter expansion" + * for each element in the collection. + * + *
{@code
+   *
+   *   new QCustomer()
+   *     .status.equalTo(Status.NEW)
+   *     // only add the expression if orderIds is not empty
+   *     .rawOrEmpty("t0.customer_id in (select o.customer_id from orders o where o.id in (?1))", orderIds);
+   *     .findList();
+   *
+   * }
+ * + *

Postgres ANY

+ * With Postgres we would often use the SQL ANY expression and array parameter binding + * rather than IN. + * + *
{@code
+   *
+   *   new QCustomer()
+   *     .status.equalTo(Status.NEW)
+   *     .rawOrEmpty("t0.customer_id in (select o.customer_id from orders o where o.id = any(?))", orderIds);
+   *     .findList();
+   *
+   * }
+ *

+ * Note that we need to cast the Postgres array for UUID types like: + *

+ *
{@code
+   *
+   *   " ... = any(?::uuid[])"
+   *
+   * }
+ * + * @param raw The raw expression that is typically a subquery + * @param values The values which is typically a list or set of id values. + */ + public R rawOrEmpty(String raw, Collection values) { + peekExprList().rawOrEmpty(raw, values); + return root; + } + + /** + * Add raw expression with a single parameter. + *

+ * The raw expression should contain a single ? at the location of the + * parameter. + *

+ *

+ * When properties in the clause are fully qualified as table-column names + * then they are not translated. logical property name names (not fully + * qualified) will still be translated to their physical name. + *

+ *

+ *

Example:

+ *
{@code
+   *
+   *   // use a database function
+   *   raw("add_days(orderDate, 10) < ?", someDate)
+   *
+   * }
+ * + *

Subquery example:

+ *
{@code
+   *
+   *   .raw("t0.customer_id in (select customer_id from customer_group where group_id = any(?::uuid[]))", groupIds)
+   *
+   * }
+ */ + public R raw(String rawExpression, Object bindValue) { + peekExprList().raw(rawExpression, bindValue); + return root; + } + + /** + * Marker that can be used to indicate that the order by clause is defined after this. + *

+ *

Example: order by customer name, order date

+ *
{@code
+   *   List orders =
+   *          new QOrder()
+   *            .customer.name.ilike("rob")
+   *            .orderBy()
+   *              .customer.name.asc()
+   *              .orderDate.asc()
+   *            .findList();
+   *
+   * }
+ */ + public R orderBy() { + // Yes this does not actually do anything! We include it because style wise it makes + // the query nicer to read and suggests that order by definitions are added after this + return root; + } + + /** + * Marker that can be used to indicate that the order by clause is defined after this. + *

+ *

Example: order by customer name, order date

+ *
{@code
+   *   List orders =
+   *          new QOrder()
+   *            .customer.name.ilike("rob")
+   *            .orderBy()
+   *              .customer.name.asc()
+   *              .orderDate.asc()
+   *            .findList();
+   *
+   * }
+ */ + public R order() { + // Yes this does not actually do anything! We include it because style wise it makes + // the query nicer to read and suggests that order by definitions are added after this + return root; + } + + /** + * Set the full raw order by clause replacing the existing order by clause if there is one. + *

+ * This follows SQL syntax using commas between each property with the + * optional asc and desc keywords representing ascending and descending order + * respectively. + */ + public R orderBy(String orderByClause) { + query.orderBy(orderByClause); + return root; + } + + /** + * Set the full raw order by clause replacing the existing order by clause if there is one. + *

+ * This follows SQL syntax using commas between each property with the + * optional asc and desc keywords representing ascending and descending order + * respectively. + */ + public R order(String orderByClause) { + query.order(orderByClause); + return root; + } + + /** + * Begin a list of expressions added by 'OR'. + *

+ * Use endOr() or endJunction() to stop added to OR and 'pop' to the parent expression list. + *

+ *

+ *

Example

+ *

+ * This example uses an 'OR' expression list with an inner 'AND' expression list. + *

+ *
{@code
+   *
+   *    List customers =
+   *          new QCustomer()
+   *            .status.equalTo(Customer.Status.GOOD)
+   *            .or()
+   *              .id.greaterThan(1000)
+   *              .and()
+   *                .name.startsWith("super")
+   *                .registered.after(fiveDaysAgo)
+   *              .endAnd()
+   *            .endOr()
+   *            .orderBy().id.desc()
+   *            .findList();
+   *
+   * }
+ *

Resulting SQL where clause

+ *
{@code sql
+   *
+   *    where t0.status = ?  and (t0.id > ?  or (t0.name like ?  and t0.registered > ? ) )
+   *    order by t0.id desc;
+   *
+   *    --bind(GOOD,1000,super%,Wed Jul 22 00:00:00 NZST 2015)
+   *
+   * }
+ */ + public R or() { + pushExprList(peekExprList().or()); + return root; + } + + /** + * Begin a list of expressions added by 'AND'. + *

+ * Use endAnd() or endJunction() to stop added to AND and 'pop' to the parent expression list. + *

+ *

+ * Note that typically the AND expression is only used inside an outer 'OR' expression. + * This is because the top level expression list defaults to an 'AND' expression list. + *

+ *

Example

+ *

+ * This example uses an 'OR' expression list with an inner 'AND' expression list. + *

+ *
{@code
+   *
+   *    List customers =
+   *          new QCustomer()
+   *            .status.equalTo(Customer.Status.GOOD)
+   *            .or() // OUTER 'OR'
+   *              .id.greaterThan(1000)
+   *              .and()  // NESTED 'AND' expression list
+   *                .name.startsWith("super")
+   *                .registered.after(fiveDaysAgo)
+   *                .endAnd()
+   *              .endOr()
+   *            .orderBy().id.desc()
+   *            .findList();
+   *
+   * }
+ *

Resulting SQL where clause

+ *
{@code sql
+   *
+   *    where t0.status = ?  and (t0.id > ?  or (t0.name like ?  and t0.registered > ? ) )
+   *    order by t0.id desc;
+   *
+   *    --bind(GOOD,1000,super%,Wed Jul 22 00:00:00 NZST 2015)
+   *
+   * }
+ */ + public R and() { + pushExprList(peekExprList().and()); + return root; + } + + /** + * Begin a list of expressions added by NOT. + *

+ * Use endNot() or endJunction() to stop added to NOT and 'pop' to the parent expression list. + *

+ */ + public R not() { + pushExprList(peekExprList().not()); + return root; + } + + /** + * Begin a list of expressions added by MUST. + *

+ * This automatically makes this query a document store query. + *

+ *

+ * Use endJunction() to stop added to MUST and 'pop' to the parent expression list. + *

+ */ + public R must() { + pushExprList(peekExprList().must()); + return root; + } + + /** + * Begin a list of expressions added by MUST NOT. + *

+ * This automatically makes this query a document store query. + *

+ *

+ * Use endJunction() to stop added to MUST NOT and 'pop' to the parent expression list. + *

+ */ + public R mustNot() { + return pushExprList(peekExprList().mustNot()); + } + + /** + * Begin a list of expressions added by SHOULD. + *

+ * This automatically makes this query a document store query. + *

+ *

+ * Use endJunction() to stop added to SHOULD and 'pop' to the parent expression list. + *

+ */ + public R should() { + return pushExprList(peekExprList().should()); + } + + /** + * End a list of expressions added by 'OR'. + */ + public R endJunction() { + if (textMode) { + textStack.pop(); + } else { + whereStack.pop(); + } + return root; + } + + /** + * End OR junction - synonym for endJunction(). + */ + public R endOr() { + return endJunction(); + } + + /** + * End AND junction - synonym for endJunction(). + */ + public R endAnd() { + return endJunction(); + } + + /** + * End NOT junction - synonym for endJunction(). + */ + public R endNot() { + return endJunction(); + } + + /** + * Push the expression list onto the appropriate stack. + */ + private R pushExprList(ExpressionList list) { + if (textMode) { + textStack.push(list); + } else { + whereStack.push(list); + } + return root; + } + + /** + * Add expression after this to the WHERE expression list. + *

+ * For queries against the normal database (not the doc store) this has no effect. + *

+ *

+ * This is intended for use with Document Store / ElasticSearch where expressions can be put into either + * the "query" section or the "filter" section of the query. Full text expressions like MATCH are in the + * "query" section but many expression can be in either - expressions after the where() are put into the + * "filter" section which means that they don't add to the relevance and are also cache-able. + *

+ */ + public R where() { + textMode = false; + return root; + } + + /** + * Begin added expressions to the 'Text' expression list. + *

+ * This automatically makes the query a document store query. + *

+ *

+ * For ElasticSearch expressions added to 'text' go into the ElasticSearch 'query context' + * and expressions added to 'where' go into the ElasticSearch 'filter context'. + *

+ */ + public R text() { + textMode = true; + return root; + } + + /** + * Add a Text Multi-match expression (document store only). + *

+ * This automatically makes the query a document store query. + *

+ */ + public R multiMatch(String query, MultiMatch multiMatch) { + peekExprList().multiMatch(query, multiMatch); + return root; + } + + /** + * Add a Text Multi-match expression (document store only). + *

+ * This automatically makes the query a document store query. + *

+ */ + public R multiMatch(String query, String... properties) { + peekExprList().multiMatch(query, properties); + return root; + } + + /** + * Add a Text common terms expression (document store only). + *

+ * This automatically makes the query a document store query. + *

+ */ + public R textCommonTerms(String query, TextCommonTerms options) { + peekExprList().textCommonTerms(query, options); + return root; + } + + /** + * Add a Text simple expression (document store only). + *

+ * This automatically makes the query a document store query. + *

+ */ + public R textSimple(String query, TextSimple options) { + peekExprList().textSimple(query, options); + return root; + } + + /** + * Add a Text query string expression (document store only). + *

+ * This automatically makes the query a document store query. + *

+ */ + public R textQueryString(String query, TextQueryString options) { + peekExprList().textQueryString(query, options); + return root; + } + + /** + * Execute the query using the given transaction. + */ + public R usingTransaction(Transaction transaction) { + query.usingTransaction(transaction); + return root; + } + + /** + * Execute the query using the given connection. + */ + public R usingConnection(Connection connection) { + query.usingConnection(connection); + return root; + } + + /** + * Execute the query returning true if a row is found. + *

+ * The query is executed using max rows of 1 and will only select the id property. + * This method is really just a convenient way to optimise a query to perform a + * 'does a row exist in the db' check. + *

+ * + *

Example using a query bean:

+ *
{@code
+   *
+   *   boolean userExists =
+   *     new QContact()
+   *       .email.equalTo("rob@foo.com")
+   *       .exists();
+   *
+   * }
+ * + *

Example:

+ *
{@code
+   *
+   *   boolean userExists = query()
+   *     .where().eq("email", "rob@foo.com")
+   *     .exists();
+   *
+   * }
+ * + * @return True if the query finds a matching row in the database + */ + public boolean exists() { + return query.exists(); + } + + /** + * Execute the query returning either a single bean or null (if no matching + * bean is found). + *

+ * If more than 1 row is found for this query then a PersistenceException is + * thrown. + *

+ *

+ * This is useful when your predicates dictate that your query should only + * return 0 or 1 results. + *

+ *

+ *

{@code
+   *
+   * // assuming the sku of products is unique...
+   * Product product =
+   *     new QProduct()
+   *         .sku.equalTo("aa113")
+   *         .findOne();
+   * ...
+   * }
+ *

+ *

+ * It is also useful with finding objects by their id when you want to specify + * further join information to optimise the query. + *

+ *

+ *

{@code
+   *
+   * // Fetch order 42 and additionally fetch join its order details...
+   * Order order =
+   *     new QOrder()
+   *         .fetch("details") // eagerly load the order details
+   *         .id.equalTo(42)
+   *         .findOne();
+   *
+   * // the order details were eagerly loaded
+   * List details = order.getDetails();
+   * ...
+   * }
+ */ + @Nullable + public T findOne() { + return query.findOne(); + } + + /** + * Execute the query returning an optional bean. + */ + @Nonnull + public Optional findOneOrEmpty() { + return query.findOneOrEmpty(); + } + + /** + * Execute the query returning the list of objects. + *

+ * This query will execute against the EbeanServer that was used to create it. + *

+ *

+ *

{@code
+   *
+   * List customers =
+   *     new QCustomer()
+   *       .name.ilike("rob%")
+   *       .findList();
+   *
+   * }
+ * + * @see Query#findList() + */ + @Nonnull + public List findList() { + return query.findList(); + } + + /** + * Execute the query returning the result as a Stream. + *

+ * Note that this can support very large queries iterating + * any number of results. To do so internally it can use + * multiple persistence contexts. + *

+ *
{@code
+   *
+   *  // use try with resources to ensure Stream is closed
+   *
+   *  try (Stream stream = query.findStream()) {
+   *    stream
+   *    .map(...)
+   *    .collect(...);
+   *  }
+   *
+   * }
+ */ + @Nonnull + public Stream findStream() { + return query.findStream(); + } + + /** + * Deprecated - migrate to findStream(). + */ + @Deprecated + public Stream findLargeStream() { + return query.findLargeStream(); + } + + /** + * Execute the query returning the set of objects. + *

+ * This query will execute against the EbeanServer that was used to create it. + *

+ *

+ *

{@code
+   *
+   * Set customers =
+   *     new QCustomer()
+   *       .name.ilike("rob%")
+   *       .findSet();
+   *
+   * }
+ * + * @see Query#findSet() + */ + @Nonnull + public Set findSet() { + return query.findSet(); + } + + /** + * Execute the query returning the list of Id's. + *

+ * This query will execute against the EbeanServer that was used to create it. + *

+ * + * @see Query#findIds() + */ + @Nonnull + public List findIds() { + return query.findIds(); + } + + /** + * Execute the query returning a map of the objects. + *

+ * This query will execute against the EbeanServer that was used to create it. + *

+ *

+ * You can use setMapKey() or asMapKey() to specify the property to be used as keys + * on the map. If one is not specified then the id property is used. + *

+ *

+ *

{@code
+   *
+   * Map map =
+   *   new QProduct()
+   *     .sku.asMapKey()
+   *     .findMap();
+   *
+   * }
+ * + * @see Query#findMap() + */ + @Nonnull + public Map findMap() { + return query.findMap(); + } + + /** + * Execute the query iterating over the results. + *

+ * Note that findIterate (and findEach and findEachWhile) uses a "per graph" + * persistence context scope and adjusts jdbc fetch buffer size for large + * queries. As such it is better to use findList for small queries. + *

+ *

+ * Remember that with {@link QueryIterator} you must call {@link QueryIterator#close()} + * when you have finished iterating the results (typically in a finally block). + *

+ *

+ * findEach() and findEachWhile() are preferred to findIterate() as they ensure + * the jdbc statement and resultSet are closed at the end of the iteration. + *

+ *

+ * This query will execute against the EbeanServer that was used to create it. + *

+ *
{@code
+   *
+   *  Query query =
+   *    new QCustomer()
+   *     .status.equalTo(Customer.Status.NEW)
+   *     .orderBy()
+   *       id.asc()
+   *     .query();
+   *
+   *  try (QueryIterator it = query.findIterate()) {
+   *    while (it.hasNext()) {
+   *      Customer customer = it.next();
+   *      // do something with customer ...
+   *    }
+   *  }
+   *
+   * }
+ */ + @Nonnull + public QueryIterator findIterate() { + return query.findIterate(); + } + + /** + * Execute the query returning a list of values for a single property. + *

+ *

Example

+ *
{@code
+   *
+   *  List names =
+   *    new QCustomer()
+   *      .setDistinct(true)
+   *      .select(name)
+   *      .findSingleAttributeList();
+   *
+   * }
+ * + * @return the list of values for the selected property + */ + @Nonnull + public
List findSingleAttributeList() { + return query.findSingleAttributeList(); + } + + /** + * Execute the query returning a single value for a single property. + *

+ *

Example

+ *
{@code
+   *
+   *  LocalDate maxDate =
+   *    new QCustomer()
+   *      .select("max(startDate)")
+   *      .findSingleAttribute();
+   *
+   * }
+ * + * @return the list of values for the selected property + */ + public
A findSingleAttribute() { + return query.findSingleAttribute(); + } + + /** + * Execute the query processing the beans one at a time. + *

+ * This method is appropriate to process very large query results as the + * beans are consumed one at a time and do not need to be held in memory + * (unlike #findList #findSet etc) + *

+ *

+ * Note that internally Ebean can inform the JDBC driver that it is expecting larger + * resultSet and specifically for MySQL this hint is required to stop it's JDBC driver + * from buffering the entire resultSet. As such, for smaller resultSets findList() is + * generally preferable. + *

+ *

+ * Compared with #findEachWhile this will always process all the beans where as + * #findEachWhile provides a way to stop processing the query result early before + * all the beans have been read. + *

+ *

+ * This method is functionally equivalent to findIterate() but instead of using an + * iterator uses the QueryEachConsumer (SAM) interface which is better suited to use + * with Java8 closures. + *

+ *

+ *

{@code
+   *
+   *  new QCustomer()
+   *     .status.equalTo(Status.NEW)
+   *     .orderBy().id.asc()
+   *     .findEach((Customer customer) -> {
+   *
+   *       // do something with customer
+   *       System.out.println("-- visit " + customer);
+   *     });
+   *
+   * }
+ * + * @param consumer the consumer used to process the queried beans. + */ + public void findEach(Consumer consumer) { + query.findEach(consumer); + } + + /** + * Execute the query using callbacks to a visitor to process the resulting + * beans one at a time. + *

+ * This method is functionally equivalent to findIterate() but instead of using an + * iterator uses the QueryEachWhileConsumer (SAM) interface which is better suited to use + * with Java8 closures. + *

+ *

+ *

+ *

{@code
+   *
+   *  new QCustomer()
+   *     .status.equalTo(Status.NEW)
+   *     .orderBy().id.asc()
+   *     .findEachWhile((Customer customer) -> {
+   *
+   *       // do something with customer
+   *       System.out.println("-- visit " + customer);
+   *
+   *       // return true to continue processing or false to stop
+   *       return (customer.getId() < 40);
+   *     });
+   *
+   * }
+ * + * @param consumer the consumer used to process the queried beans. + */ + public void findEachWhile(Predicate consumer) { + query.findEachWhile(consumer); + } + + /** + * Return versions of a @History entity bean. + *

+ * Generally this query is expected to be a find by id or unique predicates query. + * It will execute the query against the history returning the versions of the bean. + *

+ */ + @Nonnull + public List> findVersions() { + return query.findVersions(); + } + + /** + * Return versions of a @History entity bean between a start and end timestamp. + *

+ * Generally this query is expected to be a find by id or unique predicates query. + * It will execute the query against the history returning the versions of the bean. + *

+ */ + @Nonnull + public List> findVersionsBetween(Timestamp start, Timestamp end) { + return query.findVersionsBetween(start, end); + } + + /** + * Return the count of entities this query should return. + *

+ * This is the number of 'top level' or 'root level' entities. + *

+ */ + @Nonnull + public int findCount() { + return query.findCount(); + } + + /** + * Execute find row count query in a background thread. + *

+ * This returns a Future object which can be used to cancel, check the + * execution status (isDone etc) and get the value (with or without a + * timeout). + *

+ * + * @return a Future object for the row count query + */ + @Nonnull + public FutureRowCount findFutureCount() { + return query.findFutureCount(); + } + + /** + * Execute find Id's query in a background thread. + *

+ * This returns a Future object which can be used to cancel, check the + * execution status (isDone etc) and get the value (with or without a + * timeout). + *

+ * + * @return a Future object for the list of Id's + */ + @Nonnull + public FutureIds findFutureIds() { + return query.findFutureIds(); + } + + /** + * Execute find list query in a background thread. + *

+ * This query will execute in it's own PersistenceContext and using its own transaction. + * What that means is that it will not share any bean instances with other queries. + *

+ * + * @return a Future object for the list result of the query + */ + @Nonnull + public FutureList findFutureList() { + return query.findFutureList(); + } + + /** + * Return a PagedList for this query using firstRow and maxRows. + *

+ * The benefit of using this over findList() is that it provides functionality to get the + * total row count etc. + *

+ *

+ * If maxRows is not set on the query prior to calling findPagedList() then a + * PersistenceException is thrown. + *

+ *

+ *

{@code
+   *
+   *  PagedList pagedList =
+   *    new QOrder()
+   *       .setFirstRow(50)
+   *       .setMaxRows(20)
+   *       .findPagedList();
+   *
+   *       // fetch the total row count in the background
+   *       pagedList.loadRowCount();
+   *
+   *       List orders = pagedList.getList();
+   *       int totalRowCount = pagedList.getTotalRowCount();
+   *
+   * }
+ * + * @return The PagedList + */ + @Nonnull + public PagedList findPagedList() { + return query.findPagedList(); + } + + /** + * Execute as a delete query deleting the 'root level' beans that match the predicates + * in the query. + *

+ * Note that if the query includes joins then the generated delete statement may not be + * optimal depending on the database platform. + *

+ * + * @return the number of beans/rows that were deleted. + */ + public int delete() { + return query.delete(); + } + + /** + * Return the sql that was generated for executing this query. + *

+ * This is only available after the query has been executed and provided only + * for informational purposes. + *

+ */ + public String getGeneratedSql() { + return query.getGeneratedSql(); + } + + /** + * Return the type of beans being queried. + */ + @Nonnull + public Class getBeanType() { + return query.getBeanType(); + } + + /** + * Return the expression list that has been built for this query. + */ + @Nonnull + public ExpressionList getExpressionList() { + return query.where(); + } + + /** + * Start adding expressions to the having clause when using @Aggregation properties. + * + *
{@code
+   *
+   *   new QMachineUse()
+   *   // where ...
+   *   .date.inRange(fromDate, toDate)
+   *
+   *   .having()
+   *   .sumHours.greaterThan(1)
+   *   .findList()
+   *
+   *   // The sumHours property uses @Aggregation
+   *   // e.g. @Aggregation("sum(hours)")
+   *
+   * }
+ */ + public R having() { + if (whereStack == null) { + whereStack = new ArrayStack<>(); + } + // effectively putting having expression list onto stack + // such that expression now add to the having clause + whereStack.push(query.having()); + return root; + } + + /** + * Return the underlying having clause to typically when using dynamic aggregation formula. + *

+ * Note that after this we no longer have the query bean so typically we use this right + * at the end of the query. + *

+ * + *
{@code
+   *
+   *  // sum(distanceKms) ... is a "dynamic formula"
+   *  // so we use havingClause() for it like:
+   *
+   *  List machineUse =
+   *
+   *    new QMachineUse()
+   *      .select("machine, sum(fuelUsed), sum(distanceKms)")
+   *
+   *      // where ...
+   *      .date.greaterThan(LocalDate.now().minusDays(7))
+   *
+   *      .havingClause()
+   *        .gt("sum(distanceKms)", 2)
+   *        .findList();
+   *
+   * }
+ */ + public ExpressionList havingClause() { + return query.having(); + } + + /** + * Return the current expression list that expressions should be added to. + */ + protected ExpressionList peekExprList() { + + if (textMode) { + // return the current text expression list + return _peekText(); + } + + if (whereStack == null) { + whereStack = new ArrayStack<>(); + whereStack.push(query.where()); + } + // return the current expression list + return whereStack.peek(); + } + + protected ExpressionList _peekText() { + if (textStack == null) { + textStack = new ArrayStack<>(); + // empty so push on the queries base expression list + textStack.push(query.text()); + } + // return the current expression list + return textStack.peek(); + } +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/TypeQueryBean.java b/ebean-querybean/src/main/java/io/ebean/typequery/TypeQueryBean.java new file mode 100644 index 000000000..d0a538ee5 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/TypeQueryBean.java @@ -0,0 +1,22 @@ +package io.ebean.typequery; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Used to denote a type query bean. + *

+ * These are typically generated beans used to build queries using type safe query criteria. + *

+ */ +@Target({ ElementType.TYPE }) +@Retention(RetentionPolicy.RUNTIME) +public @interface TypeQueryBean { + + /** + * The version description for the query bean. + */ + String value() default "v0"; +} diff --git a/ebean-querybean/src/main/java/io/ebean/typequery/package-info.java b/ebean-querybean/src/main/java/io/ebean/typequery/package-info.java new file mode 100644 index 000000000..ca9613887 --- /dev/null +++ b/ebean-querybean/src/main/java/io/ebean/typequery/package-info.java @@ -0,0 +1,37 @@ +/** + * Provides type safe query criteria support for Ebean ORM queries. + *

+ * 'Query beans' like QCustomer are generated using the avaje-ebeanorm-typequery-generator + * for each entity bean type and can then be used to build queries with type safe criteria. + *

+ * + *

Example - usage of QCustomer

+ *
{@code
+ *
+ *    Date fiveDaysAgo = ...
+ *
+ *    List customers =
+ *        new QCustomer()
+ *
+ *          // name is a known property of type string so
+ *          // it has relevant expressions such as like, startsWith etc
+ *          .name.ilike("rob")
+ *
+ *          // status is a specific Enum type is equalTo() in() etc
+ *          .status.equalTo(Customer.Status.GOOD)
+ *
+ *          // registered is a date type with after(), before() etc
+ *          .registered.after(fiveDaysAgo)
+ *
+ *          // contacts is an associated bean containing specific
+ *          // properties and in this case we use email which is a string type
+ *          .contacts.email.endsWith("@foo.com")
+ *
+ *          .orderBy()
+ *            .name.asc()
+ *            .registered.desc()
+ *          .findList();
+ *
+ * }
+ */ +package io.ebean.typequery; \ No newline at end of file diff --git a/ebean-querybean/src/test/java/io/ebean/typequery/PBooleanTest.java b/ebean-querybean/src/test/java/io/ebean/typequery/PBooleanTest.java new file mode 100644 index 000000000..283603c5f --- /dev/null +++ b/ebean-querybean/src/test/java/io/ebean/typequery/PBooleanTest.java @@ -0,0 +1,92 @@ +package io.ebean.typequery; + +import io.ebeaninternal.server.expression.DefaultExpressionList; +import io.ebeaninternal.server.expression.SimpleExpression; +import org.example.domain.Customer; +import org.example.domain.query.QCustomer; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PBooleanTest { + + PBoolean property(QCustomer customer) { + return new PBoolean<>("active", customer); + } + + @Test + public void constructWithPrefix() throws Exception { + + QCustomer customer = new QCustomer(); + PBoolean property = new PBoolean<>("active", customer, null); + property.isTrue(); + + assertThat(getExpression(customer).getValue()).isEqualTo(Boolean.TRUE); + } + @Test + public void isTrue() throws Exception { + + QCustomer customer = new QCustomer(); + PBoolean property = property(customer); + property.isTrue(); + + assertThat(getExpression(customer).getValue()).isEqualTo(Boolean.TRUE); + } + + @Test + public void isFalse() throws Exception { + + QCustomer customer = new QCustomer(); + PBoolean property = property(customer); + property.isFalse(); + + assertThat(getExpression(customer).getValue()).isEqualTo(Boolean.FALSE); + } + + @Test + public void is_false() throws Exception { + + QCustomer customer = new QCustomer(); + PBoolean property = property(customer); + property.is(false); + + assertThat(getExpression(customer).getValue()).isEqualTo(Boolean.FALSE); + } + + @Test + public void is_true() throws Exception { + + QCustomer customer = new QCustomer(); + PBoolean property = property(customer); + property.is(true); + + assertThat(getExpression(customer).getValue()).isEqualTo(Boolean.TRUE); + } + + @Test + public void eq_true() throws Exception { + + QCustomer customer = new QCustomer(); + PBoolean property = property(customer); + property.eq(true); + + assertThat(getExpression(customer).getValue()).isEqualTo(Boolean.TRUE); + } + + @Test + public void eq_false() throws Exception { + + QCustomer customer = new QCustomer(); + PBoolean property = property(customer); + property.eq(false); + + assertThat(getExpression(customer).getValue()).isEqualTo(Boolean.FALSE); + } + + private SimpleExpression getExpression(QCustomer customer) { + DefaultExpressionList where = (DefaultExpressionList)customer.query().where(); + return (SimpleExpression)where.getUnderlyingList().get(0); + } + + +} diff --git a/ebean-querybean/src/test/java/org/example/domain/ACat.java b/ebean-querybean/src/test/java/org/example/domain/ACat.java new file mode 100644 index 000000000..9720608fb --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/ACat.java @@ -0,0 +1,16 @@ +package org.example.domain; + +import javax.persistence.DiscriminatorValue; +import javax.persistence.Entity; +import javax.persistence.Inheritance; + +@Inheritance +@DiscriminatorValue("CAT") +@Entity +public class ACat extends Animal { + + public ACat(String name) { + super(name); + } + +} diff --git a/ebean-querybean/src/test/java/org/example/domain/ADog.java b/ebean-querybean/src/test/java/org/example/domain/ADog.java new file mode 100644 index 000000000..81646476b --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/ADog.java @@ -0,0 +1,26 @@ +package org.example.domain; + +import javax.persistence.DiscriminatorValue; +import javax.persistence.Entity; +import javax.persistence.Inheritance; + +@Inheritance +@DiscriminatorValue("DOG") +@Entity +public class ADog extends Animal { + + private String registration; + + public ADog(String name, String registration) { + super(name); + this.registration = registration; + } + + public String getRegistration() { + return registration; + } + + public void setRegistration(String registration) { + this.registration = registration; + } +} diff --git a/ebean-querybean/src/test/java/org/example/domain/AWildCat.java b/ebean-querybean/src/test/java/org/example/domain/AWildCat.java new file mode 100644 index 000000000..85e2e3205 --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/AWildCat.java @@ -0,0 +1,16 @@ +package org.example.domain; + +import javax.persistence.DiscriminatorValue; +import javax.persistence.Entity; +import javax.persistence.Inheritance; + +@Inheritance +@DiscriminatorValue("WC") +@Entity +public class AWildCat extends ACat { + + public AWildCat(String name) { + super(name); + } + +} diff --git a/ebean-querybean/src/test/java/org/example/domain/Address.java b/ebean-querybean/src/test/java/org/example/domain/Address.java new file mode 100644 index 000000000..7c46b1d90 --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/Address.java @@ -0,0 +1,100 @@ +package org.example.domain; + +import javax.persistence.Entity; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import javax.validation.constraints.Size; + +/** + * Address entity bean. + */ +@Entity +@Table(name = "o_address") +public class Address extends BaseModel { + + @Size(max = 100) + String line1; + + @Size(max = 100) + String line2; + + @Size(max = 100) + String city; + + @ManyToOne + Country country; + + /** + * Create a copy of the address. Used to provide a 'snapshot' of + * the shippingAddress for a give order. + */ + public Address createCopy() { + Address copy = new Address(); + copy.setLine1(line1); + copy.setLine2(line2); + copy.setCity(city); + copy.setCountry(country); + return copy; + } + + public String toString() { + return id + " " + line1 + " " + line2 + " " + city + " " + country; + } + + /** + * Return line 1. + */ + public String getLine1() { + return line1; + } + + /** + * Set line 1. + */ + public void setLine1(String line1) { + this.line1 = line1; + } + + /** + * Return line 2. + */ + public String getLine2() { + return line2; + } + + /** + * Set line 2. + */ + public void setLine2(String line2) { + this.line2 = line2; + } + + /** + * Return city. + */ + public String getCity() { + return city; + } + + /** + * Set city. + */ + public void setCity(String city) { + this.city = city; + } + + /** + * Return country. + */ + public Country getCountry() { + return country; + } + + /** + * Set country. + */ + public void setCountry(Country country) { + this.country = country; + } + +} diff --git a/ebean-querybean/src/test/java/org/example/domain/Animal.java b/ebean-querybean/src/test/java/org/example/domain/Animal.java new file mode 100644 index 000000000..2a4196c06 --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/Animal.java @@ -0,0 +1,49 @@ +package org.example.domain; + +import io.ebean.Model; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Inheritance; +import javax.persistence.Version; + +@Entity +@Inheritance +public abstract class Animal extends Model { + + @Id + long id; + + @Version + long version; + + String name; + + public Animal(String name) { + this.name = name; + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public long getVersion() { + return version; + } + + public void setVersion(long version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/ebean-querybean/src/test/java/org/example/domain/BaseModel.java b/ebean-querybean/src/test/java/org/example/domain/BaseModel.java new file mode 100644 index 000000000..593aa1ce4 --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/BaseModel.java @@ -0,0 +1,69 @@ +package org.example.domain; + +import io.ebean.Model; +import io.ebean.annotation.CreatedTimestamp; +import io.ebean.annotation.UpdatedTimestamp; + +import javax.persistence.Id; +import javax.persistence.MappedSuperclass; +import javax.persistence.Version; +import java.sql.Timestamp; + +/** + * Base domain object with Id, version, whenCreated and whenUpdated. + * + *

+ * Extending Model to enable the 'active record' style. + * + *

+ * whenCreated and whenUpdated are generally useful for maintaining external search services (like + * elasticsearch) and audit. + */ +@MappedSuperclass +public abstract class BaseModel extends Model { + + @Id + Long id; + + @Version + Long version; + + @CreatedTimestamp + Timestamp whenCreated; + + @UpdatedTimestamp + Timestamp whenUpdated; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public Timestamp getWhenCreated() { + return whenCreated; + } + + public void setWhenCreated(Timestamp whenCreated) { + this.whenCreated = whenCreated; + } + + public Timestamp getWhenUpdated() { + return whenUpdated; + } + + public void setWhenUpdated(Timestamp whenUpdated) { + this.whenUpdated = whenUpdated; + } + +} diff --git a/ebean-querybean/src/test/java/org/example/domain/Contact.java b/ebean-querybean/src/test/java/org/example/domain/Contact.java new file mode 100644 index 000000000..e47f66248 --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/Contact.java @@ -0,0 +1,110 @@ +package org.example.domain; + +import io.ebean.annotation.DbArray; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import java.util.ArrayList; +import java.util.List; + +/** + * Contact entity bean. + */ +@Entity +@Table(name="be_contact") +public class Contact extends BaseModel { + + @DbArray + List phoneNumbers = new ArrayList(); + + @Column(length=50) + String firstName; + + @Column(length=50) + String lastName; + + @Column(length=200) + String email; + + @Column(length=20) + String phone; + + @ManyToOne(optional=false) + Customer customer; + + @OneToMany(mappedBy = "contact") + List notes; + + /** + * Default constructor. + */ + public Contact() { + } + + /** + * Construct with a firstName and lastName. + */ + public Contact(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public Customer getCustomer() { + return customer; + } + + public void setCustomer(Customer customer) { + this.customer = customer; + } + + public List getNotes() { + return notes; + } + + public void setNotes(List notes) { + this.notes = notes; + } + + public List getPhoneNumbers() { + return phoneNumbers; + } + + public void setPhoneNumbers(List phoneNumbers) { + this.phoneNumbers = phoneNumbers; + } +} diff --git a/ebean-querybean/src/test/java/org/example/domain/ContactNote.java b/ebean-querybean/src/test/java/org/example/domain/ContactNote.java new file mode 100644 index 000000000..9f684b81c --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/ContactNote.java @@ -0,0 +1,46 @@ +package org.example.domain; + +import io.ebean.Finder; + +import javax.persistence.Entity; +import javax.persistence.Lob; +import javax.persistence.ManyToOne; + +@Entity +public class ContactNote extends BaseModel { + + public static final Finder find = new Finder<>(ContactNote.class); + + @ManyToOne(optional = false) + Contact contact; + + String title; + + @Lob + String note; + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getNote() { + return note; + } + + public void setNote(String note) { + this.note = note; + } + + public Contact getContact() { + return contact; + } + + public void setContact(Contact contact) { + this.contact = contact; + } + +} diff --git a/ebean-querybean/src/test/java/org/example/domain/Country.java b/ebean-querybean/src/test/java/org/example/domain/Country.java new file mode 100644 index 000000000..30a826e00 --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/Country.java @@ -0,0 +1,59 @@ +package org.example.domain; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +/** + * Country entity bean. + */ +@Entity +@Table(name="o_country") +public class Country { + + @Id + //@Size(max=2) + String code; + + //@Size(max=60) + String name; + + public Country(String code, String name) { + this.code = code; + this.name = name; + } + + public String toString() { + return code; + } + + /** + * Return code. + */ + public String getCode() { + return code; + } + + /** + * Set code. + */ + public void setCode(String code) { + this.code = code; + } + + /** + * Return name. + */ + public String getName() { + return name; + } + + /** + * Set name. + */ + public void setName(String name) { + this.name = name; + } + + +} diff --git a/ebean-querybean/src/test/java/org/example/domain/Customer.java b/ebean-querybean/src/test/java/org/example/domain/Customer.java new file mode 100644 index 000000000..36d3fa840 --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/Customer.java @@ -0,0 +1,172 @@ +package org.example.domain; + +import io.ebean.annotation.Cache; +import io.ebean.annotation.EnumValue; +import io.ebean.types.Inet; +import org.example.domain.finder.CustomerFinder; +import org.example.domain.otherpackage.PhoneNumber; +import org.example.domain.otherpackage.ValidEmail; + +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Customer entity bean. + */ +@Cache +@Entity +@Table(name = "be_customer") +public class Customer extends BaseModel { + + /** + * Convenience Finder for 'active record' style. + */ + public static final CustomerFinder find = new CustomerFinder(); + + public enum Status { + @EnumValue("G") + GOOD, + + @EnumValue("B") + BAD, + + @EnumValue("M") + MIDDLING + } + + Status status; + + boolean inactive; + + PhoneNumber phoneNumber; + + ValidEmail email; + + @Column(length = 100) + String name; + + Date registered; + + Inet currentInet; + + @Column(length = 1000) + String comments; + + @ManyToOne(cascade = CascadeType.ALL) + Address billingAddress; + + @ManyToOne(cascade = CascadeType.ALL) + Address shippingAddress; + + @OneToMany(mappedBy = "customer", cascade = CascadeType.PERSIST) + List contacts; + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + public boolean isInactive() { + return inactive; + } + + public void setInactive(boolean inactive) { + this.inactive = inactive; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getRegistered() { + return registered; + } + + public void setRegistered(Date registered) { + this.registered = registered; + } + + public String getComments() { + return comments; + } + + public void setComments(String comments) { + this.comments = comments; + } + + public Inet getCurrentInet() { + return currentInet; + } + + public void setCurrentInet(Inet currentInet) { + this.currentInet = currentInet; + } + + public Address getBillingAddress() { + return billingAddress; + } + + public void setBillingAddress(Address billingAddress) { + this.billingAddress = billingAddress; + } + + public Address getShippingAddress() { + return shippingAddress; + } + + public void setShippingAddress(Address shippingAddress) { + this.shippingAddress = shippingAddress; + } + + public List getContacts() { + return contacts; + } + + public void setContacts(List contacts) { + this.contacts = contacts; + } + + public PhoneNumber getPhoneNumber() { + return phoneNumber; + } + + public void setPhoneNumber(final PhoneNumber phoneNumber) { + this.phoneNumber = phoneNumber; + } + + public ValidEmail getEmail() { + return email; + } + + public void setEmail(final ValidEmail email) { + this.email = email; + } + + /** + * Helper method to add a contact to the customer. + */ + public void addContact(Contact contact) { + if (contacts == null) { + contacts = new ArrayList<>(); + } + // setting the customer is automatically done when Ebean does + // a cascade save from customer to contacts. + contact.setCustomer(this); + contacts.add(contact); + } + +} diff --git a/ebean-querybean/src/test/java/org/example/domain/Order.java b/ebean-querybean/src/test/java/org/example/domain/Order.java new file mode 100644 index 000000000..7b63725fe --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/Order.java @@ -0,0 +1,137 @@ +package org.example.domain; + +import io.ebean.Finder; +import io.ebean.Model; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; +import javax.persistence.OrderBy; +import javax.persistence.Table; +import javax.validation.constraints.NotNull; +import java.sql.Date; +import java.util.ArrayList; +import java.util.List; + +/** + * Order entity bean. + */ +@Entity +@Table(name = "o_order") +public class Order extends BaseModel { + + public static final Finder find = new Finder<>(Order.class); + + public enum Status { + NEW, APPROVED, SHIPPED, COMPLETE + } + + Status status; + + Date orderDate; + + Date shipDate; + + @NotNull + @ManyToOne + Customer customer; + + @ManyToOne + Address shippingAddress; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "order") + @OrderBy("id asc") + List details; + + public String toString() { + return id + " status:" + status + " customer:" + customer; + } + + /** + * Return order date. + */ + public Date getOrderDate() { + return orderDate; + } + + /** + * Set order date. + */ + public void setOrderDate(Date orderDate) { + this.orderDate = orderDate; + } + + /** + * Return ship date. + */ + public Date getShipDate() { + return shipDate; + } + + /** + * Set ship date. + */ + public void setShipDate(Date shipDate) { + this.shipDate = shipDate; + } + + /** + * Return status. + */ + public Status getStatus() { + return status; + } + + /** + * Set status. + */ + public void setStatus(Status status) { + this.status = status; + } + + /** + * Return customer. + */ + public Customer getCustomer() { + return customer; + } + + /** + * Set customer. + */ + public void setCustomer(Customer customer) { + this.customer = customer; + } + + /** + * Set the customer with their current shipping address. + */ + public void setCustomerWithShipping(Customer customer) { + this.customer = customer; + this.shippingAddress = customer.getShippingAddress(); + } + + /** + * Return details. + */ + public List getDetails() { + return details; + } + + /** + * Set details. + */ + public void setDetails(List details) { + this.details = details; + } + + public void addDetail(OrderDetail detail) { + + if (details == null) { + details = new ArrayList<>(); + } + details.add(detail); + } + +} diff --git a/ebean-querybean/src/test/java/org/example/domain/OrderDetail.java b/ebean-querybean/src/test/java/org/example/domain/OrderDetail.java new file mode 100644 index 000000000..d16682b18 --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/OrderDetail.java @@ -0,0 +1,99 @@ +package org.example.domain; + +import javax.persistence.Entity; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +/** + * Order Detail entity bean. + */ +@Entity +@Table(name = "o_order_detail") +public class OrderDetail extends BaseModel { + + @ManyToOne + Order order; + + Integer orderQty; + + Integer shipQty; + + Double unitPrice; + + @ManyToOne + Product product; + + public OrderDetail() { + } + + public OrderDetail(Product product, Integer orderQty, Double unitPrice) { + this.product = product; + this.orderQty = orderQty; + this.unitPrice = unitPrice; + } + + /** + * Return order qty. + */ + public Integer getOrderQty() { + return orderQty; + } + + /** + * Set order qty. + */ + public void setOrderQty(Integer orderQty) { + this.orderQty = orderQty; + } + + /** + * Return ship qty. + */ + public Integer getShipQty() { + return shipQty; + } + + /** + * Set ship qty. + */ + public void setShipQty(Integer shipQty) { + this.shipQty = shipQty; + } + + public Double getUnitPrice() { + return unitPrice; + } + + public void setUnitPrice(Double unitPrice) { + this.unitPrice = unitPrice; + } + + /** + * Return order. + */ + public Order getOrder() { + return order; + } + + /** + * Set order. + */ + public void setOrder(Order order) { + this.order = order; + } + + /** + * Return product. + */ + public Product getProduct() { + return product; + } + + /** + * Set product. + */ + public void setProduct(Product product) { + this.product = product; + } + +} diff --git a/ebean-querybean/src/test/java/org/example/domain/Product.java b/ebean-querybean/src/test/java/org/example/domain/Product.java new file mode 100644 index 000000000..e41d4fc34 --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/Product.java @@ -0,0 +1,47 @@ +package org.example.domain; + +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.validation.constraints.Size; + +/** + * Product entity bean. + */ +@Entity +@Table(name = "o_product") +public class Product extends BaseModel { + + @Size(max = 20) + String sku; + + String name; + + /** + * Return sku. + */ + public String getSku() { + return sku; + } + + /** + * Set sku. + */ + public void setSku(String sku) { + this.sku = sku; + } + + /** + * Return name. + */ + public String getName() { + return name; + } + + /** + * Set name. + */ + public void setName(String name) { + this.name = name; + } + +} diff --git a/ebean-querybean/src/test/java/org/example/domain/SomePojo.java b/ebean-querybean/src/test/java/org/example/domain/SomePojo.java new file mode 100644 index 000000000..a063ab105 --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/SomePojo.java @@ -0,0 +1,19 @@ +package org.example.domain; + +import java.util.ArrayList; + +public class SomePojo { + + String name; + + ArrayList foos = new ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/ebean-querybean/src/test/java/org/example/domain/finder/CustomerFinder.java b/ebean-querybean/src/test/java/org/example/domain/finder/CustomerFinder.java new file mode 100644 index 000000000..cd35b0788 --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/finder/CustomerFinder.java @@ -0,0 +1,18 @@ +package org.example.domain.finder; + +import io.ebean.Finder; +import org.example.domain.Customer; +import org.example.domain.query.QCustomer; + +/** + */ +public class CustomerFinder extends Finder { + + public CustomerFinder() { + super(Customer.class); + } + + public QCustomer typed() { + return new QCustomer(); + } +} diff --git a/ebean-querybean/src/test/java/org/example/domain/otherpackage/Email.java b/ebean-querybean/src/test/java/org/example/domain/otherpackage/Email.java new file mode 100644 index 000000000..89ac06187 --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/otherpackage/Email.java @@ -0,0 +1,4 @@ +package org.example.domain.otherpackage; + +public interface Email extends Comparable { +} diff --git a/ebean-querybean/src/test/java/org/example/domain/otherpackage/PhoneAttributeConverter.java b/ebean-querybean/src/test/java/org/example/domain/otherpackage/PhoneAttributeConverter.java new file mode 100644 index 000000000..9500b9924 --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/otherpackage/PhoneAttributeConverter.java @@ -0,0 +1,17 @@ +package org.example.domain.otherpackage; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +@Converter +public class PhoneAttributeConverter implements AttributeConverter { + @Override + public String convertToDatabaseColumn(final PhoneNumber attribute) { + return attribute.getMsisdn(); + } + + @Override + public PhoneNumber convertToEntityAttribute(final String dbData) { + return new PhoneNumber(dbData); + } +} diff --git a/ebean-querybean/src/test/java/org/example/domain/otherpackage/PhoneNumber.java b/ebean-querybean/src/test/java/org/example/domain/otherpackage/PhoneNumber.java new file mode 100644 index 000000000..559285e81 --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/otherpackage/PhoneNumber.java @@ -0,0 +1,13 @@ +package org.example.domain.otherpackage; + +public class PhoneNumber { + private final String msisdn; + + public PhoneNumber(final String msisdn) { + this.msisdn = msisdn; + } + + public String getMsisdn() { + return msisdn; + } +} diff --git a/ebean-querybean/src/test/java/org/example/domain/otherpackage/ValidEmail.java b/ebean-querybean/src/test/java/org/example/domain/otherpackage/ValidEmail.java new file mode 100644 index 000000000..dc5a78bed --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/otherpackage/ValidEmail.java @@ -0,0 +1,18 @@ +package org.example.domain.otherpackage; + +public class ValidEmail implements Email { + private final String emailAddress; + + public ValidEmail(final String emailAddress) { + this.emailAddress = emailAddress; + } + + public String getEmailAddress() { + return emailAddress; + } + + @Override + public int compareTo(final ValidEmail o) { + return emailAddress.compareTo(o.emailAddress); + } +} diff --git a/ebean-querybean/src/test/java/org/example/domain/otherpackage/ValidEmailAttributeConverter.java b/ebean-querybean/src/test/java/org/example/domain/otherpackage/ValidEmailAttributeConverter.java new file mode 100644 index 000000000..6864366ff --- /dev/null +++ b/ebean-querybean/src/test/java/org/example/domain/otherpackage/ValidEmailAttributeConverter.java @@ -0,0 +1,17 @@ +package org.example.domain.otherpackage; + +import javax.persistence.AttributeConverter; +import javax.persistence.Converter; + +@Converter +public class ValidEmailAttributeConverter implements AttributeConverter { + @Override + public String convertToDatabaseColumn(final ValidEmail attribute) { + return attribute.getEmailAddress(); + } + + @Override + public ValidEmail convertToEntityAttribute(final String dbData) { + return new ValidEmail(dbData); + } +} diff --git a/ebean-querybean/src/test/java/org/querytest/QCustomerAndOrTest.java b/ebean-querybean/src/test/java/org/querytest/QCustomerAndOrTest.java new file mode 100644 index 000000000..231f4ae8d --- /dev/null +++ b/ebean-querybean/src/test/java/org/querytest/QCustomerAndOrTest.java @@ -0,0 +1,62 @@ +package org.querytest; + +import org.example.domain.Customer; +import org.junit.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.List; + +public class QCustomerAndOrTest { + + @Test + public void testEndAnd() { + + Date fiveDays = fiveDaysAgo(); + + Customer.find.typed() + .status.equalTo(Customer.Status.GOOD) + .or() + .id.greaterThan(1000) + .and() + .name.startsWith("super") + .registered.after(fiveDays) + .endAnd() + .orderBy().id.desc() + .findList(); + } + + @Test + public void testQuery() { + + Date fiveDays = fiveDaysAgo(); + + Customer.find.typed().name.like("DoesNotExist").delete(); + + List customers = + + Customer.find.typed() + .status.equalTo(Customer.Status.GOOD) + .or() + .id.greaterThan(1000) + .and() + .name.startsWith("super") + .registered.after(fiveDays) + .endJunction() + .orderBy().id.desc() + .findList(); + +// where t0.status = ? and (t0.id > ? or (t0.name like ? and t0.registered > ? ) ) order by t0.id desc; --bind(GOOD,1000,super%,Wed Jul 22 00:00:00 NZST 2015) +// where t0.status = ? and (t0.id > ? or (t0.name like ? and t0.registered > ? ) ) order by t0.id; --bind(GOOD,1000,super%,Wed Jul 22 00:00:00 NZST 2015) +// //where t0.id > ? and (t0.id < ? or (t0.name like ? and t0.name like ? ) ) order by t0.id; --bind(12,1234,one,two) +// + + } + + private Date fiveDaysAgo() { + LocalDateTime fiveDaysAgo = LocalDate.now().atStartOfDay().minusDays(5); + return new Date(fiveDaysAgo.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); + } +} diff --git a/ebean-querybean/src/test/java/org/querytest/QCustomerSimpleLikeQueryTest.java b/ebean-querybean/src/test/java/org/querytest/QCustomerSimpleLikeQueryTest.java new file mode 100644 index 000000000..182e2b737 --- /dev/null +++ b/ebean-querybean/src/test/java/org/querytest/QCustomerSimpleLikeQueryTest.java @@ -0,0 +1,37 @@ +package org.querytest; + +import org.example.domain.Customer; +import org.example.domain.query.QCustomer; +import org.junit.Test; + +import java.util.Date; +import java.util.List; + +public class QCustomerSimpleLikeQueryTest { + + @Test + public void testQuery() { + + List customers = + new QCustomer() + .name.ilike("rob") + .status.equalTo(Customer.Status.GOOD) + .registered.after(new Date()) + .contacts.email.endsWith("@foo.com") + .orderBy() + .name.asc() + .registered.desc() + .findList(); + + //where lower(t0.name) like ? and t0.status = ? and t0.registered > ? and u1.email like ? order by t0.name, t0.registered desc; --bind(rob,GOOD,Mon Jul 27 12:05:37 NZST 2015,%@foo.com) + } + + @Test + public void testFindEach() { + + new QCustomer() + .status.equalTo(Customer.Status.GOOD) + .orderBy().id.asc() + .findEach(customer -> System.out.println("-- visit " + customer)); + } +} diff --git a/ebean-querybean/src/test/java/org/querytest/QCustomerTest.java b/ebean-querybean/src/test/java/org/querytest/QCustomerTest.java new file mode 100644 index 000000000..ee571c126 --- /dev/null +++ b/ebean-querybean/src/test/java/org/querytest/QCustomerTest.java @@ -0,0 +1,842 @@ +package org.querytest; + +import io.ebean.DB; +import io.ebean.Database; +import io.ebean.PagedList; +import io.ebean.Query; +import io.ebean.QueryIterator; +import io.ebean.Transaction; +import io.ebean.annotation.Transactional; +import io.ebean.types.Inet; +import org.example.domain.ACat; +import org.example.domain.ADog; +import org.example.domain.Address; +import org.example.domain.Animal; +import org.example.domain.Country; +import org.example.domain.Customer; +import org.example.domain.otherpackage.PhoneNumber; +import org.example.domain.otherpackage.ValidEmail; +import org.example.domain.query.QAnimal; +import org.example.domain.query.QContact; +import org.example.domain.query.QCustomer; +import org.junit.Assert; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.StringJoiner; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.example.domain.query.QAddress.Alias.country; +import static org.example.domain.query.QAddress.Alias.line1; +import static org.example.domain.query.QContact.Alias.lastName; +import static org.example.domain.query.QCustomer.Alias.billingAddress; + +public class QCustomerTest { + + @Rule + public final TestName testName = new TestName(); + + @Test + public void findWithTransaction() { + + final Database database = DB.getDefault(); + + try (Transaction txn = database.createTransaction()) { + Customer customer = new Customer(); + customer.setName("explicitTransaction"); + + database.save(customer, txn); + + final Customer found = new QCustomer(txn) + .name.eq("explicitTransaction") + .findOne(); + assertThat(found).isNotNull(); + + // not found using other transaction + final Customer foundNot = new QCustomer() + .name.eq("explicitTransaction") + .findOne(); + assertThat(foundNot).isNull(); + + txn.commit(); + } + + } + + @Test + public void findSingleAttribute() { + + List names = new QCustomer() + .setDistinct(true) + .select(QCustomer.alias().name) + .status.equalTo(Customer.Status.BAD) + .findSingleAttributeList(); + + assertThat(names).isNotNull(); + } + + @Test + public void findIterate() { + + Customer cust = new Customer(); + cust.setName("foo"); + cust.setStatus(Customer.Status.GOOD); + cust.save(); + + List ids = new QCustomer() + .status.equalTo(Customer.Status.GOOD) + .findIds(); + + assertThat(ids).isNotEmpty(); + + + Map map = new QCustomer() + .status.equalTo(Customer.Status.GOOD) + .findMap(); + + assertThat(map.size()).isEqualTo(ids.size()); + + QueryIterator iterate = new QCustomer() + .status.equalTo(Customer.Status.GOOD) + .findIterate(); + + try { + while (iterate.hasNext()) { + Customer customer = iterate.next(); + assertThat(customer.getName()).isNotNull(); + } + } finally { + iterate.close(); + } + } + + + @Test + public void isEmpty() { + + new QCustomer() + .contacts.isEmpty() + .findList(); + + new QCustomer() + .contacts.isNotEmpty() + .findList(); + } + + @Transactional + @Test + public void forUpdate() { + + new QCustomer() + .id.eq(42) + .forUpdate() + .findOne(); + + new QCustomer() + .id.eq(42) + .forUpdateNoWait() + .findOne(); + + new QCustomer() + .id.eq(42) + .forUpdateSkipLocked() + .findOne(); + } + + + @Ignore + @Test + public void arrayContains() { + + new QContact() + .phoneNumbers.contains("4312") + .findList(); + + new QCustomer() + .contacts.phoneNumbers.contains("4312") + .findList(); + } + + @Test + public void setIncludeSoftDeletes() { + + new QCustomer() + .setIdIn(42L) + .setIncludeSoftDeletes() + .findList(); + } + + @Test + public void testIdIn() { + + new QCustomer() + .setIdIn("1", "2") + .findList(); + + new QCustomer() + .id.in(1L, 2L, 3L) + .findList(); + } + + @Test + public void testIn() { + new QCustomer() + .id.in(34L, 33L) + .name.in("asd", "foo", "bar") + .registered.in(new Date()) + .findList(); + } + + @Test + public void usingTransaction() { + + try (Transaction transaction = DB.getDefault().createTransaction()) { + + new QCustomer() + .registered.isNull() + .usingTransaction(transaction) + .findList(); + } + } + + @Test + public void usingConnection() throws SQLException { + + Customer cust = new Customer(); + cust.setName("usingConnection"); + cust.setStatus(Customer.Status.GOOD); + cust.save(); + + DataSource dataSource = DB.getDefault().getPluginApi().getDataSource(); + + try (Connection connection = dataSource.getConnection()) { + + List foo = new QCustomer() + .name.eq("usingConnection") + .usingConnection(connection) + .findList(); + + assertThat(foo).hasSize(1); + } + } + + @Test + public void testAssocOne() { + + Address address = new Address(); + address.setId(41L); + + new QCustomer() + .billingAddress.eq(address) + .findList(); + + new QCustomer() + .billingAddress.equalTo(address) + .findList(); + + new QCustomer() + .billingAddress.ne(address) + .findList(); + + new QCustomer() + .billingAddress.notEqualTo(address) + .findList(); + } + + @Test + public void testInOrEmpty() { + + List names = Arrays.asList("asd", "foo", "bar"); + + new QCustomer() + .registered.before(new Date()) + .name.inOrEmpty(names) + .findList(); + + new QCustomer() + .registered.before(new Date()) + .name.inOrEmpty(null) + .findList(); + + names = Collections.emptyList(); + + new QCustomer() + .registered.before(new Date()) + .name.inOrEmpty(names) + .findList(); + } + + @Test + public void testNotIn() { + new QCustomer() + .id.isIn(34L, 33L) + .name.notIn("asd", "foo", "bar") + .registered.in(new Date()) + .findList(); + } + + @Test + public void testQueryBoolean() { + + new QCustomer() + .name.contains("rob") + //.setUseDocStore(true) + .setMaxRows(10) + .findPagedList(); + + new QCustomer() + .inactive.isFalse() + .findList(); + } + + @Test + public void testFindOne() { + + new QCustomer() + .name.isIn("rob", "foo") + //.setUseDocStore(true) + .setMaxRows(1) + .findOne(); + + Optional maybe = new QCustomer() + .name.contains("rob") + //.setUseDocStore(true) + .setMaxRows(1) + .findOneOrEmpty(); + + maybe.isPresent(); + + new QCustomer() + .inactive.isFalse() + .findList(); + } + + private void insertCustomer(String name) { + Customer cust = new Customer(); + cust.setName(name); + cust.setStatus(Customer.Status.GOOD); + cust.save(); + } + + @Test + public void testFindStream() { + insertCustomer("stream1"); + insertCustomer("stream2"); + + StringJoiner sb = new StringJoiner("|"); + try (Stream stream = new QCustomer() + .name.startsWith("stream") + .id.asc() + .findStream()) { + + stream.forEach(it -> sb.add(it.getName())); + } + + assertThat(sb.toString()).isEqualTo("stream1|stream2"); + } + + @Test + public void testFindLargeStream() { + insertCustomer("largeStream1"); + insertCustomer("largeStream2"); + insertCustomer("largeStream3"); + + StringJoiner sb = new StringJoiner("|"); + try (Stream stream = new QCustomer() + .name.startsWith("largeStream") + .id.asc() + .findLargeStream()) { + + stream.forEach(it -> sb.add(it.getName())); + } + + assertThat(sb.toString()).isEqualTo("largeStream1|largeStream2|largeStream3"); + } + + @Test + public void testFilterMany() { + + Customer cust = new Customer(); + cust.setName("Postgres Foo"); + cust.setStatus(Customer.Status.GOOD); + cust.save(); + + new QCustomer() + .name.startsWith("Postgres") + .contacts.filterMany("firstName istartsWith ?", "Rob") + .findList(); + + final LocalDate startDate = LocalDate.now().minusDays(7); + final LocalDate endDate = LocalDate.now(); + + new QCustomer() + .name.startsWith("Postgres") + .contacts.filterMany("whenCreated inRange ? to ?", startDate, endDate) + .findList(); + } + + @Test + public void testDate_lessThan() { + + assertContains(new QCustomer().registered.lt(new Date()).query(), " where t0.registered < ?"); + assertContains(new QCustomer().registered.before(new Date()).query(), " where t0.registered < ?"); + assertContains(new QCustomer().registered.lessThan(new Date()).query(), " where t0.registered < ?"); + } + + @Test + public void testDate_lessOrEqualTo() { + + assertContains(new QCustomer().registered.le(new Date()).query(), " where t0.registered <= ?"); + assertContains(new QCustomer().registered.lessOrEqualTo(new Date()).query(), " where t0.registered <= ?"); + } + + @Test + public void testDate_greaterThan() { + + assertContains(new QCustomer().registered.after(new Date()).query(), " where t0.registered > ?"); + assertContains(new QCustomer().registered.gt(new Date()).query(), " where t0.registered > ?"); + assertContains(new QCustomer().registered.greaterThan(new Date()).query(), " where t0.registered > ?"); + } + + + @Test + public void testDate_greaterOrEqualTo() { + + assertContains(new QCustomer().registered.ge(new Date()).query(), " where t0.registered >= ?"); + assertContains(new QCustomer().registered.greaterOrEqualTo(new Date()).query(), " where t0.registered >= ?"); + } + + private void assertContains(Query query, String match) { + query.findList(); + assertThat(query.getGeneratedSql()).contains(match); + } + + @Test + public void query_setAllowLoadErrors() { + + new QCustomer() + .status.in(Customer.Status.GOOD) + .setAllowLoadErrors() + .findList(); + } + + @Test + public void query_setInheritType() { + + ACat cat = new ACat("C1"); + cat.save(); + + ACat cat2 = new ACat("C2"); + cat2.save(); + + ADog dog = new ADog("D1", "D878"); + dog.save(); + + List animals = new QAnimal() + .id.greaterOrEqualTo(1L) + .setInheritType(ACat.class) + .findList(); + + System.out.println(animals); + } + + @Test + public void select_assocManyToOne() { + + Country nz = new Country("NZ", "New Zealand"); + DB.merge(nz); + + Address address = new Address(); + address.setLine1("42 below"); + address.setCountry(nz); + + Customer customer0 = new Customer(); + customer0.setName("asdBilling0"); + customer0.setBillingAddress(address); + customer0.save(); + + Customer customer1 = new Customer(); + customer1.setName("asdBilling1"); + customer1.setBillingAddress(new Address()); + customer1.save(); + + List billingAddressIds + = new QCustomer() + .select(billingAddress) + .name.startsWith("asdBilling") + .findSingleAttributeList(); + + assertThat(billingAddressIds).hasSize(2); + + + Map map + = new QCustomer() + .billingAddress.id.asMapKey() + .name.startsWith("asdBilling") + .findMap(); + + assertThat(map).hasSize(2); + + + List customers = new QCustomer() + .billingAddress.fetch(line1, country) + .findList(); + + assertThat(customers).isNotEmpty(); + + } + + @Test + public void query_fetchString() { + + Customer cust = new Customer(); + cust.setName("baz"); + cust.setCurrentInet(new Inet("129.1.1.4")); + cust.setStatus(Customer.Status.GOOD); + cust.save(); + + new QCustomer() + .currentInet.eq(Inet.of("129.1.1.4")) + .findList(); + + new QCustomer() + .currentInet.in(Inet.setOf("129.1.1.4","129.1.1.5")) + .findList(); + + new QCustomer() + .contacts.fetch("email") + .orderBy() + .name.asc() + .contacts.email.asc() + .findList(); + + new QCustomer() + .contacts.fetchQuery("email") + .orderBy() + .name.asc() + .contacts.email.asc() + .findList(); + } + + @Test + public void query_setBaseTable() { + + new QCustomer() + .setBaseTable("BE_CUSTOMER") + .findList(); + } + + @Test + public void query_rawOrEmpty() { + + List names = Arrays.asList("A", "B"); + + new QCustomer() + .rawOrEmpty("name in (?1)", names) + .findList(); + + new QCustomer() + .rawOrEmpty("name in (?1)", null) + .findList(); + + new QCustomer() + .rawOrEmpty("name in (?1)", new ArrayList()) + .findList(); + } + + @Test + public void query_orNull() { + + new QCustomer() + .name.equalToOrNull("A") + .findList(); + + new QCustomer() + .name.eqOrNull("A") + .findList(); + + new QCustomer() + .name.greaterThanOrNull("B") + .findList(); + + new QCustomer() + .name.lessThanOrNull("C") + .findList(); + + new QCustomer() + .name.gtOrNull("GT1") + .name.geOrNull("GE1") + .name.ltOrNull("LT1") + .name.leOrNull("LE1") + .findList(); + } + + @Test + public void query_inRange() { + + new QCustomer() + .name.inRange("A", "B") + .findList(); + + new QCustomer() + .name.greaterOrEqualTo("A") + .name.lessThan("B") + .findList(); + + new QContact() + .firstName.inRangeWith(lastName, "B") + .findList(); + } + + @Test + public void query_exists() { + + boolean customerExists = + new QCustomer() + .name.equalTo("DoesNotExistReally") + .exists(); + + assertThat(customerExists).isFalse(); + } + + @Test + public void testQuery() { + + QContact contact = QContact.alias(); + QCustomer cust = QCustomer.alias(); + + new QCustomer() + // tune query + .select(cust.name) + .status.isIn(Customer.Status.BAD, Customer.Status.BAD) + .contacts.fetch() + // predicates + .findList(); + + new QCustomer() + // tune query + .select(cust.name) + .contacts.fetch() + // predicates + .findList(); + + new QCustomer() + // tune query + .select(cust.id, cust.name) + .contacts.fetch(contact.firstName, contact.lastName, contact.email) + // predicates + .id.greaterThan(1) + .findList(); + + PagedList pagedList = new QCustomer() + // tune query + .select(cust.id, cust.name) + .contacts.fetch(contact.firstName, contact.lastName, contact.email) + // predicates + .id.greaterThan(1) + .setFirstRow(20) + .setMaxRows(10) + .findPagedList(); + + pagedList.getList(); + pagedList.getList(); + +// new QCustomer() +// .asDraft() +// .findList(); +// +// new QCustomer() +// .includeSoftDeletes() +// .findList(); + +// List contacts +// = new QContact() +// .email.like("asd") +// .notes.title.like("asd") +// .orderBy() +// .id.desc() +// .findList(); +// + +// List customers = new QCustomer() +// .id.eq(1234) +// .status.equalTo(Customer.Status.BAD) +// .status.in(Customer.Status.GOOD, Customer.Status.MIDDLING) +// //.status.eq(Order.Status.APPROVED) +// .name.like("asd") +// .name.istartsWith("ASdf") +// .registered.after(new Date()) +// .contacts.email.endsWith("@foo.com") +// .contacts.notes.id.greaterThan(123L) +// .orderBy().id.asc() +// .findList(); + +// //Customer customer3 = +// new QCustomer() +// .id.gt(12) +// .or() +// .id.lt(1234) +// .and() +// .name.like("one") +// .name.like("two") +// .endAnd() +// .endOr() +// .orderBy().id.asc() +// .findList(); +// +// //where t0.id > ? and (t0.id < ? or (t0.name like ? and t0.name like ? ) ) order by t0.id; --bind(12,1234,one,two) +// +//// List customers +//// = new QCustomer() +//// .name.like("asd") +//// .findList(); +// +// Customer.find.where() +// .gt("id", 1234) +// .disjunction().eq("id", 1234).like("name", "asd") +// .endJunction().findList(); + +// QCustomer cust = QCustomer.I; +// ExpressionList expr = new QCustomer().expr(); +// expr.eq(cust.contacts.email, 123); + } + + @Test + public void testFindSet() { + + Set customerSet = new QCustomer().findSet(); + Assert.assertNotNull(customerSet); + } + + @Test + public void testFindMap() { + + Customer cust = new Customer(); + cust.setName("banana"); + cust.setStatus(Customer.Status.GOOD); + cust.save(); + + Map map = new QCustomer() + .id.greaterOrEqualTo(1L) + .name.asMapKey() + .findMap(); + + assertThat(map.get("banana")).isNotNull(); + + } + + @Test + public void testAsUpdate() { + + int rows = new QContact() + .notes.note.startsWith("Make Inactive") + .email.endsWith("@foo.com") + .asUpdate() + .setRaw("email = lower(email)") + //.set("inactive", true) + .update(); + + assertThat(rows).isEqualTo(0); + + } + + + @Test + public void testSelectFormula() { + + Customer cust = new Customer(); + cust.setName("junk junk"); + cust.setStatus(Customer.Status.GOOD); + cust.setRegistered(new Date()); + cust.save(); + + java.util.Date maxDate = new QCustomer() + .select("max(registered)") + .findSingleAttribute(); + + assertThat(maxDate).isNotNull(); + } + + + @Test + public void testFetchByScalarValue() { + Customer cust = new Customer(); + cust.setName(testName.getMethodName()); + cust.setPhoneNumber(new PhoneNumber("+18005555555")); + cust.save(); + assertThat(new QCustomer() + .name.eq(testName.getMethodName()) + .phoneNumber.eq(new PhoneNumber("+18005555555")) + .findOne()).isNotNull(); + } + + + @Test + public void testFetchByComparableScalarValue() { + Customer cust = new Customer(); + cust.setName(testName.getMethodName()); + cust.setEmail(new ValidEmail("foo2@example.org")); + cust.save(); + assertThat(new QCustomer() + .name.eq(testName.getMethodName()) + .email.eq(new ValidEmail("foo2@example.org")) + .findOne()).isNotNull(); + assertThat(new QCustomer() + .name.eq(testName.getMethodName()) + .email.gt(new ValidEmail("foo2@example.org")) + .findOne()).isNull(); + assertThat(new QCustomer() + .name.eq(testName.getMethodName()) + .email.gt(new ValidEmail("foo1@example.org")) + .findOne()).isNotNull(); + assertThat(new QCustomer() + .name.eq(testName.getMethodName()) + .email.greaterOrEqualTo(new ValidEmail("foo2@example.org")) + .findOne()).isNotNull(); + } + + + @Test + public void testFetchFormula() { + + Customer cust = new Customer(); + cust.setName("junk junk"); + cust.setStatus(Customer.Status.GOOD); + cust.setRegistered(new Date()); + cust.save(); + + List dtos = new QCustomer() + .select("id, name") + .asDto(C1dto.class) + .findList(); + + assertThat(dtos).isNotEmpty(); + } + + public static class C1dto { + + final long id; + final String name; + + public C1dto(long id, String name) { + this.id = id; + this.name = name; + } + } + +} diff --git a/ebean-querybean/src/test/java/org/querytest/QOrderTest.java b/ebean-querybean/src/test/java/org/querytest/QOrderTest.java new file mode 100644 index 000000000..ab98f0e67 --- /dev/null +++ b/ebean-querybean/src/test/java/org/querytest/QOrderTest.java @@ -0,0 +1,44 @@ +package org.querytest; + +import io.ebean.FetchGroup; +import org.example.domain.Order; +import org.example.domain.query.QCustomer; +import org.example.domain.query.QOrder; +import org.junit.Test; + +public class QOrderTest { + + private static final QCustomer cu = QCustomer.alias(); + + private static final QOrder or = QOrder.alias(); + + private static final FetchGroup fg = QOrder.forFetchGroup() + .select(or.status, or.shipDate) + .customer.fetchCache(cu.name, cu.status, cu.registered, cu.comments) + .buildFetchGroup(); + + @Test + public void fetchCache() { + + + new QOrder() + .status.eq(Order.Status.NEW) + .customer.fetchCache(cu.name, cu.registered) + .findList(); + + new QOrder() + .status.eq(Order.Status.NEW) + .customer.fetchCache() + .findList(); + } + + @Test + public void viaFetchGraph() { + + new QOrder() + .status.eq(Order.Status.NEW) + .select(fg) + .findList(); + } + +} diff --git a/ebean-querybean/src/test/resources/ebean.mf b/ebean-querybean/src/test/resources/ebean.mf new file mode 100644 index 000000000..5c97580de --- /dev/null +++ b/ebean-querybean/src/test/resources/ebean.mf @@ -0,0 +1,2 @@ +entity-packages: org.example.domain +querybean-packages: org.example.domain,org.querytest diff --git a/ebean-querybean/src/test/resources/ebean.properties b/ebean-querybean/src/test/resources/ebean.properties new file mode 100644 index 000000000..dd0ff13a3 --- /dev/null +++ b/ebean-querybean/src/test/resources/ebean.properties @@ -0,0 +1,12 @@ + +#load.properties.override=${home}/config/myapp.ebean.properties + +ebean.ddl.generate=true +ebean.ddl.run=true + +datasource.default=h2 + +datasource.h2.username=sa +datasource.h2.password= +datasource.h2.databaseUrl=jdbc:h2:mem:tests +datasource.h2.databaseDriver=org.h2.Driver diff --git a/ebean-querybean/src/test/resources/logback-test.xml b/ebean-querybean/src/test/resources/logback-test.xml new file mode 100644 index 000000000..1f100d5c0 --- /dev/null +++ b/ebean-querybean/src/test/resources/logback-test.xml @@ -0,0 +1,35 @@ + + + + TRACE + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/kotlin-querybean-generator/README.md b/kotlin-querybean-generator/README.md new file mode 100644 index 000000000..561a69c8d --- /dev/null +++ b/kotlin-querybean-generator/README.md @@ -0,0 +1,9 @@ +# kotlin-querybean-generator +Annotation processor for generating Kotlin query beans for type safe query construction. + +For Kotlin users these are preferred over Java query beans in that they use Kotlin properties +rather than Java public fields. This limits the query bean enhancement to just the beans themselves +where as with java query beans we need to enhance callers (as we are effectively simulating 'properties' +via java public fields and enhancement). + +Refer to the documentation at: https://ebean.io/docs/query/query-beans diff --git a/kotlin-querybean-generator/pom.xml b/kotlin-querybean-generator/pom.xml new file mode 100644 index 000000000..cb317da6b --- /dev/null +++ b/kotlin-querybean-generator/pom.xml @@ -0,0 +1,42 @@ + + + + ebean-parent + io.ebean + 12.4.3-SNAPSHOT + + 4.0.0 + + kotlin-querybean-generator + + + + + org.avaje.composite + composite-testing + 3.1 + test + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.2 + + 1.8 + 1.8 + + -proc:none + + + + + + diff --git a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/Append.java b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/Append.java new file mode 100644 index 000000000..b21cbc222 --- /dev/null +++ b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/Append.java @@ -0,0 +1,51 @@ +package io.ebean.querybean.generator; + +import java.io.IOException; +import java.io.Writer; + +/** + * Helper that wraps a writer with some useful methods to append content. + */ +class Append { + + private final Writer writer; + + Append(Writer writer) { + this.writer = writer; + } + + Append append(String content) { + try { + writer.append(content); + return this; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + void close() { + try { + writer.flush(); + writer.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + Append eol() { + try { + writer.append("\n"); + return this; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Append content with formatted arguments. + */ + Append append(String format, Object... args) { + return append(String.format(format, args)); + } + +} diff --git a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/Constants.java b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/Constants.java new file mode 100644 index 000000000..79827d405 --- /dev/null +++ b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/Constants.java @@ -0,0 +1,37 @@ +package io.ebean.querybean.generator; + +interface Constants { + + String AT_GENERATED = "@Generated(\"io.ebean.querybean.kotlin-generator\")"; + + String AT_TYPEQUERYBEAN = "@TypeQueryBean(\"v1\")"; + + String GENERATED_9 = "javax.annotation.processing.Generated"; + String GENERATED_8 = "javax.annotation.Generated"; + + String MAPPED_SUPERCLASS = "javax.persistence.MappedSuperclass"; + String INHERITANCE = "javax.persistence.Inheritance"; + String ENTITY = "javax.persistence.Entity"; + String EMBEDDABLE = "javax.persistence.Embeddable"; + String CONVERTER = "javax.persistence.Converter"; + String EBEAN_COMPONENT = "io.ebean.annotation.EbeanComponent"; + + String DBARRAY = "io.ebean.annotation.DbArray"; + String DBJSON = "io.ebean.annotation.DbJson"; + String DBJSONB = "io.ebean.annotation.DbJsonB"; + String DBNAME = "io.ebean.annotation.DbName"; + + String TQROOTBEAN = "io.ebean.typequery.TQRootBean"; + String TQASSOCBEAN = "io.ebean.typequery.TQAssocBean"; + String TQPROPERTY = "io.ebean.typequery.TQProperty"; + String TYPEQUERYBEAN = "io.ebean.typequery.TypeQueryBean"; + String DATABASE = "io.ebean.Database"; + String DB = "io.ebean.DB"; + String FETCHGROUP = "io.ebean.FetchGroup"; + String QUERY = "io.ebean.Query"; + String TRANSACTION = "io.ebean.Transaction"; + + String MODULEINFO = "io.ebean.config.ModuleInfo"; + String METAINF_MANIFEST = "META-INF/ebean-generated-info.mf"; + String METAINF_SERVICES_MODULELOADER = "META-INF/services/io.ebean.config.ModuleInfoLoader"; +} diff --git a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/FindDbName.java b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/FindDbName.java new file mode 100644 index 000000000..9fd013382 --- /dev/null +++ b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/FindDbName.java @@ -0,0 +1,55 @@ +package io.ebean.querybean.generator; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; +import java.util.List; +import java.util.Map; +import java.util.Set; + +class FindDbName { + + /** + * Return the value of the DbName annotation or null if it isn't found on the element. + */ + static String value(TypeElement element, Types typeUtils) { + + AnnotationMirror mirror = findDbNameMirror(element); + if (mirror != null) { + return readDbNameValue(mirror); + } + final TypeMirror typeMirror = element.getSuperclass(); + if (typeMirror.getKind() == TypeKind.NONE) { + return null; + } + final TypeElement element1 = (TypeElement)typeUtils.asElement(typeMirror); + return value(element1, typeUtils); + } + + private static String readDbNameValue(AnnotationMirror mirror) { + + final Map elementValues = mirror.getElementValues(); + final Set> entries = elementValues.entrySet(); + for (Map.Entry entry : entries) { + if ("value".equals(entry.getKey().getSimpleName().toString())) { + return (String) entry.getValue().getValue(); + } + } + return null; + } + + private static AnnotationMirror findDbNameMirror(TypeElement element) { + final List mirrors = element.getAnnotationMirrors(); + for (AnnotationMirror mirror : mirrors) { + final String name = mirror.getAnnotationType().asElement().toString(); + if (Constants.DBNAME.equals(name)) { + return mirror; + } + } + return null; + } +} diff --git a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/KotlinLangAdapter.java b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/KotlinLangAdapter.java new file mode 100644 index 000000000..03837a21a --- /dev/null +++ b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/KotlinLangAdapter.java @@ -0,0 +1,117 @@ +package io.ebean.querybean.generator; + +class KotlinLangAdapter implements LangAdapter { + + @Override + public void beginClass(Append writer, String shortName) { + writer.append("class Q%s : TQRootBean<%1$s, Q%1$s> {", shortName).eol(); + } + + @Override + public void beginAssocClass(Append writer, String shortName, String origShortName) { + writer.append("class Q%s : TQAssocBean<%s,R> {", shortName, origShortName).eol(); + } + + @Override + public void alias(Append writer, String shortName) { + + writer.append(" companion object {").eol(); + writer.append(" /**").eol(); + writer.append(" * shared 'Alias' instance used to provide").eol(); + writer.append(" * properties to select and fetch clauses").eol(); + writer.append(" */").eol(); + writer.append(" val _alias = Q").append(shortName).append("(true)").eol(); + writer.eol(); + writer.append(" /**").eol(); + writer.append(" * Return a query bean used to build a FetchGroup.").eol(); + writer.append(" */").eol(); + writer.append(" fun forFetchGroup(): Q%s {", shortName).eol(); + writer.append(" return Q%s(FetchGroup.queryFor(%s::class.java));", shortName, shortName).eol(); + writer.append(" }").eol(); + writer.append(" }").eol().eol(); + } + + @Override + public void assocBeanConstructor(Append writer, String shortName) { + + writer.append(" constructor(name: String, root: R) : super(name, root)").eol(); + writer.eol(); + writer.append(" constructor(name: String, root: R, prefix: String) : super(name, root, prefix)").eol(); + } + + @Override + public void fetch(Append writer, String origShortName) { + + writeAssocBeanFetch(writer, origShortName, "", "Eagerly fetch this association loading the specified properties."); + writeAssocBeanFetch(writer, origShortName, "Query", "Eagerly fetch this association using a 'query join' loading the specified properties."); + writeAssocBeanFetch(writer, origShortName, "Cache", "Eagerly fetch this association using L2 cache."); + writeAssocBeanFetch(writer, origShortName, "Lazy", "Use lazy loading for this association loading the specified properties."); + } + + private void writeAssocBeanFetch(Append writer, String origShortName, String fetchType, String comment) { + +// fun fetch(vararg properties: TQProperty): R { +// return fetchProperties(*properties) +// } + + writer.append(" /**").eol(); + writer.append(" * ").append(comment).eol(); + writer.append(" */").eol(); + writer.append(" fun fetch%s(vararg properties: TQProperty) : R {", fetchType, origShortName).eol(); + writer.append(" return fetch%sProperties(*properties)", fetchType).eol(); + writer.append(" }").eol(); + writer.eol(); + } + + + @Override + public void rootBeanConstructor(Append writer, String shortName, String dbName) { + + String name = (dbName == null) ? "default" : dbName; + + writer.append(" /**").eol(); + writer.append(" * Construct using the %s Database.", name).eol(); + writer.append(" */").eol(); + if (dbName == null) { + writer.append(" constructor() : super(%s::class.java)", shortName).eol().eol(); + } else { + writer.append(" constructor() : super(%s::class.java, DB.byName(\"%s\"))", shortName, dbName).eol().eol(); + } + + writer.append(" /**").eol(); + writer.append(" * Construct with a given Transaction.", name).eol(); + writer.append(" */").eol(); + if (dbName == null) { + writer.append(" constructor(transaction: Transaction) : super(%s::class.java, transaction)", shortName).eol().eol(); + } else { + writer.append(" constructor(transaction: Transaction) : super(%s::class.java, DB.byName(\"%s\"), transaction)", shortName, dbName).eol().eol(); + } + + writer.eol(); + writer.append(" /**").eol(); + writer.append(" * Construct with a given Database.").eol(); + writer.append(" */").eol(); + writer.append(" constructor(database: Database) : super(%s::class.java, database)", shortName).eol().eol(); + + writer.append(" /**").eol(); + writer.append(" * Construct for Alias.").eol(); + writer.append(" */").eol(); + writer.append(" private constructor(dummy: Boolean) : super(dummy)").eol().eol(); + + writer.append(" /**").eol(); + writer.append(" * Private constructor for FetchGroup building.").eol(); + writer.append(" */").eol(); + writer.append(" private constructor(fetchGroupQuery: Query<%s>) : super(fetchGroupQuery)", shortName).eol(); + } + + @Override + public void fieldDefn(Append writer, String propertyName, String typeDefn) { + + writer.append(" lateinit var %s: ", propertyName); + if (typeDefn.endsWith(",Integer>")) { + typeDefn = typeDefn.replace(",Integer>", ",Int>"); + } + writer.append(typeDefn); + } + +} diff --git a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/LangAdapter.java b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/LangAdapter.java new file mode 100644 index 000000000..3c7dba065 --- /dev/null +++ b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/LangAdapter.java @@ -0,0 +1,19 @@ +package io.ebean.querybean.generator; + +public interface LangAdapter { + + void beginClass(Append writer, String shortName); + + void beginAssocClass(Append writer, String shortName, String origShortName); + + void alias(Append writer, String shortName); + + void rootBeanConstructor(Append writer, String shortName, String dbName); + + void assocBeanConstructor(Append writer, String shortName); + + void fetch(Append writer, String origShortName); + + void fieldDefn(Append writer, String propertyName, String typeDefn); + +} diff --git a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/ModuleMeta.java b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/ModuleMeta.java new file mode 100644 index 000000000..a1e98f3a1 --- /dev/null +++ b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/ModuleMeta.java @@ -0,0 +1,21 @@ +package io.ebean.querybean.generator; + +import java.util.List; + +class ModuleMeta { + private final List entities; + private final List other; + + ModuleMeta(List entities, List other) { + this.entities = entities; + this.other = other; + } + + List getEntities() { + return entities; + } + + List getOther() { + return other; + } +} diff --git a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/ProcessingContext.java b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/ProcessingContext.java new file mode 100644 index 000000000..88242259b --- /dev/null +++ b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/ProcessingContext.java @@ -0,0 +1,576 @@ +package io.ebean.querybean.generator; + +import javax.annotation.processing.Filer; +import javax.annotation.processing.FilerException; +import javax.annotation.processing.Messager; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.WildcardType; +import javax.lang.model.util.ElementFilter; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import javax.tools.Diagnostic; +import javax.tools.FileObject; +import javax.tools.JavaFileObject; +import javax.tools.StandardLocation; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.LineNumberReader; +import java.io.Reader; +import java.nio.file.NoSuchFileException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.stream.Stream; + +/** + * Context for the source generation. + */ +class ProcessingContext implements Constants { + + private final ProcessingEnvironment processingEnv; + private final String generatedSources; + + private final Types typeUtils; + private final Filer filer; + private final Messager messager; + private final Elements elementUtils; + private final String generatedAnnotation; + + private final PropertyTypeMap propertyTypeMap = new PropertyTypeMap(); + + private final ReadModuleInfo readModuleInfo; + + /** + * All entity packages regardless of DB (for META-INF/ebean-generated-info.mf). + */ + private final Set allEntityPackages = new TreeSet<>(); + + private final Set otherClasses = new TreeSet<>(); + + /** + * The DB name prefixed entities. + */ + private final Set prefixEntities = new TreeSet<>(); + + /** + * Entity classes for the default database. + */ + private final Set dbEntities = new TreeSet<>(); + + /** + * Entity classes for non default databases. + */ + private final Map> otherDbEntities = new TreeMap<>(); + + /** + * All loaded entities regardless of db (to detect ones we add back from loadedPrefixEntities). + */ + private final Set loaded = new HashSet<>(); + + /** + * For partial compile the previous list of prefixed entity classes. + */ + private List loadedPrefixEntities = new ArrayList<>(); ; + + /** + * The package for the generated ModuleInfoLoader. + */ + private String factoryPackage; + + ProcessingContext(ProcessingEnvironment processingEnv) { + this.processingEnv = processingEnv; + this.typeUtils = processingEnv.getTypeUtils(); + this.filer = processingEnv.getFiler(); + this.messager = processingEnv.getMessager(); + this.elementUtils = processingEnv.getElementUtils(); + + boolean jdk8 = processingEnv.getSourceVersion().compareTo(SourceVersion.RELEASE_8) <= 0; + this.generatedAnnotation = generatedAnnotation(jdk8); + this.generatedSources = initGeneratedSources(processingEnv); + this.readModuleInfo = new ReadModuleInfo(this); + } + + TypeElement entityAnnotation() { + return elementUtils.getTypeElement(ENTITY); + } + + TypeElement embeddableAnnotation() { + return elementUtils.getTypeElement(EMBEDDABLE); + } + + TypeElement converterAnnotation() { + return elementUtils.getTypeElement(CONVERTER); + } + + TypeElement componentAnnotation() { + return elementUtils.getTypeElement(EBEAN_COMPONENT); + } + + private String generatedAnnotation(boolean jdk8) { + if (jdk8) { + return isTypeAvailable(GENERATED_8) ? GENERATED_8 : null; + } + return isTypeAvailable(GENERATED_9) ? GENERATED_9 : null; + } + + private String initGeneratedSources(ProcessingEnvironment processingEnv) { + String generatedDir = processingEnv.getOptions().get("kapt.kotlin.generated"); + return (generatedDir != null) ? generatedDir : "target/generated-sources/kapt/compile"; + } + + private boolean isTypeAvailable(String canonicalName) { + return null != elementUtils.getTypeElement(canonicalName); + } + + /** + * Return the base directory to put the kotlin generated source. + */ + String generatedSourcesDir() { + return generatedSources; + } + + /** + * Escape the type (e.g. java.lang.String) from the TypeMirror toString(). + */ + private String typeDef(TypeMirror typeMirror) { + if (typeMirror.getKind() == TypeKind.DECLARED) { + DeclaredType declaredType = (DeclaredType) typeMirror; + return Split.trimType(declaredType.asElement().toString()); + } else { + return Split.trimType(typeMirror.toString()); + } + } + + /** + * Gather all the fields (properties) for the given bean element. + */ + List allFields(Element element) { + List list = new ArrayList<>(); + gatherProperties(list, element); + return list; + } + + /** + * Recursively gather all the fields (properties) for the given bean element. + */ + private void gatherProperties(List fields, Element element) { + + TypeElement typeElement = (TypeElement) element; + TypeMirror superclass = typeElement.getSuperclass(); + Element mappedSuper = typeUtils.asElement(superclass); + if (isMappedSuperOrInheritance(mappedSuper)) { + gatherProperties(fields, mappedSuper); + } + + List allFields = ElementFilter.fieldsIn(element.getEnclosedElements()); + for (VariableElement field : allFields) { + if (!ignoreField(field)) { + fields.add(field); + } + } + } + + /** + * Not interested in static, transient or Ebean internal fields. + */ + private boolean ignoreField(VariableElement field) { + return isStaticOrTransient(field) || ignoreEbeanInternalFields(field); + } + + private boolean ignoreEbeanInternalFields(VariableElement field) { + String fieldName = field.getSimpleName().toString(); + return fieldName.startsWith("_ebean") || fieldName.startsWith("_EBEAN"); + } + + private boolean isStaticOrTransient(VariableElement field) { + Set modifiers = field.getModifiers(); + return (modifiers.contains(Modifier.STATIC) || modifiers.contains(Modifier.TRANSIENT)); + } + + private static boolean hasAnnotations(Element element, String... annotations) { + return getAnnotation(element, annotations) != null; + } + + private static AnnotationMirror getAnnotation(Element element, String... annotations) { + if (element == null) { + return null; + } + for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) { + final String name = annotationMirror.getAnnotationType().asElement().toString(); + for (String annotation : annotations) { + if (annotation.equals(name)) { + return annotationMirror; + } + } + } + return null; + } + + private boolean isMappedSuperOrInheritance(Element mappedSuper) { + return hasAnnotations(mappedSuper, MAPPED_SUPERCLASS, INHERITANCE); + } + + private boolean isEntityOrEmbedded(Element mappedSuper) { + return hasAnnotations(mappedSuper, ENTITY, EMBEDDABLE); + } + + boolean isEntity(Element element) { + return hasAnnotations(element, ENTITY); + } + + boolean isEmbeddable(Element element) { + return hasAnnotations(element, EMBEDDABLE); + } + + /** + * Find the DbName annotation and return name if found. + */ + String findDbName(TypeElement element) { + return FindDbName.value(element, typeUtils); + } + + /** + * Return true if it is a DbJson field. + */ + private static boolean dbJsonField(Element field) { + return hasAnnotations(field, DBJSON, DBJSONB); + } + + /** + * Return true if it is a DbArray field. + */ + private static boolean dbArrayField(Element field) { + return hasAnnotations(field, DBARRAY); + } + + PropertyType getPropertyType(VariableElement field) { + + TypeMirror typeMirror = field.asType(); + + TypeMirror currentType = typeMirror; + while (currentType != null) { + PropertyType type = propertyTypeMap.getType(typeDef(currentType)); + if (type != null) { + // simple scalar type + return type; + } + // go up in class hierarchy + TypeElement fieldType = (TypeElement) typeUtils.asElement(currentType); + currentType = (fieldType == null) ? null : fieldType.getSuperclass(); + } + + if (dbJsonField(field)) { + return propertyTypeMap.getDbJsonType(); + } + + if (dbArrayField(field)) { + // get generic parameter type + DeclaredType declaredType = (DeclaredType) typeMirror; + String fullType = typeDef(declaredType.getTypeArguments().get(0)); + String shortName = langShortType(Split.shortName(fullType)); + return new PropertyTypeArray(fullType, shortName); + } + + Element fieldType = typeUtils.asElement(typeMirror); + if (fieldType == null) { + return null; + } + // workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=544288 + fieldType = elementUtils.getTypeElement(fieldType.toString()); + + if (fieldType.getKind() == ElementKind.ENUM) { + String fullType = typeDef(typeMirror); + return new PropertyTypeEnum(fullType, Split.shortName(fullType)); + } + + if (isEntityOrEmbedded(fieldType)) { + // public QAssocContact contacts; + return createPropertyTypeAssoc(typeDef(typeMirror)); + } + + PropertyType result = null; + if (typeMirror.getKind() == TypeKind.DECLARED) { + DeclaredType declaredType = (DeclaredType) typeMirror; + List typeArguments = declaredType.getTypeArguments(); + if (typeArguments.size() == 1) { + TypeMirror argType = typeArguments.get(0); + if (argType.getKind() == TypeKind.WILDCARD) { + argType = ((WildcardType) argType).getExtendsBound(); + } + Element argElement = typeUtils.asElement(argType); + if (isEntityOrEmbedded(argElement)) { + result = createPropertyTypeAssoc(typeDef(argElement.asType())); + } else { + // look for targetEntity annotation attribute + final String targetEntity = readTargetEntity(field); + if (targetEntity != null) { + final TypeElement element = elementUtils.getTypeElement(targetEntity); + if (isEntityOrEmbedded(element)) { + result = createPropertyTypeAssoc(typeDef(element.asType())); + } + } + } + } + } + + if (result != null) { + return result; + } else { + if (typeInstanceOf(typeMirror, "java.lang.Comparable")) { + return new PropertyTypeScalarComparable(typeDef(typeMirror)); + } else { + return new PropertyTypeScalar(typeDef(typeMirror)); + } + } + } + + private boolean typeInstanceOf(final TypeMirror typeMirror, final CharSequence desiredInterface) { + TypeElement typeElement = (TypeElement) typeUtils.asElement(typeMirror); + if (typeElement == null || typeElement.getQualifiedName().contentEquals("java.lang.Object")) { + return false; + } + if (typeElement.getQualifiedName().contentEquals(desiredInterface)) { + return true; + } + + return typeInstanceOf(typeElement.getSuperclass(), desiredInterface) || + typeElement + .getInterfaces() + .stream() + .anyMatch(t -> typeInstanceOf(t, desiredInterface)); + } + + private String readTargetEntity(Element declaredType) { + for (AnnotationMirror annotation : declaredType.getAnnotationMirrors()) { + final Object targetEntity = readTargetEntityFromAnnotation(annotation); + if (targetEntity != null) { + return targetEntity.toString(); + } + } + return null; + } + + private static Object readTargetEntityFromAnnotation(AnnotationMirror mirror) { + for (Map.Entry entry : mirror.getElementValues().entrySet()) { + if ("targetEntity".equals(entry.getKey().getSimpleName().toString())) { + return entry.getValue().getValue(); + } + } + return null; + } + + private String langShortType(String shortName) { + if ("Integer".equals(shortName)) { + return "Int"; + } + return shortName; + } + + /** + * Create the QAssoc PropertyType. + */ + private PropertyType createPropertyTypeAssoc(String fullName) { + + String[] split = Split.split(fullName); + String propertyName = "QAssoc" + split[1]; + String packageName = packageAppend(split[0]); + return new PropertyTypeAssoc(propertyName, packageName); + } + + /** + * Prepend the package to the suffix taking null into account. + */ + private String packageAppend(String origPackage) { + if (origPackage == null) { + return "query.assoc"; + } else { + return origPackage + "." + "query.assoc"; + } + } + + /** + * Create a file writer for the given class name. + */ + JavaFileObject createWriter(String factoryClassName) throws IOException { + return filer.createSourceFile(factoryClassName); + } + + void logError(Element e, String msg, Object... args) { + messager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args), e); + } + + /** + * Log a info message. + */ + void logNote(String msg, Object... args) { + messager.printMessage(Diagnostic.Kind.NOTE, String.format(msg, args)); + } + + boolean isGeneratedAvailable() { + return generatedAnnotation != null; + } + + String getGeneratedAnnotation() { + return generatedAnnotation; + } + + void readModuleInfo() { + String factory = loadMetaInfServices(); + if (factory != null) { + TypeElement factoryType = elementUtils.getTypeElement(factory); + if (factoryType != null) { + // previous prefixed entities to add back for partial compile + final ModuleMeta read = readModuleInfo.read(factoryType); + loadedPrefixEntities.addAll(read.getEntities()); + otherClasses.addAll(read.getOther()); + } + } + } + + /** + * Register an entity with optional dbName. + */ + void addEntity(String beanFullName, String dbName) { + + loaded.add(beanFullName); + final String pkg = packageOf(beanFullName); + if (pkg != null) { + allEntityPackages.add(pkg); + updateFactoryPackage(pkg); + } + if (dbName != null) { + prefixEntities.add(dbName + ":" + beanFullName); + otherDbEntities.computeIfAbsent(dbName, s -> new TreeSet<>()).add(beanFullName); + } else { + prefixEntities.add(beanFullName); + dbEntities.add(beanFullName); + } + } + + /** + * Add back entity classes for partial compile. + */ + int complete() { + int added = 0; + for (String oldPrefixEntity : loadedPrefixEntities) { + // maybe split as dbName:entityClass + final String[] prefixEntityClass = oldPrefixEntity.split(":"); + + String dbName = null; + String entityClass; + if (prefixEntityClass.length > 1) { + dbName = prefixEntityClass[0]; + entityClass = prefixEntityClass[1]; + } else { + entityClass = prefixEntityClass[0]; + } + if (!loaded.contains(entityClass)) { + addEntity(entityClass, dbName); + added++; + } + } + return added; + } + + private String packageOf(String beanFullName) { + final int pos = beanFullName.lastIndexOf('.'); + if (pos > -1) { + return beanFullName.substring(0, pos); + } + return null; + } + + private void updateFactoryPackage(String pkg) { + if (pkg != null && (factoryPackage == null || factoryPackage.length() > pkg.length())) { + factoryPackage = pkg; + } + } + + FileObject createMetaInfServicesWriter() throws IOException { + return createMetaInfWriter(METAINF_SERVICES_MODULELOADER); + } + + FileObject createManifestWriter() throws IOException { + return createMetaInfWriter(Constants.METAINF_MANIFEST); + } + + FileObject createMetaInfWriter(String target) throws IOException { + return filer.createResource(StandardLocation.CLASS_OUTPUT, "", target); + } + + boolean hasOtherClasses() { + return !otherClasses.isEmpty(); + } + + Set getOtherClasses() { + return otherClasses; + } + + void addOther(Element element) { + otherClasses.add(element.toString()); + } + + Set getPrefixEntities() { + return prefixEntities; + } + + Set getDbEntities() { + return dbEntities; + } + + Map> getOtherDbEntities() { + return otherDbEntities; + } + + + Set getAllEntityPackages() { + return allEntityPackages; + } + + String getFactoryPackage() { + return factoryPackage != null ? factoryPackage : "unknown"; + } + + /** + * Return the class name of the generated ModuleInfoLoader + * (such that we can read the current meta data for partial compile). + */ + String loadMetaInfServices() { + try { + FileObject fileObject = processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", METAINF_SERVICES_MODULELOADER); + if (fileObject != null) { + Reader reader = fileObject.openReader(true); + LineNumberReader lineReader = new LineNumberReader(reader); + String line = lineReader.readLine(); + if (line != null) { + return line.trim(); + } + } + + } catch (FileNotFoundException | NoSuchFileException e) { + // ignore - no services file yet + } catch (FilerException e) { + logNote(null, "FilerException reading services file: " + e.getMessage()); + } catch (Exception e) { + e.printStackTrace(); + logError(null, "Error reading services file: " + e.getMessage()); + } + return null; + } +} diff --git a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/Processor.java b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/Processor.java new file mode 100644 index 000000000..cb3eba64d --- /dev/null +++ b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/Processor.java @@ -0,0 +1,117 @@ +package io.ebean.querybean.generator; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Process compiled entity beans and generates 'query beans' for them. + */ +public class Processor extends AbstractProcessor implements Constants { + + private static final String GENERATE_KOTLIN_CODE_OPTION = "generate.kotlin.code"; + private static final String KAPT_KOTLIN_GENERATED_OPTION = "kapt.kotlin.generated"; + + private ProcessingContext processingContext; + + public Processor() { + } + + @Override + public Set getSupportedOptions() { + + Set options = new LinkedHashSet<>(); + options.add(KAPT_KOTLIN_GENERATED_OPTION); + options.add(GENERATE_KOTLIN_CODE_OPTION); + return options; + } + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + this.processingContext = new ProcessingContext(processingEnv); + } + + @Override + public Set getSupportedAnnotationTypes() { + Set annotations = new LinkedHashSet<>(); + annotations.add(ENTITY); + annotations.add(EMBEDDABLE); + annotations.add(CONVERTER); + annotations.add(EBEAN_COMPONENT); + annotations.add(MODULEINFO); + return annotations; + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latest(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + + processingContext.readModuleInfo(); + int count = processEntities(roundEnv); + processOthers(roundEnv); + final int loaded = processingContext.complete(); + if (roundEnv.processingOver()) { + writeModuleInfoBean(); + } + if (count > 0) { + String msg = "Ebean APT generated %s query beans, loaded %s others - META-INF/ebean-generated-info.mf entity-packages: %s"; + processingContext.logNote(msg, count, loaded, processingContext.getAllEntityPackages()); + } + return true; + } + + private int processEntities(RoundEnvironment roundEnv) { + int count = 0; + for (Element element : roundEnv.getElementsAnnotatedWith(processingContext.embeddableAnnotation())) { + generateQueryBeans(element); + count++; + } + for (Element element : roundEnv.getElementsAnnotatedWith(processingContext.entityAnnotation())) { + generateQueryBeans(element); + count++; + } + return count; + } + + private void processOthers(RoundEnvironment round) { + processOthers(round, processingContext.converterAnnotation()); + processOthers(round, processingContext.componentAnnotation()); + } + + private void processOthers(RoundEnvironment roundEnv, TypeElement otherType) { + if (otherType != null) { + for (Element element : roundEnv.getElementsAnnotatedWith(otherType)) { + processingContext.addOther(element); + } + } + } + + private void writeModuleInfoBean() { + try { + SimpleModuleInfoWriter writer = new SimpleModuleInfoWriter(processingContext); + writer.write(); + } catch (Throwable e) { + processingContext.logError(null, "Failed to write ModuleInfoLoader " + e.getMessage()); + } + } + + private void generateQueryBeans(Element element) { + try { + SimpleQueryBeanWriter beanWriter = new SimpleQueryBeanWriter((TypeElement) element, processingContext); + beanWriter.writeRootBean(); + beanWriter.writeAssocBean(); + } catch (Throwable e) { + processingContext.logError(element, "Error generating query beans: " + e); + } + } +} diff --git a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyMeta.java b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyMeta.java new file mode 100644 index 000000000..5e0c06259 --- /dev/null +++ b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyMeta.java @@ -0,0 +1,37 @@ +package io.ebean.querybean.generator; + +/** + * Meta data for a property. + */ +class PropertyMeta { + + /** + * The property name. + */ + private final String name; + + /** + * The property type. + */ + private final PropertyType type; + + /** + * Construct given the property name and type. + */ + PropertyMeta(String name, PropertyType type) { + this.name = name; + this.type = type; + } + + String getName() { + return name; + } + + /** + * Return the type definition given the type short name and flag indicating if it is an associated bean type. + */ + String getTypeDefn(String shortName, boolean assoc) { + return type.getTypeDefn(shortName, assoc); + } + +} diff --git a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyType.java b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyType.java new file mode 100644 index 000000000..c83ac8847 --- /dev/null +++ b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyType.java @@ -0,0 +1,51 @@ +package io.ebean.querybean.generator; + +import java.util.Set; + +/** + * Property type definition. + */ +class PropertyType { + + /** + * The property type className or primitive short name. + */ + final String propertyType; + + /** + * Construct with a className of primitive name for the type. + */ + PropertyType(String propertyType) { + this.propertyType = propertyType; + } + + @Override + public String toString() { + return propertyType; + } + + /** + * Return the type definition for this property. + * + * @param shortName The short name of the property type + * @param assoc flag set to true if the property is on an association bean + */ + String getTypeDefn(String shortName, boolean assoc) { + if (assoc) { + // PLong + return propertyType + ""; + + } else { + // PLong + return propertyType + ""; + } + } + + /** + * Add any required imports for this property to the allImports set. + */ + void addImports(Set allImports) { + allImports.add("io.ebean.typequery." + propertyType); + } + +} diff --git a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeArray.java b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeArray.java new file mode 100644 index 000000000..3b9b29095 --- /dev/null +++ b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeArray.java @@ -0,0 +1,37 @@ +package io.ebean.querybean.generator; + + +import java.util.Set; + +/** + * Array property type. + */ +class PropertyTypeArray extends PropertyType { + + private final String elementClass; + + private final String elementShortName; + + PropertyTypeArray(String elementClass, String elementShortName) { + super("PArray"); + this.elementClass = elementClass; + this.elementShortName = elementShortName; + } + + @Override + String getTypeDefn(String shortName, boolean assoc) { + if (assoc) { + return "PArray"; + + } else { + return "PArray"; + } + } + + @Override + void addImports(Set allImports) { + super.addImports(allImports); + allImports.add(elementClass); + } + +} diff --git a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeAssoc.java b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeAssoc.java new file mode 100644 index 000000000..07611b782 --- /dev/null +++ b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeAssoc.java @@ -0,0 +1,31 @@ +package io.ebean.querybean.generator; + +import java.util.Set; + +/** + * Property type for associated beans (OneToMany, ManyToOne etc). + */ +class PropertyTypeAssoc extends PropertyType { + + /** + * The package name for this associated query bean. + */ + private final String assocPackage; + + /** + * Construct given the associated bean type name and package. + * + * @param qAssocTypeName the associated bean type name. + * @param assocPackage the associated bean package. + */ + PropertyTypeAssoc(String qAssocTypeName, String assocPackage) { + super(qAssocTypeName); + this.assocPackage = assocPackage; + } + + @Override + void addImports(Set allImports) { + allImports.add(assocPackage + "." + propertyType); + } + +} diff --git a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeEnum.java b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeEnum.java new file mode 100644 index 000000000..63753e35f --- /dev/null +++ b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeEnum.java @@ -0,0 +1,37 @@ +package io.ebean.querybean.generator; + + +import java.util.Set; + +/** + * Enum property type. + */ +class PropertyTypeEnum extends PropertyType { + + private final String enumClass; + + private final String enumShortName; + + PropertyTypeEnum(String enumClass, String enumShortName) { + super("PEnum"); + this.enumClass = enumClass; + this.enumShortName = enumShortName; + } + + @Override + String getTypeDefn(String shortName, boolean assoc) { + if (assoc) { + return "PEnum"; + + } else { + return "PEnum"; + } + } + + @Override + void addImports(Set allImports) { + super.addImports(allImports); + allImports.add(enumClass); + } + +} diff --git a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeMap.java b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeMap.java new file mode 100644 index 000000000..f5676f060 --- /dev/null +++ b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeMap.java @@ -0,0 +1,120 @@ +package io.ebean.querybean.generator; + +import java.io.File; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.URI; +import java.net.URL; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Currency; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; +import java.util.UUID; + +/** + * Holds the Property types and how they match to class types. + */ +class PropertyTypeMap { + + /** + * Property type for Db Json properties. + */ + private final PropertyType dbJsonType = new PropertyType("PJson"); + + private Map map = new HashMap<>(); + + PropertyTypeMap() { + + map.put("boolean", new PropertyType("PBoolean")); + map.put("short", new PropertyType("PShort")); + map.put("int", new PropertyType("PInteger")); + map.put("long", new PropertyType("PLong")); + map.put("double", new PropertyType("PDouble")); + map.put("float", new PropertyType("PFloat")); + + addType(Boolean.class); + addType(Short.class); + addType(Integer.class); + addType(Long.class); + addType(Double.class); + addType(Float.class); + addType(String.class); + addType(Timestamp.class); + map.put(java.sql.Date.class.getName(), new PropertyType("PSqlDate")); + map.put(java.util.Date.class.getName(), new PropertyType("PUtilDate")); + addType(java.sql.Time.class); + addType(TimeZone.class); + addType(BigDecimal.class); + addType(BigInteger.class); + addType(Calendar.class); + addType(Currency.class); + addType(Class.class); + map.put("java.lang.Class", new PropertyType("PClass")); + addType(Locale.class); + addType(File.class); + addType(InetAddress.class); + + map.put(URI.class.getName(), new PropertyType("PUri")); + map.put(URL.class.getName(), new PropertyType("PUrl")); + map.put(UUID.class.getName(), new PropertyType("PUuid")); + map.put("io.ebean.types.Inet", new PropertyType("PInet")); + map.put("io.ebean.types.Cidr", new PropertyType("PCidr")); + + addJava8Types(); + addJodaTypes(); + } + + private void addType(Class cls) { + String simpleName = cls.getSimpleName(); + map.put(cls.getName(), new PropertyType("P"+simpleName)); + } + + private void addJava8Types() { + + try { + Class.forName("java.time.Instant"); + } catch (ClassNotFoundException e) { + return; + } + addType(java.time.DayOfWeek.class); + addType(java.time.Duration.class); + addType(java.time.Instant.class); + addType(java.time.LocalDate.class); + addType(java.time.LocalDateTime.class); + addType(java.time.LocalTime.class); + addType(java.time.Month.class); + addType(java.time.MonthDay.class); + addType(java.time.OffsetDateTime.class); + addType(java.time.OffsetTime.class); + addType(java.time.Year.class); + addType(java.time.YearMonth.class); + addType(java.time.ZoneId.class); + addType(java.time.ZoneOffset.class); + } + + private void addJodaTypes() { + map.put("org.joda.time.DateTime", new PropertyType("PJodaDateTime")); + map.put("org.joda.time.DateMidnight", new PropertyType("PJodaDateMidnight")); + map.put("org.joda.time.LocalDate", new PropertyType("PJodaLocalDate")); + map.put("org.joda.time.LocalDateTime", new PropertyType("PJodaLocalDateTime")); + map.put("org.joda.time.LocalTime", new PropertyType("PJodaLocalTime")); + } + + /** + * Return the property type for the given class description. + */ + PropertyType getType(String classDesc) { + return map.get(classDesc); + } + + /** + * Return the Db Json property type (for DbJson and DbJsonB). + */ + PropertyType getDbJsonType() { + return dbJsonType; + } +} diff --git a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeScalar.java b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeScalar.java new file mode 100644 index 000000000..379ca1d7d --- /dev/null +++ b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeScalar.java @@ -0,0 +1,48 @@ +package io.ebean.querybean.generator; + +import java.util.Set; + +/** + * Property type for associated beans (OneToMany, ManyToOne etc). + */ +class PropertyTypeScalar extends PropertyType { + + /** + * The package name for this associated query bean. + */ + private final String assocPackage; + private final String attributeSimpleName; + + /** + * Construct given the associated bean type name and package. + * + * @param attributeClass the type in the database bean that will be serialized via ScalarType + */ + PropertyTypeScalar(String attributeClass) { + super("PScalar"); + int split = attributeClass.lastIndexOf('.'); + this.assocPackage = attributeClass.substring(0, split); + this.attributeSimpleName = attributeClass.substring(split + 1); + } + + @Override + String getTypeDefn(String shortName, boolean assoc) { + if (assoc) { + // PScalarType + return "PScalar"; + } else { + // PScalarType + return "PScalar"; + } + } + + /** + * All required imports to the allImports set. + */ + @Override + void addImports(Set allImports) { + super.addImports(allImports); + allImports.add(assocPackage + "." + attributeSimpleName); + } + +} diff --git a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeScalarComparable.java b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeScalarComparable.java new file mode 100644 index 000000000..031b150a4 --- /dev/null +++ b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeScalarComparable.java @@ -0,0 +1,48 @@ +package io.ebean.querybean.generator; + +import java.util.Set; + +/** + * Property type for associated beans (OneToMany, ManyToOne etc). + */ +class PropertyTypeScalarComparable extends PropertyType { + + /** + * The package name for this associated query bean. + */ + private final String assocPackage; + private final String attributeSimpleName; + + /** + * Construct given the associated bean type name and package. + * + * @param attributeClass the type in the database bean that will be serialized via ScalarType + */ + PropertyTypeScalarComparable(String attributeClass) { + super("PScalarComparable"); + int split = attributeClass.lastIndexOf('.'); + this.assocPackage = attributeClass.substring(0, split); + this.attributeSimpleName = attributeClass.substring(split + 1); + } + + @Override + String getTypeDefn(String shortName, boolean assoc) { + if (assoc) { + // PScalarType + return "PScalarComparable"; + } else { + // PScalarType + return "PScalarComparable"; + } + } + + /** + * All required imports to the allImports set. + */ + @Override + void addImports(Set allImports) { + super.addImports(allImports); + allImports.add(assocPackage + "." + attributeSimpleName); + } + +} diff --git a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/ReadModuleInfo.java b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/ReadModuleInfo.java new file mode 100644 index 000000000..007670c24 --- /dev/null +++ b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/ReadModuleInfo.java @@ -0,0 +1,68 @@ +package io.ebean.querybean.generator; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Helper to read the current/existing prefixed entity classes. + *

+ * These are added back on partial compile. + */ +class ReadModuleInfo { + + private final ProcessingContext ctx; + + public ReadModuleInfo(ProcessingContext ctx) { + this.ctx = ctx; + } + + ModuleMeta read(Element element) { + final List mirrors = element.getAnnotationMirrors(); + for (AnnotationMirror mirror : mirrors) { + final String name = mirror.getAnnotationType().asElement().toString(); + if (Constants.MODULEINFO.equals(name)) { + List entities = readEntities("entities", mirror); + List other = readEntities("other", mirror); + return new ModuleMeta(entities, other); + } + } + return null; + } + + private List readEntities(String key, AnnotationMirror mirror) { + final Map elementValues = mirror.getElementValues(); + final Set> entries = elementValues.entrySet(); + + for (Map.Entry entry : entries) { + if (key.equals(entry.getKey().getSimpleName().toString())) { + return readAttributes(entry.getValue()); + } + } + return Collections.emptyList(); + } + + @SuppressWarnings("unchecked") + private List readAttributes(AnnotationValue value) { + final Object entitiesValue = value.getValue(); + if (entitiesValue != null) { + try { + List vals = new ArrayList<>(); + List coll = (List) entitiesValue; + for (AnnotationValue annotationValue : coll) { + vals.add((String) annotationValue.getValue()); + } + return vals; + } catch (Exception e) { + ctx.logError(null, "Error reading ModuleInfo annotation, err " + e); + } + } + return Collections.emptyList(); + } +} \ No newline at end of file diff --git a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleModuleInfoWriter.java b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleModuleInfoWriter.java new file mode 100644 index 000000000..a8f734968 --- /dev/null +++ b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleModuleInfoWriter.java @@ -0,0 +1,204 @@ +package io.ebean.querybean.generator; + +import javax.tools.FileObject; +import javax.tools.JavaFileObject; +import java.io.IOException; +import java.io.Writer; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; + +/** + * Write the source code for the factory. + */ +class SimpleModuleInfoWriter { + + private final ProcessingContext processingContext; + + private final String factoryPackage; + private final String factoryShortName; + private final String factoryFullName; + + private Append writer; + + SimpleModuleInfoWriter(ProcessingContext processingContext) { + this.processingContext = processingContext; + this.factoryPackage = processingContext.getFactoryPackage(); + this.factoryShortName = "_ebean$ModuleInfo"; + this.factoryFullName = factoryPackage + "." + factoryShortName; + } + + void write() throws IOException { + writer = new Append(createFileWriter()); + writePackage(); + writeStartClass(); + writeEndClass(); + writer.close(); + writeServicesFile(); + writeManifestFile(); + } + + private void writeServicesFile() { + + try { + FileObject jfo = processingContext.createMetaInfServicesWriter(); + if (jfo != null) { + Writer writer = jfo.openWriter(); + writer.write(factoryFullName); + writer.close(); + } + + } catch (IOException e) { + e.printStackTrace(); + processingContext.logError(null, "Failed to write services file " + e.getMessage()); + } + } + + private void writeManifestFile() { + try { + final Set allEntityPackages = processingContext.getAllEntityPackages(); + if (!allEntityPackages.isEmpty()) { + FileObject jfo = processingContext.createManifestWriter(); + if (jfo != null) { + Writer writer = jfo.openWriter(); + writer.write("generated-by: Ebean kotlin-querybean-generator\n"); + writer.write(manifestEntityPackages(allEntityPackages)); + writer.write("\n"); + writer.close(); + } + } + + } catch (IOException e) { + e.printStackTrace(); + processingContext.logError(null, "Failed to write services file " + e.getMessage()); + } + } + + private String manifestEntityPackages(Set allEntityPackages) { + StringBuilder builder = new StringBuilder("entity-packages: "); + for (String pkg : allEntityPackages) { + // one package per line + builder.append(pkg).append("\n").append(" "); + } + return builder.delete(builder.lastIndexOf("\n"), builder.length()).append("\n").toString(); + } + + private void writePackage() { + + writer.append("package %s;", factoryPackage).eol().eol(); + + writer.append("import java.util.ArrayList;").eol(); + writer.append("import java.util.Collections;").eol(); + writer.append("import java.util.List;").eol(); + final String generated = processingContext.getGeneratedAnnotation(); + if (generated != null) { + writer.append("import %s;", generated).eol(); + } + writer.eol(); + writer.append("import io.ebean.config.ModuleInfo;").eol(); + writer.append("import io.ebean.config.ModuleInfoLoader;").eol(); + writer.eol(); + + } + + void buildAtContextModule(Append writer) { + if (processingContext.isGeneratedAvailable()) { + writer.append(Constants.AT_GENERATED).eol(); + } + writer.append("@ModuleInfo("); + if (processingContext.hasOtherClasses()) { + writer.append("other={%s}, ", otherClasses()); + } + writer.append("entities={%s}", prefixEntities()); + writer.append(")").eol(); + } + + private String otherClasses() { + return quoteTypes(processingContext.getOtherClasses()); + } + + private String prefixEntities() { + return quoteTypes(processingContext.getPrefixEntities()); + } + + private String quoteTypes(Set otherClasses) { + StringJoiner sb = new StringJoiner(","); + for (String fullType : otherClasses) { + sb.add("\"" + fullType + "\""); + } + return sb.toString(); + } + + private void writeStartClass() { + + buildAtContextModule(writer); + + writer.append("public class %s implements ModuleInfoLoader {", factoryShortName).eol().eol(); + writeMethodOtherClasses(); + writeMethodEntityClasses(processingContext.getDbEntities(), null); + + final Map> otherDbEntities = processingContext.getOtherDbEntities(); + writeMethodEntityClassesFor(otherDbEntities.keySet()); + + for (Map.Entry> otherDb : otherDbEntities.entrySet()) { + writeMethodEntityClasses(otherDb.getValue(), otherDb.getKey()); + } + } + + private void writeMethodOtherClasses() { + writer.append(" private List> otherClasses() {").eol(); + if (!processingContext.hasOtherClasses()) { + writer.append(" return Collections.emptyList();").eol(); + } else { + writer.append(" List> others = new ArrayList<>();").eol(); + for (String otherType : processingContext.getOtherClasses()) { + writer.append(" others.add(%s.class);", otherType).eol(); + } + writer.append(" return others;").eol(); + } + writer.append(" }").eol().eol(); + } + + private void writeMethodEntityClasses(Set dbEntities, String dbName) { + + String modifier = "public"; + String method = "entityClasses"; + + if (dbName == null) { + writer.append(" @Override").eol(); + } else { + method = dbName + "_entities"; + modifier = "private"; + } + writer.append(" %s List> %s() {", modifier, method).eol(); + writer.append(" List> entities = new ArrayList<>();").eol(); + for (String dbEntity : dbEntities) { + writer.append(" entities.add(%s.class);", dbEntity).eol(); + } + if (processingContext.hasOtherClasses()) { + writer.append(" entities.addAll(otherClasses());").eol(); + } + writer.append(" return entities;").eol(); + writer.append(" }").eol().eol(); + } + + private void writeMethodEntityClassesFor(Set otherDbNames) { + + writer.append(" @Override").eol(); + writer.append(" public List> entityClassesFor(String dbName) {").eol().eol(); + for (String dbName : otherDbNames) { + writer.append(" if (\"%s\".equals(dbName)) return %s_entities();", dbName, dbName).eol(); + } + writer.append(" return Collections.emptyList();").eol(); + writer.append(" }").eol().eol(); + } + + private void writeEndClass() { + writer.append("}").eol(); + } + + private Writer createFileWriter() throws IOException { + JavaFileObject jfo = processingContext.createWriter(factoryFullName); + return jfo.openWriter(); + } +} diff --git a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleQueryBeanWriter.java b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleQueryBeanWriter.java new file mode 100644 index 000000000..7c5fb4ba8 --- /dev/null +++ b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleQueryBeanWriter.java @@ -0,0 +1,361 @@ +package io.ebean.querybean.generator; + + +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +/** + * A simple implementation that generates and writes query beans. + */ +class SimpleQueryBeanWriter { + + private static final String[] javaTypes = { + "java.lang.String", + "java.lang.Integer", + "java.lang.Long", + "java.lang.Double", + "java.lang.Float", + "java.lang.Short", + "java.lang.Boolean", + "java.lang.Byte", + "java.lang.Char" + }; + + private static final String[] kotlinTypes = { + "kotlin.String", + "kotlin.Int", + "kotlin.Long", + "kotlin.Double", + "kotlin.Float", + "kotlin.Short", + "kotlin.Boolean", + "kotlin.Byte", + "kotlin.Char" + }; + + private final Set importTypes = new TreeSet<>(); + + private final List properties = new ArrayList<>(); + + private final TypeElement element; + + private final ProcessingContext processingContext; + + private final boolean isEntity; + private final boolean embeddable; + private final String dbName; + private final String beanFullName; + private final LangAdapter langAdapter; + private boolean writingAssocBean; + private final String generatedSourcesDir; + + private String destPackage; + private String origDestPackage; + + private String shortName; + private String origShortName; + + private Append writer; + + SimpleQueryBeanWriter(TypeElement element, ProcessingContext processingContext) { + this.langAdapter = new KotlinLangAdapter(); + this.generatedSourcesDir = processingContext.generatedSourcesDir(); + this.element = element; + this.processingContext = processingContext; + this.beanFullName = element.getQualifiedName().toString(); + this.destPackage = derivePackage(beanFullName) + ".query"; + this.shortName = deriveShortName(beanFullName); + this.isEntity = processingContext.isEntity(element); + this.embeddable = processingContext.isEmbeddable(element); + this.dbName = findDbName(); + } + + private String findDbName() { + return processingContext.findDbName(element); + } + + private LangAdapter lang() { + return langAdapter; + } + + private void gatherPropertyDetails() { + + final String generated = processingContext.getGeneratedAnnotation(); + if (generated != null) { + importTypes.add(generated); + } + importTypes.add(beanFullName); + importTypes.add(Constants.TQROOTBEAN); + importTypes.add(Constants.TYPEQUERYBEAN); + importTypes.add(Constants.DATABASE); + importTypes.add(Constants.FETCHGROUP); + importTypes.add(Constants.QUERY); + importTypes.add(Constants.TRANSACTION); + if (dbName != null) { + importTypes.add(Constants.DB); + } + addClassProperties(); + } + + /** + * Recursively add properties from the inheritance hierarchy. + *

+ * Includes properties from mapped super classes and usual inheritance. + *

+ */ + private void addClassProperties() { + + List fields = processingContext.allFields(element); + + for (VariableElement field : fields) { + PropertyType type = processingContext.getPropertyType(field); + if (type != null) { + type.addImports(importTypes); + properties.add(new PropertyMeta(field.getSimpleName().toString(), type)); + } + } + } + + /** + * Write the type query bean (root bean). + */ + void writeRootBean() throws IOException { + + gatherPropertyDetails(); + + if (isEmbeddable()) { + processingContext.addEntity(beanFullName, dbName); + } else if (isEntity()) { + processingContext.addEntity(beanFullName, dbName); + writer = new Append(createFileWriter()); + + translateKotlinImportTypes(); + + writePackage(); + writeImports(); + writeClass(); + writeAlias(); + writeFields(); + writeConstructors(); + //writeStaticAliasClass(); + writeClassEnd(); + + writer.close(); + } + } + + /** + * Translate the base types (String, Integer etc) to Kotlin types. + */ + private void translateKotlinImportTypes() { + for (int i = 0; i < javaTypes.length; i++) { + if (importTypes.remove(javaTypes[i])) { + importTypes.add(kotlinTypes[i]); + } + } + } + + private boolean isEntity() { + return isEntity; + } + + private boolean isEmbeddable() { + return embeddable; + } + + /** + * Write the type query assoc bean. + */ + void writeAssocBean() throws IOException { + + writingAssocBean = true; + origDestPackage = destPackage; + destPackage = destPackage + ".assoc"; + origShortName = shortName; + shortName = "Assoc" + shortName; + + prepareAssocBeanImports(); + + writer = new Append(createFileWriter()); + + writePackage(); + writeImports(); + writeClass(); + writeFields(); + writeConstructors(); + writeClassEnd(); + + writer.close(); + } + + /** + * Prepare the imports for writing assoc bean. + */ + private void prepareAssocBeanImports() { + + importTypes.remove(Constants.DB); + importTypes.remove(Constants.TQROOTBEAN); + importTypes.remove(Constants.DATABASE); + importTypes.remove(Constants.FETCHGROUP); + importTypes.remove(Constants.QUERY); + importTypes.add(Constants.TQASSOCBEAN); + if (isEntity()) { + importTypes.add(Constants.TQPROPERTY); + importTypes.add(origDestPackage + ".Q" + origShortName); + } + + // remove imports for the same package + Iterator importsIterator = importTypes.iterator(); + String checkImportStart = destPackage + ".QAssoc"; + while (importsIterator.hasNext()) { + String importType = importsIterator.next(); + if (importType.startsWith(checkImportStart)) { + importsIterator.remove(); + } + } + } + + /** + * Write constructors. + */ + private void writeConstructors() { + + if (writingAssocBean) { + writeAssocBeanFetch(); + writeAssocBeanConstructor(); + } else { + writeRootBeanConstructor(); + } + } + + /** + * Write the constructors for 'root' type query bean. + */ + private void writeRootBeanConstructor() { + lang().rootBeanConstructor(writer, shortName, dbName); + } + + private void writeAssocBeanFetch() { + if (isEntity()) { + lang().fetch(writer, origShortName); + } + } + + /** + * Write constructor for 'assoc' type query bean. + */ + private void writeAssocBeanConstructor() { + + lang().assocBeanConstructor(writer, shortName); + } + + /** + * Write all the fields. + */ + private void writeFields() { + + for (PropertyMeta property : properties) { + String typeDefn = property.getTypeDefn(shortName, writingAssocBean); + lang().fieldDefn(writer, property.getName(), typeDefn); + writer.eol(); + } + writer.eol(); + } + + /** + * Write the class definition. + */ + private void writeClass() { + + if (writingAssocBean) { + writer.append("/**").eol(); + writer.append(" * Association query bean for %s.", shortName).eol(); + writer.append(" * ").eol(); + writer.append(" * THIS IS A GENERATED OBJECT, DO NOT MODIFY THIS CLASS.").eol(); + writer.append(" */").eol(); + if (processingContext.isGeneratedAvailable()) { + writer.append(Constants.AT_GENERATED).eol(); + } + writer.append(Constants.AT_TYPEQUERYBEAN).eol(); + lang().beginAssocClass(writer, shortName, origShortName); + + } else { + writer.append("/**").eol(); + writer.append(" * Query bean for %s.", shortName).eol(); + writer.append(" * ").eol(); + writer.append(" * THIS IS A GENERATED OBJECT, DO NOT MODIFY THIS CLASS.").eol(); + writer.append(" */").eol(); + if (processingContext.isGeneratedAvailable()) { + writer.append(Constants.AT_GENERATED).eol(); + } + writer.append(Constants.AT_TYPEQUERYBEAN).eol(); + lang().beginClass(writer, shortName); + } + + writer.eol(); + } + + private void writeAlias() { + if (!writingAssocBean) { + lang().alias(writer, shortName); + } + } + + private void writeClassEnd() { + writer.append("}").eol(); + } + + /** + * Write all the imports. + */ + private void writeImports() { + + for (String importType : importTypes) { + writer.append("import %s;", importType).eol(); + } + writer.eol(); + } + + private void writePackage() { + writer.append("package %s;", destPackage).eol().eol(); + } + + private Writer createFileWriter() throws IOException { + + String relPath = destPackage.replace('.', '/'); + + File absDir = new File(generatedSourcesDir, relPath); + if (!absDir.exists() && !absDir.mkdirs()) { + processingContext.logNote("failed to create directories for:" + absDir.getAbsolutePath()); + } + + String fullPath = relPath + "/Q" + shortName + ".kt"; + File absFile = new File(generatedSourcesDir, fullPath); + return new FileWriter(absFile); + } + + private String derivePackage(String name) { + int pos = name.lastIndexOf('.'); + if (pos == -1) { + return ""; + } + return name.substring(0, pos); + } + + private String deriveShortName(String name) { + int pos = name.lastIndexOf('.'); + if (pos == -1) { + return name; + } + return name.substring(pos + 1); + } +} diff --git a/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/Split.java b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/Split.java new file mode 100644 index 000000000..61f6f2b55 --- /dev/null +++ b/kotlin-querybean-generator/src/main/java/io/ebean/querybean/generator/Split.java @@ -0,0 +1,44 @@ +package io.ebean.querybean.generator; + +/** + * Helper for splitting package and class name. + */ +class Split { + + /** + * Split into package and class name. + */ + static String[] split(String className) { + String[] result = new String[2]; + int startPos = className.lastIndexOf('.'); + if (startPos == -1) { + result[1] = className; + return result; + } + result[0] = className.substring(0, startPos); + result[1] = className.substring(startPos + 1); + return result; + } + + /** + * Trim off package to return the simple class name. + */ + static String shortName(String className) { + int startPos = className.lastIndexOf('.'); + if (startPos == -1) { + return className; + } + return className.substring(startPos + 1); + } + + /** + * Trim "? extends" from a Kotlin "out" collection type. + */ + static String trimType(String fullType) { + if (fullType.startsWith("? ")) { + return fullType.substring(10); + } + return fullType; + } + +} diff --git a/kotlin-querybean-generator/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/kotlin-querybean-generator/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 000000000..0ca63ab19 --- /dev/null +++ b/kotlin-querybean-generator/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +io.ebean.querybean.generator.Processor \ No newline at end of file diff --git a/kotlin-querybean-generator/src/test/java/io/ebean/querybean/generator/SplitTest.java b/kotlin-querybean-generator/src/test/java/io/ebean/querybean/generator/SplitTest.java new file mode 100644 index 000000000..c382fd7b6 --- /dev/null +++ b/kotlin-querybean-generator/src/test/java/io/ebean/querybean/generator/SplitTest.java @@ -0,0 +1,40 @@ +package io.ebean.querybean.generator; + +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +public class SplitTest { + + @Test + public void trimType() { + assertEquals(Split.trimType("com.foo.domain.Customer"), "com.foo.domain.Customer"); + assertEquals(Split.trimType("? extends com.foo.domain.Customer"), "com.foo.domain.Customer"); + } + + @Test + public void shortName() { + assertEquals(Split.shortName("com.foo.domain.Customer"), "Customer"); + assertEquals(Split.shortName("Customer"), "Customer"); + } + + @Test + public void split_normal() { + + String[] split = Split.split("com.foo.domain.Customer"); + + assertEquals(split[0], "com.foo.domain"); + assertEquals(split[1], "Customer"); + } + + @Test + public void split_noPackage() { + + String[] split = Split.split("Customer"); + + assertNull(split[0]); + assertEquals(split[1], "Customer"); + } + +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9450b86c2..a03d96bb3 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,9 @@ ebean-core ebean-autotune ebean-test + querybean-generator + kotlin-querybean-generator + ebean-querybean diff --git a/querybean-generator/README.md b/querybean-generator/README.md new file mode 100644 index 000000000..08406ef83 --- /dev/null +++ b/querybean-generator/README.md @@ -0,0 +1,4 @@ +# querybean-generator +Java annotation processor for generating query beans for type safe query construction. + +Refer to the documentation at: https://ebean.io/docs/query/query-beans diff --git a/querybean-generator/pom.xml b/querybean-generator/pom.xml new file mode 100644 index 000000000..8d24ec54e --- /dev/null +++ b/querybean-generator/pom.xml @@ -0,0 +1,43 @@ + + + + ebean-parent + io.ebean + 12.4.3-SNAPSHOT + + 4.0.0 + + querybean-generator + + + + + + org.avaje.composite + composite-testing + 3.1 + test + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.2 + + 1.8 + 1.8 + + -proc:none + + + + + + diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/Append.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/Append.java new file mode 100644 index 000000000..b21cbc222 --- /dev/null +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/Append.java @@ -0,0 +1,51 @@ +package io.ebean.querybean.generator; + +import java.io.IOException; +import java.io.Writer; + +/** + * Helper that wraps a writer with some useful methods to append content. + */ +class Append { + + private final Writer writer; + + Append(Writer writer) { + this.writer = writer; + } + + Append append(String content) { + try { + writer.append(content); + return this; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + void close() { + try { + writer.flush(); + writer.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + Append eol() { + try { + writer.append("\n"); + return this; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Append content with formatted arguments. + */ + Append append(String format, Object... args) { + return append(String.format(format, args)); + } + +} diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/Constants.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/Constants.java new file mode 100644 index 000000000..959c08721 --- /dev/null +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/Constants.java @@ -0,0 +1,38 @@ +package io.ebean.querybean.generator; + +interface Constants { + + String AT_GENERATED = "@Generated(\"io.ebean.querybean.generator\")"; + + String AT_TYPEQUERYBEAN = "@TypeQueryBean(\"v1\")"; + + String GENERATED_9 = "javax.annotation.processing.Generated"; + String GENERATED_8 = "javax.annotation.Generated"; + + String MAPPED_SUPERCLASS = "javax.persistence.MappedSuperclass"; + String INHERITANCE = "javax.persistence.Inheritance"; + String ENTITY = "javax.persistence.Entity"; + String EMBEDDABLE = "javax.persistence.Embeddable"; + String CONVERTER = "javax.persistence.Converter"; + String EBEAN_COMPONENT = "io.ebean.annotation.EbeanComponent"; + + String DBARRAY = "io.ebean.annotation.DbArray"; + String DBJSON = "io.ebean.annotation.DbJson"; + String DBJSONB = "io.ebean.annotation.DbJsonB"; + String DBNAME = "io.ebean.annotation.DbName"; + + String TQROOTBEAN = "io.ebean.typequery.TQRootBean"; + String TQASSOCBEAN = "io.ebean.typequery.TQAssocBean"; + String TQPROPERTY = "io.ebean.typequery.TQProperty"; + String TYPEQUERYBEAN = "io.ebean.typequery.TypeQueryBean"; + String DATABASE = "io.ebean.Database"; + String DB = "io.ebean.DB"; + String FETCHGROUP = "io.ebean.FetchGroup"; + String QUERY = "io.ebean.Query"; + String TRANSACTION = "io.ebean.Transaction"; + + String MODULEINFO = "io.ebean.config.ModuleInfo"; + String METAINF_MANIFEST = "META-INF/ebean-generated-info.mf"; + String METAINF_SERVICES_MODULELOADER = "META-INF/services/io.ebean.config.ModuleInfoLoader"; + +} \ No newline at end of file diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/FindDbName.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/FindDbName.java new file mode 100644 index 000000000..9fd013382 --- /dev/null +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/FindDbName.java @@ -0,0 +1,55 @@ +package io.ebean.querybean.generator; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; +import java.util.List; +import java.util.Map; +import java.util.Set; + +class FindDbName { + + /** + * Return the value of the DbName annotation or null if it isn't found on the element. + */ + static String value(TypeElement element, Types typeUtils) { + + AnnotationMirror mirror = findDbNameMirror(element); + if (mirror != null) { + return readDbNameValue(mirror); + } + final TypeMirror typeMirror = element.getSuperclass(); + if (typeMirror.getKind() == TypeKind.NONE) { + return null; + } + final TypeElement element1 = (TypeElement)typeUtils.asElement(typeMirror); + return value(element1, typeUtils); + } + + private static String readDbNameValue(AnnotationMirror mirror) { + + final Map elementValues = mirror.getElementValues(); + final Set> entries = elementValues.entrySet(); + for (Map.Entry entry : entries) { + if ("value".equals(entry.getKey().getSimpleName().toString())) { + return (String) entry.getValue().getValue(); + } + } + return null; + } + + private static AnnotationMirror findDbNameMirror(TypeElement element) { + final List mirrors = element.getAnnotationMirrors(); + for (AnnotationMirror mirror : mirrors) { + final String name = mirror.getAnnotationType().asElement().toString(); + if (Constants.DBNAME.equals(name)) { + return mirror; + } + } + return null; + } +} diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/ModuleMeta.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/ModuleMeta.java new file mode 100644 index 000000000..a1e98f3a1 --- /dev/null +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/ModuleMeta.java @@ -0,0 +1,21 @@ +package io.ebean.querybean.generator; + +import java.util.List; + +class ModuleMeta { + private final List entities; + private final List other; + + ModuleMeta(List entities, List other) { + this.entities = entities; + this.other = other; + } + + List getEntities() { + return entities; + } + + List getOther() { + return other; + } +} diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/ProcessingContext.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/ProcessingContext.java new file mode 100644 index 000000000..54cb79301 --- /dev/null +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/ProcessingContext.java @@ -0,0 +1,558 @@ +package io.ebean.querybean.generator; + +import javax.annotation.processing.Filer; +import javax.annotation.processing.FilerException; +import javax.annotation.processing.Messager; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import javax.tools.Diagnostic; +import javax.tools.FileObject; +import javax.tools.JavaFileObject; +import javax.tools.StandardLocation; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.LineNumberReader; +import java.io.Reader; +import java.nio.file.NoSuchFileException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +/** + * Context for the source generation. + */ +class ProcessingContext implements Constants { + + private final ProcessingEnvironment processingEnv; + private final Types typeUtils; + private final Filer filer; + private final Messager messager; + private final Elements elementUtils; + private final String generatedAnnotation; + + private final PropertyTypeMap propertyTypeMap = new PropertyTypeMap(); + + private final ReadModuleInfo readModuleInfo; + + /** + * All entity packages regardless of DB (for META-INF/ebean-generated-info.mf). + */ + private final Set allEntityPackages = new TreeSet<>(); + + private final Set otherClasses = new TreeSet<>(); + + /** + * The DB name prefixed entities. + */ + private final Set prefixEntities = new TreeSet<>(); + + /** + * Entity classes for the default database. + */ + private final Set dbEntities = new TreeSet<>(); + + /** + * Entity classes for non default databases. + */ + private final Map> otherDbEntities = new TreeMap<>(); + + /** + * All loaded entities regardless of db (to detect ones we add back from loadedPrefixEntities). + */ + private final Set loaded = new HashSet<>(); + + /** + * For partial compile the previous list of prefixed entity classes. + */ + private List loadedPrefixEntities = new ArrayList<>(); + + /** + * The package for the generated ModuleInfoLoader. + */ + private String factoryPackage; + + ProcessingContext(ProcessingEnvironment processingEnv) { + this.processingEnv = processingEnv; + this.typeUtils = processingEnv.getTypeUtils(); + this.filer = processingEnv.getFiler(); + this.messager = processingEnv.getMessager(); + this.elementUtils = processingEnv.getElementUtils(); + + boolean jdk8 = processingEnv.getSourceVersion().compareTo(SourceVersion.RELEASE_8) <= 0; + this.generatedAnnotation = generatedAnnotation(jdk8); + this.readModuleInfo = new ReadModuleInfo(this); + } + + TypeElement entityAnnotation() { + return elementUtils.getTypeElement(ENTITY); + } + + TypeElement embeddableAnnotation() { + return elementUtils.getTypeElement(EMBEDDABLE); + } + + TypeElement converterAnnotation() { + return elementUtils.getTypeElement(CONVERTER); + } + + TypeElement componentAnnotation() { + return elementUtils.getTypeElement(EBEAN_COMPONENT); + } + + private String generatedAnnotation(boolean jdk8) { + if (jdk8) { + return isTypeAvailable(GENERATED_8) ? GENERATED_8 : null; + } + return isTypeAvailable(GENERATED_9) ? GENERATED_9 : null; + } + + private boolean isTypeAvailable(String canonicalName) { + return null != elementUtils.getTypeElement(canonicalName); + } + + /** + * Gather all the fields (properties) for the given bean element. + */ + List allFields(Element element) { + List list = new ArrayList<>(); + gatherProperties(list, element); + return list; + } + + /** + * Recursively gather all the fields (properties) for the given bean element. + */ + private void gatherProperties(List fields, Element element) { + + TypeElement typeElement = (TypeElement) element; + TypeMirror superclass = typeElement.getSuperclass(); + Element mappedSuper = typeUtils.asElement(superclass); + if (isMappedSuperOrInheritance(mappedSuper)) { + gatherProperties(fields, mappedSuper); + } + + List allFields = ElementFilter.fieldsIn(element.getEnclosedElements()); + for (VariableElement field : allFields) { + if (!ignoreField(field)) { + fields.add(field); + } + } + } + + /** + * Not interested in static, transient or Ebean internal fields. + */ + private boolean ignoreField(VariableElement field) { + return isStaticOrTransient(field) || ignoreEbeanInternalFields(field); + } + + private boolean ignoreEbeanInternalFields(VariableElement field) { + String fieldName = field.getSimpleName().toString(); + return fieldName.startsWith("_ebean") || fieldName.startsWith("_EBEAN"); + } + + private boolean isStaticOrTransient(VariableElement field) { + Set modifiers = field.getModifiers(); + return (modifiers.contains(Modifier.STATIC) || modifiers.contains(Modifier.TRANSIENT)); + } + + private static boolean hasAnnotations(Element element, String... annotations) { + return getAnnotation(element, annotations) != null; + } + + private static AnnotationMirror getAnnotation(Element element, String... annotations) { + if (element == null) { + return null; + } + for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) { + final String name = annotationMirror.getAnnotationType().asElement().toString(); + for (String annotation : annotations) { + if (annotation.equals(name)) { + return annotationMirror; + } + } + } + return null; + } + + private boolean isMappedSuperOrInheritance(Element mappedSuper) { + return hasAnnotations(mappedSuper, MAPPED_SUPERCLASS, INHERITANCE); + } + + private boolean isEntityOrEmbedded(Element mappedSuper) { + return hasAnnotations(mappedSuper, ENTITY, EMBEDDABLE); + } + + boolean isEntity(Element element) { + return hasAnnotations(element, ENTITY); + } + + boolean isEmbeddable(Element element) { + return hasAnnotations(element, EMBEDDABLE); + } + + /** + * Find the DbName annotation and return name if found. + */ + String findDbName(TypeElement element) { + return FindDbName.value(element, typeUtils); + } + + /** + * Return true if it is a DbJson field. + */ + private static boolean dbJsonField(Element field) { + return hasAnnotations(field, DBJSON, DBJSONB); + } + + /** + * Return true if it is a DbArray field. + */ + private static boolean dbArrayField(Element field) { + return hasAnnotations(field, DBARRAY); + } + + /** + * Escape the type (e.g. java.lang.String) from the TypeMirror toString(). + */ + private static String typeDef(TypeMirror typeMirror) { + if (typeMirror.getKind() == TypeKind.DECLARED) { + DeclaredType declaredType = (DeclaredType) typeMirror; + return declaredType.asElement().toString(); + } else { + return typeMirror.toString(); + } + } + + PropertyType getPropertyType(VariableElement field) { + + TypeMirror typeMirror = field.asType(); + + TypeMirror currentType = typeMirror; + while (currentType != null) { + PropertyType type = propertyTypeMap.getType(typeDef(currentType)); + if (type != null) { + // simple scalar type + return type; + } + // go up in class hierarchy + TypeElement fieldType = (TypeElement) typeUtils.asElement(currentType); + currentType = (fieldType == null) ? null : fieldType.getSuperclass(); + } + + if (dbJsonField(field)) { + return propertyTypeMap.getDbJsonType(); + } + + if (dbArrayField(field)) { + // get generic parameter type + DeclaredType declaredType = (DeclaredType) typeMirror; + String fullType = typeDef(declaredType.getTypeArguments().get(0)); + return new PropertyTypeArray(fullType, Split.shortName(fullType)); + } + + Element fieldType = typeUtils.asElement(typeMirror); + if (fieldType == null) { + return null; + } + + // workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=544288 + fieldType = elementUtils.getTypeElement(fieldType.toString()); + + if (fieldType.getKind() == ElementKind.ENUM) { + String fullType = typeDef(typeMirror); + return new PropertyTypeEnum(fullType, Split.shortName(fullType)); + } + + if (isEntityOrEmbedded(fieldType)) { + // public QAssocContact contacts; + return createPropertyTypeAssoc(typeDef(typeMirror)); + } + + final PropertyType result; + if (typeMirror.getKind() == TypeKind.DECLARED) { + result = createManyTypeAssoc(field, (DeclaredType) typeMirror); + } else { + result = null; + } + + if (result != null) { + return result; + } else { + if (typeInstanceOf(typeMirror, "java.lang.Comparable")) { + return new PropertyTypeScalarComparable(typeDef(typeMirror)); + } else { + return new PropertyTypeScalar(typeDef(typeMirror)); + } + } + } + + private boolean typeInstanceOf(final TypeMirror typeMirror, final CharSequence desiredInterface) { + TypeElement typeElement = (TypeElement) typeUtils.asElement(typeMirror); + if (typeElement == null || typeElement.getQualifiedName().contentEquals("java.lang.Object")) { + return false; + } + if (typeElement.getQualifiedName().contentEquals(desiredInterface)) { + return true; + } + + return typeInstanceOf(typeElement.getSuperclass(), desiredInterface) || + typeElement + .getInterfaces() + .stream() + .anyMatch(t -> typeInstanceOf(t, desiredInterface)); + } + + private PropertyType createManyTypeAssoc(VariableElement field, DeclaredType declaredType) { + List typeArguments = declaredType.getTypeArguments(); + if (typeArguments.size() == 1) { + Element argElement = typeUtils.asElement(typeArguments.get(0)); + if (isEntityOrEmbedded(argElement)) { + return createPropertyTypeAssoc(typeDef(argElement.asType())); + } else { + // look for targetEntity annotation attribute + final String targetEntity = readTargetEntity(field); + if (targetEntity != null) { + final TypeElement element = elementUtils.getTypeElement(targetEntity); + if (isEntityOrEmbedded(element)) { + return createPropertyTypeAssoc(typeDef(element.asType())); + } + } + } + } + return null; + } + + private String readTargetEntity(Element declaredType) { + for (AnnotationMirror annotation : declaredType.getAnnotationMirrors()) { + final Object targetEntity = readTargetEntityFromAnnotation(annotation); + if (targetEntity != null) { + return targetEntity.toString(); + } + } + return null; + } + + private static Object readTargetEntityFromAnnotation(AnnotationMirror mirror) { + for (Map.Entry entry : mirror.getElementValues().entrySet()) { + if ("targetEntity".equals(entry.getKey().getSimpleName().toString())) { + return entry.getValue().getValue(); + } + } + return null; + } + + /** + * Create the QAssoc PropertyType. + */ + private PropertyType createPropertyTypeAssoc(String fullName) { + + String[] split = Split.split(fullName); + String propertyName = "QAssoc" + split[1]; + String packageName = packageAppend(split[0]); + return new PropertyTypeAssoc(propertyName, packageName); + } + + /** + * Prepend the package to the suffix taking null into account. + */ + private String packageAppend(String origPackage) { + if (origPackage == null) { + return "query.assoc"; + } else { + return origPackage + "." + "query.assoc"; + } + } + + /** + * Create a file writer for the given class name. + */ + JavaFileObject createWriter(String factoryClassName, Element originatingElement) throws IOException { + return filer.createSourceFile(factoryClassName, originatingElement); + } + + /** + * Create a file writer for the given class name without an originating element. + */ + JavaFileObject createWriter(String factoryClassName) throws IOException { + return filer.createSourceFile(factoryClassName); + } + + void logError(Element e, String msg, Object... args) { + messager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args), e); + } + + void logNote(String msg, Object... args) { + messager.printMessage(Diagnostic.Kind.NOTE, String.format(msg, args)); + } + + boolean isGeneratedAvailable() { + return generatedAnnotation != null; + } + + String getGeneratedAnnotation() { + return generatedAnnotation; + } + + void readModuleInfo() { + String factory = loadMetaInfServices(); + if (factory != null) { + TypeElement factoryType = elementUtils.getTypeElement(factory); + if (factoryType != null) { + // previous prefixed entities to add back for partial compile + final ModuleMeta read = readModuleInfo.read(factoryType); + loadedPrefixEntities.addAll(read.getEntities()); + otherClasses.addAll(read.getOther()); + } + } + } + + /** + * Register an entity with optional dbName. + */ + void addEntity(String beanFullName, String dbName) { + + loaded.add(beanFullName); + final String pkg = packageOf(beanFullName); + if (pkg != null) { + allEntityPackages.add(pkg); + updateFactoryPackage(pkg); + } + if (dbName != null) { + prefixEntities.add(dbName + ":" + beanFullName); + otherDbEntities.computeIfAbsent(dbName, s -> new TreeSet<>()).add(beanFullName); + } else { + prefixEntities.add(beanFullName); + dbEntities.add(beanFullName); + } + } + + /** + * Add back entity classes for partial compile. + */ + int complete() { + int added = 0; + for (String oldPrefixEntity : loadedPrefixEntities) { + // maybe split as dbName:entityClass + final String[] prefixEntityClass = oldPrefixEntity.split(":"); + + String dbName = null; + String entityClass; + if (prefixEntityClass.length > 1) { + dbName = prefixEntityClass[0]; + entityClass = prefixEntityClass[1]; + } else { + entityClass = prefixEntityClass[0]; + } + if (!loaded.contains(entityClass)) { + addEntity(entityClass, dbName); + added++; + } + } + return added; + } + + private String packageOf(String beanFullName) { + final int pos = beanFullName.lastIndexOf('.'); + if (pos > -1) { + return beanFullName.substring(0, pos); + } + return null; + } + + private void updateFactoryPackage(String pkg) { + if (pkg != null && (factoryPackage == null || factoryPackage.length() > pkg.length())) { + factoryPackage = pkg; + } + } + + FileObject createMetaInfServicesWriter() throws IOException { + return createMetaInfWriter(METAINF_SERVICES_MODULELOADER); + } + + FileObject createManifestWriter() throws IOException { + return createMetaInfWriter(METAINF_MANIFEST); + } + + FileObject createMetaInfWriter(String target) throws IOException { + return filer.createResource(StandardLocation.CLASS_OUTPUT, "", target); + } + + public boolean hasOtherClasses() { + return !otherClasses.isEmpty(); + } + + public Set getOtherClasses() { + return otherClasses; + } + + void addOther(Element element) { + otherClasses.add(element.toString()); + } + + Set getPrefixEntities() { + return prefixEntities; + } + + Set getDbEntities() { + return dbEntities; + } + + Map> getOtherDbEntities() { + return otherDbEntities; + } + + + Set getAllEntityPackages() { + return allEntityPackages; + } + + String getFactoryPackage() { + return factoryPackage != null ? factoryPackage : "unknown"; + } + + /** + * Return the class name of the generated ModuleInfoLoader + * (such that we can read the current meta data for partial compile). + */ + String loadMetaInfServices() { + try { + FileObject fileObject = processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", METAINF_SERVICES_MODULELOADER); + if (fileObject != null) { + Reader reader = fileObject.openReader(true); + LineNumberReader lineReader = new LineNumberReader(reader); + String line = lineReader.readLine(); + if (line != null) { + return line.trim(); + } + } + + } catch (FileNotFoundException | NoSuchFileException e) { + // ignore - no services file yet + } catch (FilerException e) { + logNote(null, "FilerException reading services file: " + e.getMessage()); + } catch (Exception e) { + e.printStackTrace(); + logError(null, "Error reading services file: " + e.getMessage()); + } + return null; + } +} diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/Processor.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/Processor.java new file mode 100644 index 000000000..1421c636f --- /dev/null +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/Processor.java @@ -0,0 +1,108 @@ +package io.ebean.querybean.generator; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Process compiled entity beans and generates 'query beans' for them. + */ +public class Processor extends AbstractProcessor implements Constants { + + private ProcessingContext processingContext; + + public Processor() { + } + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + this.processingContext = new ProcessingContext(processingEnv); + } + + @Override + public Set getSupportedAnnotationTypes() { + Set annotations = new LinkedHashSet<>(); + annotations.add(ENTITY); + annotations.add(EMBEDDABLE); + annotations.add(CONVERTER); + annotations.add(EBEAN_COMPONENT); + annotations.add(MODULEINFO); + return annotations; + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latest(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + processingContext.readModuleInfo(); + int count = processEntities(roundEnv); + processOthers(roundEnv); + final int loaded = processingContext.complete(); + if (roundEnv.processingOver()) { + writeModuleInfoBean(); + } + if (count > 0) { + String msg = "Ebean APT generated %s query beans, loaded %s others - META-INF/ebean-generated-info.mf entity-packages: %s"; + processingContext.logNote(msg, count, loaded, processingContext.getAllEntityPackages()); + } + return true; + } + + private int processEntities(RoundEnvironment roundEnv) { + int count = 0; + for (Element element : roundEnv.getElementsAnnotatedWith(processingContext.embeddableAnnotation())) { + generateQueryBeans(element); + count++; + } + for (Element element : roundEnv.getElementsAnnotatedWith(processingContext.entityAnnotation())) { + generateQueryBeans(element); + count++; + } + return count; + } + + private void processOthers(RoundEnvironment round) { + processOthers(round, processingContext.converterAnnotation()); + processOthers(round, processingContext.componentAnnotation()); + } + + private void processOthers(RoundEnvironment roundEnv, TypeElement otherType) { + if (otherType != null) { + for (Element element : roundEnv.getElementsAnnotatedWith(otherType)) { + processingContext.addOther(element); + } + } + } + + private void writeModuleInfoBean() { + try { + SimpleModuleInfoWriter writer = new SimpleModuleInfoWriter(processingContext); + writer.write(); + } catch (Throwable e) { + e.printStackTrace(); + processingContext.logError(null, "Failed to write ModuleInfoLoader error:" + e + " stack:" + Arrays.toString(e.getStackTrace())); + } + } + + private void generateQueryBeans(Element element) { + try { + SimpleQueryBeanWriter beanWriter = new SimpleQueryBeanWriter((TypeElement) element, processingContext); + beanWriter.writeRootBean(); + beanWriter.writeAssocBean(); + } catch (Throwable e) { + e.printStackTrace(); + processingContext.logError(element, "Error generating query beans: " + e); + } + } + +} diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyMeta.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyMeta.java new file mode 100644 index 000000000..6634f5b21 --- /dev/null +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyMeta.java @@ -0,0 +1,46 @@ +package io.ebean.querybean.generator; + +/** + * Meta data for a property. + */ +class PropertyMeta { + + /** + * The property name. + */ + private final String name; + + /** + * The property type. + */ + private final PropertyType type; + + /** + * Construct given the property name and type. + */ + PropertyMeta(String name, PropertyType type) { + this.name = name; + this.type = type; + } + + /** + * Return the type definition given the type short name and flag indicating if it is an associated bean type. + */ + private String getTypeDefn(String shortName, boolean assoc) { + return type.getTypeDefn(shortName, assoc); + } + + void writeFieldDefn(Append writer, String shortName, boolean assoc) { + + writer.append(" public "); + writer.append(getTypeDefn(shortName, assoc)); + writer.append(" ").append(name).append(";"); + } + + void writeFieldAliasDefn(Append writer, String shortName) { + + writer.append(" public static "); + writer.append(getTypeDefn(shortName, false)); + writer.append(" ").append(name).append(" = _alias.").append(name).append(";"); + } +} diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyType.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyType.java new file mode 100644 index 000000000..f22595187 --- /dev/null +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyType.java @@ -0,0 +1,50 @@ +package io.ebean.querybean.generator; + +import java.util.Set; + +/** + * Property type definition. + */ +class PropertyType { + + /** + * The property type className or primitive short name. + */ + final String propertyType; + + /** + * Construct with a className of primitive name for the type. + */ + PropertyType(String propertyType) { + this.propertyType = propertyType; + } + + @Override + public String toString() { + return propertyType; + } + + /** + * Return the type definition for this property. + * + * @param shortName The short name of the property type + * @param assoc flag set to true if the property is on an association bean + */ + String getTypeDefn(String shortName, boolean assoc) { + if (assoc) { + // PLong + return propertyType + ""; + } else { + // PLong + return propertyType + ""; + } + } + + /** + * Add any required imports for this property to the allImports set. + */ + void addImports(Set allImports) { + allImports.add("io.ebean.typequery." + propertyType); + } + +} diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeArray.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeArray.java new file mode 100644 index 000000000..3b9b29095 --- /dev/null +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeArray.java @@ -0,0 +1,37 @@ +package io.ebean.querybean.generator; + + +import java.util.Set; + +/** + * Array property type. + */ +class PropertyTypeArray extends PropertyType { + + private final String elementClass; + + private final String elementShortName; + + PropertyTypeArray(String elementClass, String elementShortName) { + super("PArray"); + this.elementClass = elementClass; + this.elementShortName = elementShortName; + } + + @Override + String getTypeDefn(String shortName, boolean assoc) { + if (assoc) { + return "PArray"; + + } else { + return "PArray"; + } + } + + @Override + void addImports(Set allImports) { + super.addImports(allImports); + allImports.add(elementClass); + } + +} diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeAssoc.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeAssoc.java new file mode 100644 index 000000000..c9b912886 --- /dev/null +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeAssoc.java @@ -0,0 +1,34 @@ +package io.ebean.querybean.generator; + +import java.util.Set; + +/** + * Property type for associated beans (OneToMany, ManyToOne etc). + */ +class PropertyTypeAssoc extends PropertyType { + + /** + * The package name for this associated query bean. + */ + private final String assocPackage; + + /** + * Construct given the associated bean type name and package. + * + * @param qAssocTypeName the associated bean type name. + * @param assocPackage the associated bean package. + */ + PropertyTypeAssoc(String qAssocTypeName, String assocPackage) { + super(qAssocTypeName); + this.assocPackage = assocPackage; + } + + /** + * All required imports to the allImports set. + */ + @Override + void addImports(Set allImports) { + allImports.add(assocPackage + "." + propertyType); + } + +} diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeEnum.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeEnum.java new file mode 100644 index 000000000..63753e35f --- /dev/null +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeEnum.java @@ -0,0 +1,37 @@ +package io.ebean.querybean.generator; + + +import java.util.Set; + +/** + * Enum property type. + */ +class PropertyTypeEnum extends PropertyType { + + private final String enumClass; + + private final String enumShortName; + + PropertyTypeEnum(String enumClass, String enumShortName) { + super("PEnum"); + this.enumClass = enumClass; + this.enumShortName = enumShortName; + } + + @Override + String getTypeDefn(String shortName, boolean assoc) { + if (assoc) { + return "PEnum"; + + } else { + return "PEnum"; + } + } + + @Override + void addImports(Set allImports) { + super.addImports(allImports); + allImports.add(enumClass); + } + +} diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeMap.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeMap.java new file mode 100644 index 000000000..b60a42cd7 --- /dev/null +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeMap.java @@ -0,0 +1,119 @@ +package io.ebean.querybean.generator; + +import java.io.File; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.InetAddress; +import java.net.URI; +import java.net.URL; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Currency; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; +import java.util.UUID; + +/** + * Holds the Property types and how they match to class types. + */ +class PropertyTypeMap { + + /** + * Property type for Db Json properties. + */ + private final PropertyType dbJsonType = new PropertyType("PJson"); + + private Map map = new HashMap<>(); + + PropertyTypeMap() { + + map.put("boolean", new PropertyType("PBoolean")); + map.put("short", new PropertyType("PShort")); + map.put("int", new PropertyType("PInteger")); + map.put("long", new PropertyType("PLong")); + map.put("double", new PropertyType("PDouble")); + map.put("float", new PropertyType("PFloat")); + + addType(Boolean.class); + addType(Short.class); + addType(Integer.class); + addType(Long.class); + addType(Double.class); + addType(Float.class); + addType(String.class); + addType(Timestamp.class); + map.put(java.sql.Date.class.getName(), new PropertyType("PSqlDate")); + map.put(java.util.Date.class.getName(), new PropertyType("PUtilDate")); + addType(java.sql.Time.class); + addType(TimeZone.class); + addType(BigDecimal.class); + addType(BigInteger.class); + addType(Calendar.class); + addType(Currency.class); + addType(Class.class); + addType(Locale.class); + map.put("java.lang.Class", new PropertyType("PClass")); + addType(File.class); + addType(InetAddress.class); + + map.put(URI.class.getName(), new PropertyType("PUri")); + map.put(URL.class.getName(), new PropertyType("PUrl")); + map.put(UUID.class.getName(), new PropertyType("PUuid")); + map.put("io.ebean.types.Inet", new PropertyType("PInet")); + map.put("io.ebean.types.Cidr", new PropertyType("PCidr")); + addJava8Types(); + addJodaTypes(); + } + + private void addType(Class cls) { + String simpleName = cls.getSimpleName(); + map.put(cls.getName(), new PropertyType("P"+simpleName)); + } + + private void addJava8Types() { + + try { + Class.forName("java.time.Instant"); + } catch (ClassNotFoundException e) { + return; + } + addType(java.time.DayOfWeek.class); + addType(java.time.Duration.class); + addType(java.time.Instant.class); + addType(java.time.LocalDate.class); + addType(java.time.LocalDateTime.class); + addType(java.time.LocalTime.class); + addType(java.time.Month.class); + addType(java.time.MonthDay.class); + addType(java.time.OffsetDateTime.class); + addType(java.time.OffsetTime.class); + addType(java.time.Year.class); + addType(java.time.YearMonth.class); + addType(java.time.ZoneId.class); + addType(java.time.ZoneOffset.class); + } + + private void addJodaTypes() { + map.put("org.joda.time.DateTime", new PropertyType("PJodaDateTime")); + map.put("org.joda.time.DateMidnight", new PropertyType("PJodaDateMidnight")); + map.put("org.joda.time.LocalDate", new PropertyType("PJodaLocalDate")); + map.put("org.joda.time.LocalDateTime", new PropertyType("PJodaLocalDateTime")); + map.put("org.joda.time.LocalTime", new PropertyType("PJodaLocalTime")); + } + + /** + * Return the property type for the given class description. + */ + PropertyType getType(String classDesc) { + return map.get(classDesc); + } + + /** + * Return the Db Json property type (for DbJson and DbJsonB). + */ + PropertyType getDbJsonType() { + return dbJsonType; + } +} diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeScalar.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeScalar.java new file mode 100644 index 000000000..379ca1d7d --- /dev/null +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeScalar.java @@ -0,0 +1,48 @@ +package io.ebean.querybean.generator; + +import java.util.Set; + +/** + * Property type for associated beans (OneToMany, ManyToOne etc). + */ +class PropertyTypeScalar extends PropertyType { + + /** + * The package name for this associated query bean. + */ + private final String assocPackage; + private final String attributeSimpleName; + + /** + * Construct given the associated bean type name and package. + * + * @param attributeClass the type in the database bean that will be serialized via ScalarType + */ + PropertyTypeScalar(String attributeClass) { + super("PScalar"); + int split = attributeClass.lastIndexOf('.'); + this.assocPackage = attributeClass.substring(0, split); + this.attributeSimpleName = attributeClass.substring(split + 1); + } + + @Override + String getTypeDefn(String shortName, boolean assoc) { + if (assoc) { + // PScalarType + return "PScalar"; + } else { + // PScalarType + return "PScalar"; + } + } + + /** + * All required imports to the allImports set. + */ + @Override + void addImports(Set allImports) { + super.addImports(allImports); + allImports.add(assocPackage + "." + attributeSimpleName); + } + +} diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeScalarComparable.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeScalarComparable.java new file mode 100644 index 000000000..031b150a4 --- /dev/null +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/PropertyTypeScalarComparable.java @@ -0,0 +1,48 @@ +package io.ebean.querybean.generator; + +import java.util.Set; + +/** + * Property type for associated beans (OneToMany, ManyToOne etc). + */ +class PropertyTypeScalarComparable extends PropertyType { + + /** + * The package name for this associated query bean. + */ + private final String assocPackage; + private final String attributeSimpleName; + + /** + * Construct given the associated bean type name and package. + * + * @param attributeClass the type in the database bean that will be serialized via ScalarType + */ + PropertyTypeScalarComparable(String attributeClass) { + super("PScalarComparable"); + int split = attributeClass.lastIndexOf('.'); + this.assocPackage = attributeClass.substring(0, split); + this.attributeSimpleName = attributeClass.substring(split + 1); + } + + @Override + String getTypeDefn(String shortName, boolean assoc) { + if (assoc) { + // PScalarType + return "PScalarComparable"; + } else { + // PScalarType + return "PScalarComparable"; + } + } + + /** + * All required imports to the allImports set. + */ + @Override + void addImports(Set allImports) { + super.addImports(allImports); + allImports.add(assocPackage + "." + attributeSimpleName); + } + +} diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/ReadModuleInfo.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/ReadModuleInfo.java new file mode 100644 index 000000000..01f19a304 --- /dev/null +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/ReadModuleInfo.java @@ -0,0 +1,69 @@ +package io.ebean.querybean.generator; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Helper to read the current/existing prefixed entity classes. + *

+ * These are added back on partial compile. + */ +class ReadModuleInfo { + + private final ProcessingContext ctx; + + public ReadModuleInfo(ProcessingContext ctx) { + this.ctx = ctx; + } + + ModuleMeta read(Element element) { + final List mirrors = element.getAnnotationMirrors(); + for (AnnotationMirror mirror : mirrors) { + final String name = mirror.getAnnotationType().asElement().toString(); + if (Constants.MODULEINFO.equals(name)) { + List entities = readEntities("entities", mirror); + List other = readEntities("other", mirror); + return new ModuleMeta(entities, other); + } + } + return null; + } + + @SuppressWarnings("unchecked") + private List readEntities(String key, AnnotationMirror mirror) { + final Map elementValues = mirror.getElementValues(); + final Set> entries = elementValues.entrySet(); + + for (Map.Entry entry : entries) { + if (key.equals(entry.getKey().getSimpleName().toString())) { + return readAttributes(entry.getValue()); + } + } + return Collections.emptyList(); + } + + @SuppressWarnings("unchecked") + private List readAttributes(AnnotationValue value) { + final Object entitiesValue = value.getValue(); + if (entitiesValue != null) { + try { + List vals = new ArrayList<>(); + List coll = (List) entitiesValue; + for (AnnotationValue annotationValue : coll) { + vals.add((String) annotationValue.getValue()); + } + return vals; + } catch (Exception e) { + ctx.logError(null, "Error reading ModuleInfo annotation, err " + e); + } + } + return Collections.emptyList(); + } +} diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleModuleInfoWriter.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleModuleInfoWriter.java new file mode 100644 index 000000000..c48896a50 --- /dev/null +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleModuleInfoWriter.java @@ -0,0 +1,202 @@ +package io.ebean.querybean.generator; + +import javax.tools.FileObject; +import javax.tools.JavaFileObject; +import java.io.IOException; +import java.io.Writer; +import java.util.Map; +import java.util.Set; +import java.util.StringJoiner; + +/** + * Write the source code for the factory. + */ +class SimpleModuleInfoWriter { + + private final ProcessingContext processingContext; + + private final String factoryPackage; + private final String factoryShortName; + private final String factoryFullName; + + private Append writer; + + SimpleModuleInfoWriter(ProcessingContext processingContext) { + this.processingContext = processingContext; + this.factoryPackage = processingContext.getFactoryPackage(); + this.factoryShortName = "_ebean$ModuleInfo"; + this.factoryFullName = factoryPackage + "." + factoryShortName; + } + + void write() throws IOException { + writer = new Append(createFileWriter()); + writePackage(); + writeStartClass(); + writeEndClass(); + writer.close(); + writeServicesFile(); + writeManifestFile(); + } + + private void writeServicesFile() { + try { + FileObject jfo = processingContext.createMetaInfServicesWriter(); + if (jfo != null) { + Writer writer = jfo.openWriter(); + writer.write(factoryFullName); + writer.close(); + } + + } catch (IOException e) { + e.printStackTrace(); + processingContext.logError(null, "Failed to write services file " + e.getMessage()); + } + } + + private void writeManifestFile() { + try { + final Set allEntityPackages = processingContext.getAllEntityPackages(); + if (!allEntityPackages.isEmpty()) { + FileObject jfo = processingContext.createManifestWriter(); + if (jfo != null) { + Writer writer = jfo.openWriter(); + writer.write("generated-by: Ebean query bean generator\n"); + writer.write(manifestEntityPackages(allEntityPackages)); + writer.write("\n"); + writer.close(); + } + } + + } catch (IOException e) { + e.printStackTrace(); + processingContext.logError(null, "Failed to write services file " + e.getMessage()); + } + } + + private String manifestEntityPackages(Set allEntityPackages) { + StringBuilder builder = new StringBuilder("entity-packages: "); + for (String pkg : allEntityPackages) { + // one package per line + builder.append(pkg).append("\n").append(" "); + } + return builder.delete(builder.lastIndexOf("\n"), builder.length()).append("\n").toString(); + } + + private void writePackage() { + + writer.append("package %s;", factoryPackage).eol().eol(); + + writer.append("import java.util.ArrayList;").eol(); + writer.append("import java.util.Collections;").eol(); + writer.append("import java.util.List;").eol(); + final String generated = processingContext.getGeneratedAnnotation(); + if (generated != null) { + writer.append("import %s;", generated).eol(); + } + writer.eol(); + writer.append("import io.ebean.config.ModuleInfo;").eol(); + writer.append("import io.ebean.config.ModuleInfoLoader;").eol(); + writer.eol(); + } + + void buildAtContextModule(Append writer) { + if (processingContext.isGeneratedAvailable()) { + writer.append(Constants.AT_GENERATED).eol(); + } + writer.append("@ModuleInfo("); + if (processingContext.hasOtherClasses()) { + writer.append("other={%s}, ", otherClasses()); + } + writer.append("entities={%s}", prefixEntities()); + writer.append(")").eol(); + } + + private String otherClasses() { + return quoteTypes(processingContext.getOtherClasses()); + } + + private String prefixEntities() { + return quoteTypes(processingContext.getPrefixEntities()); + } + + private String quoteTypes(Set otherClasses) { + StringJoiner sb = new StringJoiner(","); + for (String fullType : otherClasses) { + sb.add("\"" + fullType + "\""); + } + return sb.toString(); + } + + private void writeStartClass() { + + buildAtContextModule(writer); + + writer.append("public class %s implements ModuleInfoLoader {", factoryShortName).eol().eol(); + writeMethodOtherClasses(); + writeMethodEntityClasses(processingContext.getDbEntities(), null); + + final Map> otherDbEntities = processingContext.getOtherDbEntities(); + writeMethodEntityClassesFor(otherDbEntities.keySet()); + + for (Map.Entry> otherDb : otherDbEntities.entrySet()) { + writeMethodEntityClasses(otherDb.getValue(), otherDb.getKey()); + } + } + + private void writeMethodOtherClasses() { + writer.append(" private List> otherClasses() {").eol(); + if (!processingContext.hasOtherClasses()) { + writer.append(" return Collections.emptyList();").eol(); + } else { + writer.append(" List> others = new ArrayList<>();").eol(); + for (String otherType : processingContext.getOtherClasses()) { + writer.append(" others.add(%s.class);", otherType).eol(); + } + writer.append(" return others;").eol(); + } + writer.append(" }").eol().eol(); + } + + private void writeMethodEntityClasses(Set dbEntities, String dbName) { + + String modifier = "public"; + String method = "entityClasses"; + + if (dbName == null) { + writer.append(" @Override").eol(); + } else { + method = dbName + "_entities"; + modifier = "private"; + } + writer.append(" %s List> %s() {", modifier, method).eol(); + writer.append(" List> entities = new ArrayList<>();").eol(); + for (String dbEntity : dbEntities) { + writer.append(" entities.add(%s.class);", dbEntity).eol(); + } + if (processingContext.hasOtherClasses()) { + writer.append(" entities.addAll(otherClasses());").eol(); + } + writer.append(" return entities;").eol(); + writer.append(" }").eol().eol(); + } + + private void writeMethodEntityClassesFor(Set otherDbNames) { + + writer.append(" @Override").eol(); + writer.append(" public List> entityClassesFor(String dbName) {").eol().eol(); + for (String dbName : otherDbNames) { + writer.append(" if (\"%s\".equals(dbName)) return %s_entities();", dbName, dbName).eol(); + } + writer.append(" return Collections.emptyList();").eol(); + writer.append(" }").eol().eol(); + } + + private void writeEndClass() { + writer.append("}").eol(); + } + + private Writer createFileWriter() throws IOException { + JavaFileObject jfo = processingContext.createWriter(factoryFullName); + return jfo.openWriter(); + } +} diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleQueryBeanWriter.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleQueryBeanWriter.java new file mode 100644 index 000000000..a54ddc840 --- /dev/null +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/SimpleQueryBeanWriter.java @@ -0,0 +1,395 @@ +package io.ebean.querybean.generator; + + +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.tools.JavaFileObject; +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +/** + * A simple implementation that generates and writes query beans. + */ +class SimpleQueryBeanWriter { + + private final Set importTypes = new TreeSet<>(); + + private final List properties = new ArrayList<>(); + + private final TypeElement element; + + private final ProcessingContext processingContext; + + private final String dbName; + private final String beanFullName; + private final boolean isEntity; + private final boolean embeddable; + private boolean writingAssocBean; + + private String destPackage; + private String origDestPackage; + + private String shortName; + private String origShortName; + private Append writer; + + SimpleQueryBeanWriter(TypeElement element, ProcessingContext processingContext) { + this.element = element; + this.processingContext = processingContext; + this.beanFullName = element.getQualifiedName().toString(); + this.destPackage = derivePackage(beanFullName) + ".query"; + this.shortName = deriveShortName(beanFullName); + this.isEntity = processingContext.isEntity(element); + this.embeddable = processingContext.isEmbeddable(element); + this.dbName = findDbName(); + } + + private String findDbName() { + return processingContext.findDbName(element); + } + + private boolean isEntity() { + return isEntity; + } + + private boolean isEmbeddable() { + return embeddable; + } + + private void gatherPropertyDetails() { + final String generated = processingContext.getGeneratedAnnotation(); + if (generated != null) { + importTypes.add(generated); + } + importTypes.add(beanFullName); + importTypes.add(Constants.TQROOTBEAN); + importTypes.add(Constants.TYPEQUERYBEAN); + importTypes.add(Constants.DATABASE); + importTypes.add(Constants.FETCHGROUP); + importTypes.add(Constants.QUERY); + importTypes.add(Constants.TRANSACTION); + + if (dbName != null) { + importTypes.add(Constants.DB); + } + addClassProperties(); + } + + /** + * Recursively add properties from the inheritance hierarchy. + *

+ * Includes properties from mapped super classes and usual inheritance. + *

+ */ + private void addClassProperties() { + for (VariableElement field : processingContext.allFields(element)) { + PropertyType type = processingContext.getPropertyType(field); + if (type != null) { + type.addImports(importTypes); + properties.add(new PropertyMeta(field.getSimpleName().toString(), type)); + } + } + } + + /** + * Write the type query bean (root bean). + */ + void writeRootBean() throws IOException { + + gatherPropertyDetails(); + if (isEmbeddable()) { + processingContext.addEntity(beanFullName, dbName); + } else if (isEntity()) { + processingContext.addEntity(beanFullName, dbName); + writer = new Append(createFileWriter()); + + writePackage(); + writeImports(); + writeClass(); + writeAlias(); + writeFields(); + writeConstructors(); + writeStaticAliasClass(); + writeClassEnd(); + + writer.close(); + } + } + + /** + * Write the type query assoc bean. + */ + void writeAssocBean() throws IOException { + writingAssocBean = true; + origDestPackage = destPackage; + destPackage = destPackage + ".assoc"; + origShortName = shortName; + shortName = "Assoc" + shortName; + + prepareAssocBeanImports(); + + writer = new Append(createFileWriter()); + + writePackage(); + writeImports(); + writeClass(); + writeFields(); + writeConstructors(); + writeClassEnd(); + + writer.close(); + } + + /** + * Prepare the imports for writing assoc bean. + */ + private void prepareAssocBeanImports() { + importTypes.remove(Constants.DB); + importTypes.remove(Constants.TQROOTBEAN); + importTypes.remove(Constants.DATABASE); + importTypes.remove(Constants.FETCHGROUP); + importTypes.remove(Constants.QUERY); + importTypes.add(Constants.TQASSOCBEAN); + if (isEntity()) { + importTypes.add(Constants.TQPROPERTY); + importTypes.add(origDestPackage + ".Q" + origShortName); + } + + // remove imports for the same package + Iterator importsIterator = importTypes.iterator(); + String checkImportStart = destPackage + ".QAssoc"; + while (importsIterator.hasNext()) { + String importType = importsIterator.next(); + if (importType.startsWith(checkImportStart)) { + importsIterator.remove(); + } + } + } + + /** + * Write constructors. + */ + private void writeConstructors() { + if (writingAssocBean) { + writeAssocBeanFetch(); + writeAssocBeanConstructor(); + } else { + writeRootBeanConstructor(); + } + } + + /** + * Write the constructors for 'root' type query bean. + */ + private void writeRootBeanConstructor() { + + writer.eol(); + writer.append(" /**").eol(); + writer.append(" * Return a query bean used to build a FetchGroup.").eol(); + writer.append(" */").eol(); + writer.append(" public static Q%s forFetchGroup() {", shortName).eol(); + writer.append(" return new Q%s(FetchGroup.queryFor(%s.class));", shortName, shortName).eol(); + writer.append(" }").eol(); + writer.eol(); + + String name = (dbName == null) ? "default" : dbName; + writer.append(" /**").eol(); + writer.append(" * Construct using the %s Database.", name).eol(); + writer.append(" */").eol(); + writer.append(" public Q%s() {", shortName).eol(); + if (dbName == null) { + writer.append(" super(%s.class);", shortName).eol(); + } else { + writer.append(" super(%s.class, DB.byName(\"%s\"));", shortName, dbName).eol(); + } + writer.append(" }").eol(); + writer.eol(); + + writer.append(" /**").eol(); + writer.append(" * Construct with a given transaction.", name).eol(); + writer.append(" */").eol(); + writer.append(" public Q%s(Transaction transaction) {", shortName).eol(); + if (dbName == null) { + writer.append(" super(%s.class, transaction);", shortName).eol(); + } else { + writer.append(" super(%s.class, DB.byName(\"%s\"), transaction);", shortName, dbName).eol(); + } + writer.append(" }").eol(); + + writer.eol(); + writer.append(" /**").eol(); + writer.append(" * Construct with a given Database.").eol(); + writer.append(" */").eol(); + writer.append(" public Q%s(Database database) {", shortName).eol(); + writer.append(" super(%s.class, database);", shortName).eol(); + writer.append(" }").eol(); + writer.eol(); + + writer.eol(); + writer.append(" /**").eol(); + writer.append(" * Construct for Alias.").eol(); + writer.append(" */").eol(); + writer.append(" private Q%s(boolean dummy) {", shortName).eol(); + writer.append(" super(dummy);").eol(); + writer.append(" }").eol(); + + writer.eol(); + writer.append(" /**").eol(); + writer.append(" * Private constructor for FetchGroup building.").eol(); + writer.append(" */").eol(); + writer.append(" private Q%s(Query<%s> fetchGroupQuery) {", shortName, shortName).eol(); + writer.append(" super(fetchGroupQuery);").eol(); + writer.append(" }").eol(); + } + + private void writeAssocBeanFetch() { + if (isEntity()) { + writeAssocBeanFetch("", "Eagerly fetch this association loading the specified properties."); + writeAssocBeanFetch("Query", "Eagerly fetch this association using a 'query join' loading the specified properties."); + writeAssocBeanFetch("Cache", "Eagerly fetch this association using L2 cache."); + writeAssocBeanFetch("Lazy", "Use lazy loading for this association loading the specified properties."); + } + } + + private void writeAssocBeanFetch(String fetchType, String comment) { + writer.append(" /**").eol(); + writer.append(" * ").append(comment).eol(); + writer.append(" */").eol(); + writer.append(" @SafeVarargs @SuppressWarnings(\"varargs\")").eol(); + writer.append(" public final R fetch%s(TQProperty... properties) {", fetchType, origShortName).eol(); + writer.append(" return fetch%sProperties(properties);", fetchType).eol(); + writer.append(" }").eol(); + writer.eol(); + } + + /** + * Write constructor for 'assoc' type query bean. + */ + private void writeAssocBeanConstructor() { + writer.append(" public Q%s(String name, R root) {", shortName).eol(); + writer.append(" super(name, root);").eol(); + writer.append(" }").eol().eol(); + + writer.append(" public Q%s(String name, R root, String prefix) {", shortName).eol(); + writer.append(" super(name, root, prefix);").eol(); + writer.append(" }").eol(); + } + + /** + * Write all the fields. + */ + private void writeFields() { + for (PropertyMeta property : properties) { + property.writeFieldDefn(writer, shortName, writingAssocBean); + writer.eol(); + } + writer.eol(); + } + + /** + * Write the class definition. + */ + private void writeClass() { + if (writingAssocBean) { + writer.append("/**").eol(); + writer.append(" * Association query bean for %s.", shortName).eol(); + writer.append(" * ").eol(); + writer.append(" * THIS IS A GENERATED OBJECT, DO NOT MODIFY THIS CLASS.").eol(); + writer.append(" */").eol(); + if (processingContext.isGeneratedAvailable()) { + writer.append(Constants.AT_GENERATED).eol(); + } + writer.append(Constants.AT_TYPEQUERYBEAN).eol(); + writer.append("public class Q%s extends TQAssocBean<%s,R> {", shortName, origShortName).eol(); + + } else { + writer.append("/**").eol(); + writer.append(" * Query bean for %s.", shortName).eol(); + writer.append(" * ").eol(); + writer.append(" * THIS IS A GENERATED OBJECT, DO NOT MODIFY THIS CLASS.").eol(); + writer.append(" */").eol(); + if (processingContext.isGeneratedAvailable()) { + writer.append(Constants.AT_GENERATED).eol(); + } + writer.append(Constants.AT_TYPEQUERYBEAN).eol(); + writer.append("public class Q%s extends TQRootBean<%1$s,Q%1$s> {", shortName).eol(); + } + + writer.eol(); + } + + private void writeAlias() { + if (!writingAssocBean) { + writer.append(" private static final Q%s _alias = new Q%1$s(true);", shortName).eol().eol(); + + writer.append(" /**").eol(); + writer.append(" * Return the shared 'Alias' instance used to provide properties to ").eol(); + writer.append(" * select() and fetch() ").eol(); + writer.append(" */").eol(); + writer.append(" public static Q%s alias() {", shortName).eol(); + writer.append(" return _alias;").eol(); + writer.append(" }").eol(); + writer.eol(); + } + } + + private void writeStaticAliasClass() { + writer.eol(); + writer.append(" /**").eol(); + writer.append(" * Provides static properties to use in select() and fetch() ").eol(); + writer.append(" * clauses of a query. Typically referenced via static imports. ").eol(); + writer.append(" */").eol(); + writer.append(" public static class Alias {").eol(); + for (PropertyMeta property : properties) { + property.writeFieldAliasDefn(writer, shortName); + writer.eol(); + } + writer.append(" }").eol(); + } + + private void writeClassEnd() { + writer.append("}").eol(); + } + + /** + * Write all the imports. + */ + private void writeImports() { + + for (String importType : importTypes) { + writer.append("import %s;", importType).eol(); + } + writer.eol(); + } + + private void writePackage() { + writer.append("package %s;", destPackage).eol().eol(); + } + + + private Writer createFileWriter() throws IOException { + JavaFileObject jfo = processingContext.createWriter(destPackage + "." + "Q" + shortName, element); + return jfo.openWriter(); + } + + private String derivePackage(String name) { + int pos = name.lastIndexOf('.'); + if (pos == -1) { + return ""; + } + return name.substring(0, pos); + } + + private String deriveShortName(String name) { + int pos = name.lastIndexOf('.'); + if (pos == -1) { + return name; + } + return name.substring(pos + 1); + } +} diff --git a/querybean-generator/src/main/java/io/ebean/querybean/generator/Split.java b/querybean-generator/src/main/java/io/ebean/querybean/generator/Split.java new file mode 100644 index 000000000..c682344e7 --- /dev/null +++ b/querybean-generator/src/main/java/io/ebean/querybean/generator/Split.java @@ -0,0 +1,34 @@ +package io.ebean.querybean.generator; + +/** + * Helper for splitting package and class name. + */ +class Split { + + /** + * Split into package and class name. + */ + static String[] split(String className) { + String[] result = new String[2]; + int startPos = className.lastIndexOf('.'); + if (startPos == -1) { + result[1] = className; + return result; + } + result[0] = className.substring(0, startPos); + result[1] = className.substring(startPos + 1); + return result; + } + + /** + * Trim off package to return the simple class name. + */ + static String shortName(String className) { + int startPos = className.lastIndexOf('.'); + if (startPos == -1) { + return className; + } + return className.substring(startPos + 1); + } + +} diff --git a/querybean-generator/src/main/resources/META-INF/gradle/incremental.annotation.processors b/querybean-generator/src/main/resources/META-INF/gradle/incremental.annotation.processors new file mode 100644 index 000000000..678105220 --- /dev/null +++ b/querybean-generator/src/main/resources/META-INF/gradle/incremental.annotation.processors @@ -0,0 +1 @@ +io.ebean.querybean.generator.Processor,isolating diff --git a/querybean-generator/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/querybean-generator/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 000000000..0ca63ab19 --- /dev/null +++ b/querybean-generator/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +io.ebean.querybean.generator.Processor \ No newline at end of file diff --git a/querybean-generator/src/test/java/io/ebean/querybean/generator/SplitTest.java b/querybean-generator/src/test/java/io/ebean/querybean/generator/SplitTest.java new file mode 100644 index 000000000..24be4a023 --- /dev/null +++ b/querybean-generator/src/test/java/io/ebean/querybean/generator/SplitTest.java @@ -0,0 +1,35 @@ +package io.ebean.querybean.generator; + +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +public class SplitTest { + + @Test + public void shortName() throws Exception { + + assertEquals(Split.shortName("com.foo.domain.Customer"), "Customer"); + assertEquals(Split.shortName("Customer"), "Customer"); + } + + @Test + public void split_normal() throws Exception { + + String[] split = Split.split("com.foo.domain.Customer"); + + assertEquals(split[0], "com.foo.domain"); + assertEquals(split[1], "Customer"); + } + + @Test + public void split_noPackage() throws Exception { + + String[] split = Split.split("Customer"); + + assertNull(split[0]); + assertEquals(split[1], "Customer"); + } + +} \ No newline at end of file