.
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/jpalite-core/lombok.config b/jpalite-core/lombok.config
new file mode 100644
index 0000000..2066d75
--- /dev/null
+++ b/jpalite-core/lombok.config
@@ -0,0 +1 @@
+lombok.log.fieldName=LOG
diff --git a/jpalite-core/pom.xml b/jpalite-core/pom.xml
new file mode 100644
index 0000000..461d58f
--- /dev/null
+++ b/jpalite-core/pom.xml
@@ -0,0 +1,88 @@
+
+
+
+
+
+ 4.0.0
+
+ io.jpalite
+ jpalite-parent
+ 3.0.0
+ ../pom.xml
+
+
+ jpalite-core
+ JPALite Core library
+
+
+ 21
+ 21
+ UTF-8
+
+
+
+
+ jakarta.persistence
+ jakarta.persistence-api
+
+
+ com.github.jsqlparser
+ jsqlparser
+
+
+ io.quarkus
+ quarkus-opentelemetry
+ provided
+
+
+ org.projectlombok
+ lombok
+
+
+ io.quarkus
+ quarkus-infinispan-client
+ provided
+
+
+ org.infinispan
+ infinispan-api
+
+
+ org.infinispan
+ infinispan-client-hotrod
+
+
+ org.infinispan
+ infinispan-query-dsl
+
+
+ org.infinispan.protostream
+ protostream-processor
+
+
+ org.graalvm.sdk
+ graal-sdk
+
+
+ org.apache.commons
+ commons-lang3
+
+
+
diff --git a/jpalite-core/src/main/java/io/jpalite/Caching.java b/jpalite-core/src/main/java/io/jpalite/Caching.java
new file mode 100644
index 0000000..9255439
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/Caching.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+@Target({TYPE})
+@Retention(RUNTIME)
+public @interface Caching
+{
+ /**
+ * The time units used to express the idle timeout
+ *
+ * @return The TimeUnit
+ */
+ TimeUnit unit() default TimeUnit.DAYS;
+
+ /**
+ * The idle time before the record is removed from the cache. The default is 1 hour
+ *
+ * @return The idle time express in the TimeUnit
+ */
+ long idleTime() default 1;
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/ConverterClass.java b/jpalite-core/src/main/java/io/jpalite/ConverterClass.java
new file mode 100644
index 0000000..0abc352
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/ConverterClass.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+public interface ConverterClass
+{
+ /**
+ * The name of the converter
+ *
+ * @return The name
+ */
+ String getName();
+
+ /**
+ * The class of the converter
+ *
+ * @return The class
+ */
+ Class> getConvertClass();
+
+ /**
+ * A check to see if the converter should be auto applied
+ *
+ * @return true if auto apply
+ */
+ boolean isAutoApply();
+
+ /**
+ * The converter
+ *
+ * @return The converter
+ */
+ //We need to use the raw type here as the converter is not generic
+ @SuppressWarnings("java:S1452")
+ TradeSwitchConvert, ?> getConverter();
+
+ /**
+ * The attribute type
+ *
+ * @return The attribute type
+ */
+ Class> getAttributeType();
+
+ /**
+ * The database type
+ *
+ * @return The database type
+ */
+ Class> getDbType();
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/DataSourceProvider.java b/jpalite-core/src/main/java/io/jpalite/DataSourceProvider.java
new file mode 100644
index 0000000..9422e43
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/DataSourceProvider.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+
+import javax.sql.DataSource;
+
+public interface DataSourceProvider
+{
+ DataSource getDataSource(String dataSourceName);
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/DatabasePool.java b/jpalite-core/src/main/java/io/jpalite/DatabasePool.java
new file mode 100644
index 0000000..3419d16
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/DatabasePool.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+import jakarta.annotation.Nonnull;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+
+public interface DatabasePool
+{
+ /**
+ * Allocated a new connection. It is the caller's responsibility to close the connection. This call is for internal
+ * purposes only and should not be used.
+ *
+ * @return A new connection
+ * @throws SQLException
+ */
+ Connection getConnection() throws SQLException;
+
+ /**
+ * Create a new persistence context and allocate a connection to it. The result is thread local and only one
+ * connection manager will be created per thread.
+ *
+ * If the properties in the persistence contains the {@link PersistenceContext#PERSISTENCE_JTA_MANAGED} property with a value of TRUE a
+ * new PersistenceContext will be created
+ *
+ * @param persistenceUnit The persistence unit for the context
+ * @return An instance of {@link PersistenceContext}
+ * @throws SQLException
+ */
+ PersistenceContext getPersistenceContext(@Nonnull JPALitePersistenceUnit persistenceUnit) throws SQLException;
+
+ /**
+ * Instruct the database pool to close all connections own by the thread calling the method
+ */
+ void cleanup();
+
+ /**
+ * The pool name
+ *
+ * @return
+ */
+ String getPoolName();
+
+ /**
+ * Return the version of the database the pool is connected to
+ *
+ * @return the database version
+ */
+ String getDbVersion();
+
+ /**
+ * Return product name of the database the pool is connected to
+ *
+ * @return the database name
+ */
+ String getDbProductName();
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/EntityCache.java b/jpalite-core/src/main/java/io/jpalite/EntityCache.java
new file mode 100644
index 0000000..b99fec7
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/EntityCache.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+import jakarta.annotation.Nonnull;
+import jakarta.persistence.Cache;
+import jakarta.transaction.*;
+
+import java.util.List;
+
+public interface EntityCache extends Cache
+{
+ /**
+ * Create a new transaction and associate it with the current thread.
+ *
+ * @throws jakarta.transaction.NotSupportedException Thrown if the thread is already
+ * associated with a transaction and the Transaction Manager
+ * implementation does not support nested transactions.
+ * @throws jakarta.transaction.SystemException Thrown if the transaction manager
+ * encounters an unexpected error condition.
+ */
+ public void begin() throws NotSupportedException, SystemException;
+
+ /**
+ * Complete the transaction associated with the current thread. When this
+ * method completes, the thread is no longer associated with a transaction.
+ *
+ * @throws jakarta.transaction.RollbackException Thrown to indicate that
+ * the transaction has been rolled back rather than committed.
+ * @throws jakarta.transaction.HeuristicMixedException Thrown to indicate that a heuristic
+ * decision was made and that some relevant updates have been committed
+ * while others have been rolled back.
+ * @throws jakarta.transaction.HeuristicRollbackException Thrown to indicate that a
+ * heuristic decision was made and that all relevant updates have been
+ * rolled back.
+ * @throws SecurityException Thrown to indicate that the thread is
+ * not allowed to commit the transaction.
+ * @throws IllegalStateException Thrown if the current thread is
+ * not associated with a transaction.
+ * @throws SystemException Thrown if the transaction manager
+ * encounters an unexpected error condition.
+ */
+ void commit() throws RollbackException,
+ HeuristicMixedException, HeuristicRollbackException, SecurityException,
+ IllegalStateException, SystemException;
+
+ /**
+ * Roll back the transaction associated with the current thread. When this
+ * method completes, the thread is no longer associated with a
+ * transaction.
+ *
+ * @throws SecurityException Thrown to indicate that the thread is
+ * not allowed to roll back the transaction.
+ * @throws IllegalStateException Thrown if the current thread is
+ * not associated with a transaction.
+ * @throws SystemException Thrown if the transaction manager
+ * encounters an unexpected error condition.
+ */
+ void rollback() throws IllegalStateException, SecurityException,
+ SystemException;
+
+ /**
+ * Search the cache for an entity using the primary key.
+ *
+ * @param entityType The class type of the entity
+ * @param primaryKey The primary key
+ * @return the entity or null if not found
+ */
+ T find(Class entityType, Object primaryKey);
+
+ /**
+ * Search for the entity in the cache using the where clause
+ *
+ * @param entityType
+ * @param query
+ * @param
+ * @return
+ */
+ @Nonnull
+ List search(Class entityType, String query);
+
+ /**
+ * Add an entity to the cache.
+ *
+ * @param entity The entity to attach
+ */
+ void add(JPAEntity entity);
+
+ /**
+ * Add or update an entity to the cache. The last modified timestamp will also be updated
+ *
+ * @param entity The entity to update or add
+ */
+ void update(JPAEntity entity);
+
+ /**
+ * Detach an entity from the cache and mark the entity as DETACHED
+ *
+ * @param entity the entity to detach
+ */
+ void remove(JPAEntity entity);
+
+ /**
+ * Return the time for when an entity-type was last updated
+ *
+ * @param entityType The entity type
+ * @param
+ * @return The time since epoch the entity was updated
+ */
+ long lastModified(Class entityType);
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/EntityField.java b/jpalite-core/src/main/java/io/jpalite/EntityField.java
new file mode 100644
index 0000000..e94bcc8
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/EntityField.java
@@ -0,0 +1,185 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.FetchType;
+
+import java.util.Set;
+
+public interface EntityField
+{
+ /**
+ * This method is used to get the value of the field from the entity
+ *
+ * @param entity
+ * @return The value retrieved from the entity
+ */
+ Object invokeGetter(Object entity);
+
+ /**
+ * This method is used to set the value of the field on the entity
+ *
+ * @param entity The entity to set the value on
+ * @param value The value to set on the entity
+ */
+ void invokeSetter(Object entity, Object value);
+
+ /**
+ * Method to get the class of the entity
+ *
+ * @return The class of the entity
+ */
+ Class> getEnityClass();
+
+ /**
+ * Method to get the name of the field
+ *
+ * @return The name of the field
+ */
+ String getName();
+
+ /**
+ * Method to get the field number
+ *
+ * @return The field number
+ */
+ int getFieldNr();
+
+ /**
+ * Method to get the type of the field
+ *
+ * @return The type of the field
+ */
+ Class> getType();
+
+ /**
+ * Method to get the field type
+ *
+ * @return The field type
+ */
+ FieldType getFieldType();
+
+ /**
+ * Method to get the column name
+ *
+ * @return The column name
+ */
+ String getColumn();
+
+ /**
+ * Method to get the mapping type
+ *
+ * @return The mapping type
+ */
+ MappingType getMappingType();
+
+ /**
+ * Method to check if the field is unique
+ *
+ * @return True if the field is unique, false otherwise
+ */
+ boolean isUnique();
+
+ /**
+ * Method to check if the field is nullable
+ *
+ * @return True if the field is nullable, false otherwise
+ */
+ boolean isNullable();
+
+ /**
+ * Method to check if the field is insertable
+ *
+ * @return True if the field is insertable, false otherwise
+ */
+ boolean isInsertable();
+
+ /**
+ * Method to check if the field is updatable
+ *
+ * @return True if the field is updatable, false otherwise
+ */
+ boolean isUpdatable();
+
+ /**
+ * Method to check if the field is an identity field
+ *
+ * @return True if the field is an identity field, false otherwise
+ */
+ boolean isIdField();
+
+ /**
+ * Method to check if the field is a version field
+ *
+ * @return True if the field is a version field, false otherwise
+ */
+ boolean isVersionField();
+
+ /**
+ * Method to retrieve the cascade settings for the field
+ *
+ * @return The cascade settings
+ */
+ Set getCascade();
+
+ /**
+ * Method to retrieve the fetch type for the field
+ *
+ * @return The fetch type
+ */
+ FetchType getFetchType();
+
+ /**
+ * Method to set the fetch type for the field
+ *
+ * @param fetchType The fetch type
+ */
+ void setFetchType(FetchType fetchType);
+
+ /**
+ * Method to retrieve the mapped by value. This value is only set if the mapping type is {@link MappingType#MANY_TO_ONE} or
+ * {@link MappingType#ONE_TO_ONE}
+ *
+ * @return The mapped by value. If no mapped by is set, null is returned
+ */
+ String getMappedBy();
+
+ /**
+ * Method to retrieve the column definition. This value is only set if the mapping type is {@link MappingType#BASIC}
+ *
+ * @return The column definition. If no column definition is set, null is returned
+ */
+ String getColumnDefinition();
+
+ /**
+ * Method to retrieve the table name. This value is only set if the mapping type is {@link MappingType#BASIC}
+ *
+ * @return The table name. If no table is set, null is returned
+ */
+ String getTable();
+
+ /**
+ * Method to retrieve attribute converter class. This value is only set if the mapping type is {@link MappingType#BASIC}
+ *
+ * @return The attribute converter class. If no converter is set, null is returned
+ */
+ @SuppressWarnings("java:S3740")
+ // Suppress warning for generic types
+ TradeSwitchConvert getConverterClass();
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/EntityLifecycle.java b/jpalite-core/src/main/java/io/jpalite/EntityLifecycle.java
new file mode 100644
index 0000000..a094aba
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/EntityLifecycle.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+public interface EntityLifecycle
+{
+ void postLoad(Object entity);
+
+ void prePersist(Object entity);
+
+ void postPersist(Object entity);
+
+ void preUpdate(Object entity);
+
+ void postUpdate(Object entity);
+
+ void preRemove(Object entity);
+
+ void postRemove(Object entity);
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/EntityLocalCache.java b/jpalite-core/src/main/java/io/jpalite/EntityLocalCache.java
new file mode 100644
index 0000000..dab998f
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/EntityLocalCache.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+import java.util.function.Consumer;
+
+public interface EntityLocalCache
+{
+ /**
+ * Mark all the entities in the cache as DETACHED and clear the cache
+ */
+ void clear();
+
+ /**
+ * Search the cache for an entity using the primary key. Null will be returned if the entity not found or is
+ * REMOVED.
+ *
+ * @param entityType The class type of the entity
+ * @param primaryKey The primary key
+ * @return the entity or null if not found or if the entity is REMOVED
+ */
+ T find(Class entityType, Object primaryKey);
+
+ /**
+ * Search the cache for an entity using the primary key. If the entity not found and or has a status of REMOVED null
+ * will be returned.
+ *
+ * @param entityType The class type of the entity
+ * @param primaryKey The primary key
+ * @param checkIfRemoved if true throw an {@link IllegalArgumentException} if the entity has a status of REMOVED.
+ * @return the entity or null if not found
+ * @throws IllegalArgumentException if the entity has a status of REMOVED.
+ */
+ T find(Class entityType, Object primaryKey, boolean checkIfRemoved);
+
+ /**
+ * Search the cache for all entity of type entityType and performs an action for each element found.
+ *
+ * @param entityType The entity class type
+ * @param action a non-interfering action to perform on the
+ * elements
+ */
+ void foreachType(Class entityType, Consumer action);
+
+ /**
+ * Performs an action for each element of this stream.
+ *
+ * @param action a non-interfering action to perform on the
+ * elements
+ */
+ void foreach(Consumer action);
+
+ /**
+ * Attach an entity to the cache and mark the entity as ATTACHED. If there is no active transaction the entity will
+ * not be attached and the entity will be marked as DETACHED.
+ *
+ * @param entity The entity to attach
+ */
+ void manage(JPAEntity entity);
+
+ /**
+ * Detach an entity from the cache and mark the entity as DETACHED
+ *
+ * @param entity the entity to detach
+ */
+ void detach(JPAEntity entity);
+
+ /**
+ * Check if the given entity is current attached to the cache
+ *
+ * @param entity The entity to check
+ * @return true if attached
+ */
+ boolean contains(JPAEntity entity);
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/EntityMapException.java b/jpalite-core/src/main/java/io/jpalite/EntityMapException.java
new file mode 100644
index 0000000..1f03abb
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/EntityMapException.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+import jakarta.persistence.PersistenceException;
+
+import java.io.Serial;
+
+public class EntityMapException extends PersistenceException
+{
+ @Serial
+ private static final long serialVersionUID = -7640891001823058796L;
+
+ public EntityMapException(String message)
+ {
+ super(message);
+ }
+
+ public EntityMapException(String message, Throwable cause)
+ {
+ super(message, cause);
+ }
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/EntityMetaData.java b/jpalite-core/src/main/java/io/jpalite/EntityMetaData.java
new file mode 100644
index 0000000..f3adba6
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/EntityMetaData.java
@@ -0,0 +1,192 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public interface EntityMetaData
+{
+ /**
+ * Get the entity type
+ *
+ * @return the {@link EntityType} value
+ */
+ EntityType getEntityType();
+
+ /**
+ * The entity name
+ *
+ * @return the Entity name
+ */
+ String getName();
+
+ /**
+ * The Java Class associated with the entity
+ *
+ * @return The entity class
+ */
+ Class getEntityClass();
+
+ /**
+ * Create a new instance of an entity. This is a helper method where "new Entity()" is not an option
+ *
+ * @return The new instance
+ */
+ @Nonnull
+ T getNewEntity();
+
+ /**
+ * The table linked to the entity
+ *
+ * @return The table name
+ */
+ String getTable();
+
+ /**
+ * Retrieve an EntityField for a given field name.
+ *
+ * @param fieldName The field name
+ * @return The EntityField for a field.
+ * @throws UnknownFieldException If the fields does not exist
+ */
+ @Nonnull
+ EntityField getEntityField(String fieldName);
+
+ /**
+ * Check if the given field name is an entity field
+ *
+ * @param fieldName The field name to check
+ * @return True if a field
+ */
+ boolean isEntityField(String fieldName);
+
+ /**
+ * Retrieve the legacy state of the entity. An entity is seen as a legacy entity if there is no @Entity annotation.
+ *
+ * @return True if a legacy entity
+ */
+ boolean isLegacyEntity();
+
+ /**
+ * Retrieve an EntityField for a given column name
+ *
+ * @param column The column name
+ * @return The EntityField for a field or null if not found
+ */
+ @Nullable
+ EntityField getEntityFieldByColumn(String column);
+
+ /**
+ * Retrieve an EntityField for a given field number.
+ *
+ * @param fieldNr The field name
+ * @return The EntityField for a field.
+ * @throws UnknownFieldException If the fields does not exist
+ */
+ @Nonnull
+ EntityField getEntityFieldByNr(int fieldNr);
+
+ /**
+ * Return the list of all the entity fields in the entity.
+ *
+ * @return The list of fields
+ */
+ Collection getEntityFields();
+
+ /**
+ * Return all the listeners for the entity
+ *
+ * @return List of all the listeners
+ */
+ EntityLifecycle getLifecycleListeners();
+
+ /**
+ * Return the class used for the primary key
+ *
+ * @return The class of the primary key
+ */
+ @Nullable
+ @SuppressWarnings("java:S1452")
+ //generic wildcard is required
+ EntityMetaData> getIPrimaryKeyMetaData();
+
+ /**
+ * Return a list of all the defined id fields
+ *
+ * @return Set of id fields
+ */
+ @Nonnull
+ List getIdFields();
+
+ /**
+ * True if the entity have more than one ID field
+ *
+ * @return true if there are more than one if fields in the entity
+ */
+ boolean hasMultipleIdFields();
+
+ /**
+ * Return the first (only) id field. This method can only be used if there is only one id field.
+ *
+ * @return The id field
+ * @throws IllegalArgumentException if the entity have multiple id fields
+ */
+ EntityField getIdField();
+
+ /**
+ * Check if the entity can be cached
+ *
+ * @return true if cacheable
+ */
+ boolean isCacheable();
+
+ long getIdleTime();
+
+ TimeUnit getCacheTimeUnit();
+
+ /**
+ * True if the entity have a version field
+ *
+ * @return
+ */
+ boolean hasVersionField();
+
+ /**
+ * Return the metadata for the version field.
+ *
+ * @throws IllegalArgumentException if there is not version field defined
+ */
+ EntityField getVersionField();
+
+ /**
+ * Return a comma delimited string with columns
+ */
+ String getColumns();
+
+ /**
+ * The protobuf protocal file for the entity
+ *
+ * @return The proto file as a string
+ */
+ String getProtoFile();
+}//EntityMetaData
diff --git a/jpalite-core/src/main/java/io/jpalite/EntityMetaDataManager.java b/jpalite-core/src/main/java/io/jpalite/EntityMetaDataManager.java
new file mode 100644
index 0000000..5283a38
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/EntityMetaDataManager.java
@@ -0,0 +1,175 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+import io.jpalite.impl.ConverterClassImpl;
+import io.jpalite.impl.EntityMetaDataImpl;
+import jakarta.annotation.Nonnull;
+import jakarta.persistence.PersistenceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.ReentrantLock;
+
+@SuppressWarnings("java:S3740") // Suppress SonarQube warning about using Generics
+public class EntityMetaDataManager
+{
+ private static final Logger LOG = LoggerFactory.getLogger(EntityMetaDataManager.class);
+ private static final Map> REGISTRY_ENTITY_CLASSES = new ConcurrentHashMap<>();
+ private static final Map REGISTRY_ENTITY_NAMES = new ConcurrentHashMap<>();
+ private static final Map, ConverterClass> REGISTRY_CONVERTERS = new ConcurrentHashMap<>();
+ private static boolean registryLoaded = false;
+ private static final ReentrantLock lock = new ReentrantLock();
+
+ static {
+ loadEntities();
+ }
+
+ private static int loadEntities()
+ {
+ lock.lock();
+ try {
+ if (!registryLoaded) {
+ try {
+ ClassLoader loader = Thread.currentThread().getContextClassLoader();
+
+ long start = System.currentTimeMillis();
+ Enumeration urls = loader.getResources("META-INF/io.jpalite.converters");
+ while (urls.hasMoreElements()) {
+ URL location = urls.nextElement();
+ try (InputStream inputStream = location.openStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
+
+ String line = reader.readLine();
+ while (line != null) {
+ ConverterClass convertClass = new ConverterClassImpl(loader.loadClass(line));
+ REGISTRY_CONVERTERS.put(convertClass.getAttributeType(), convertClass);
+ line = reader.readLine();
+ }//while
+ }//try
+ }//while
+ LOG.info("Loaded {} converters in {}ms", REGISTRY_CONVERTERS.size(), System.currentTimeMillis() - start);
+
+ start = System.currentTimeMillis();
+ urls = loader.getResources("META-INF/persistenceUnits.properties");
+ while (urls.hasMoreElements()) {
+ URL location = urls.nextElement();
+ try (InputStream inputStream = location.openStream()) {
+ Properties properties = new Properties();
+ properties.load(inputStream);
+ properties.forEach((k, v) ->
+ {
+ try {
+ Class> entityClass = loader.loadClass(v.toString());
+ String regEntity = REGISTRY_ENTITY_NAMES.get(entityClass.getSimpleName());
+ if (regEntity == null || !regEntity.equals(v.toString())) {
+ register(new EntityMetaDataImpl(entityClass));
+ }//if
+ }
+ catch (ClassNotFoundException ex) {
+ LOG.warn("Error loading Entity {}", v, ex);
+ }//catch
+ });
+ }//try
+ }//while
+ LOG.info("Loaded {} entities in {}ms", REGISTRY_ENTITY_CLASSES.size(), System.currentTimeMillis() - start);
+ }//try
+ catch (ClassNotFoundException ex) {
+ throw new PersistenceException("Error loading converter class", ex);
+ }//catch
+ catch (IOException ex) {
+ throw new PersistenceException("Error reading persistenceUnits.properties or org.tradeswitch.converters", ex);
+ }//catch
+
+ registryLoaded = true;
+ }//if
+ }//try
+ finally {
+ lock.unlock();
+ }
+
+ return REGISTRY_ENTITY_NAMES.size();
+ }//loadEntities
+
+ public static int getEntityCount()
+ {
+ return REGISTRY_ENTITY_NAMES.size();
+ }//getEntityCount
+
+ @Nonnull
+ public static EntityMetaData getMetaData(Class> entityName)
+ {
+ EntityMetaData metaData = REGISTRY_ENTITY_CLASSES.get(entityName.getCanonicalName());
+ if (metaData == null) {
+ throw new IllegalArgumentException(entityName.getCanonicalName() + " is not a known entity or not yet registered");
+ }//if
+
+ return metaData;
+ }//getMetaData
+
+ public static void register(@Nonnull EntityMetaData> metaData)
+ {
+ if (metaData == null) {
+ throw new IllegalArgumentException("EntityMetaData cannot be null");
+ }//if
+
+ if (REGISTRY_ENTITY_NAMES.containsKey(metaData.getName())) {
+ throw new IllegalArgumentException("EntityMetaData already registered for " + metaData.getName());
+ }//if
+
+ REGISTRY_ENTITY_NAMES.put(metaData.getName(), metaData.getEntityClass().getCanonicalName());
+ REGISTRY_ENTITY_CLASSES.put(metaData.getEntityClass().getCanonicalName(), metaData);
+ }//register
+
+ public static boolean isRegistered(Class> entityName)
+ {
+ return REGISTRY_ENTITY_CLASSES.containsKey(entityName.getCanonicalName());
+ }//isRegistered
+
+ public static ConverterClass getConvertClass(Class> attributeType)
+ {
+ return REGISTRY_CONVERTERS.get(attributeType);
+ }//getConvertClass
+
+ public static EntityMetaData getMetaData(String entityName)
+ {
+ EntityMetaData> metaData = null;
+ String entityClass = REGISTRY_ENTITY_NAMES.get(entityName);
+ if (entityClass != null) {
+ metaData = REGISTRY_ENTITY_CLASSES.get(entityClass);
+ }//if
+ if (metaData == null) {
+ throw new IllegalArgumentException(entityName + " is not a known entity or not yet registered");
+ }//if
+ return metaData;
+ }//getMetaData
+
+ private EntityMetaDataManager()
+ {
+ //Made private to prevent instantiation
+ }//EntityMetaDataManager
+}//EntityMetaDataManager
diff --git a/jpalite-core/src/main/java/io/jpalite/EntityState.java b/jpalite-core/src/main/java/io/jpalite/EntityState.java
new file mode 100644
index 0000000..fc9a117
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/EntityState.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+import jakarta.persistence.PersistenceException;
+
+public enum EntityState
+{
+ /**
+ * This instance isn't, and never was, attached to an EntityManager. This instance has no corresponding rows in the
+ * database; it's usually just a new object that we created to save to the database.
+ */
+ TRANSIENT,
+
+ /**
+ * This instance is associated with a unique EntityManager object. Upon calling upon the EntityManager to persist
+ * the change to the database, this entity is guaranteed to have a corresponding consistent record in the database.
+ */
+ MANAGED,
+
+ /**
+ * This instance was once attached to the EntityManager (in a persistent state), but now it’s not. An instance
+ * enters this state if we evict it from the context, clear or close the EntityManager, or put the instance through
+ * serialization/deserialization process.
+ */
+ DETACHED,
+ /**
+ * The instance was deleted from the database and is scheduled to be removed from the entity manager. The entity is
+ * ignored and reference to an entity in this state will generate a {@link PersistenceException} exception
+ */
+ REMOVED
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/EntityTransactionListener.java b/jpalite-core/src/main/java/io/jpalite/EntityTransactionListener.java
new file mode 100644
index 0000000..b32ae75
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/EntityTransactionListener.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+public interface EntityTransactionListener
+{
+ default void preTransactionCommitEvent()
+ {
+ }
+
+ default void preTransactionBeginEvent()
+ {
+ }
+
+ default void preTransactionRollbackEvent()
+ {
+ }
+
+ default void postTransactionCommitEvent()
+ {
+ }
+
+ default void postTransactionBeginEvent()
+ {
+ }
+
+ default void postTransactionRollbackEvent()
+ {
+ }
+}//EntityTransactionListener
diff --git a/jpalite-core/src/main/java/io/jpalite/EntityType.java b/jpalite-core/src/main/java/io/jpalite/EntityType.java
new file mode 100644
index 0000000..4b656b4
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/EntityType.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+public enum EntityType
+{
+ ENTITY_NORMAL,
+ ENTITY_DATABASE,
+ ENTITY_IDCLASS,
+ ENTITY_EMBEDDABLE
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/FieldType.java b/jpalite-core/src/main/java/io/jpalite/FieldType.java
new file mode 100644
index 0000000..27dbc8e
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/FieldType.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+import org.infinispan.protostream.descriptors.Type;
+import org.infinispan.protostream.descriptors.WireType;
+
+public enum FieldType
+{
+ TYPE_BOOLEAN(Type.BOOL, null),
+ TYPE_INTEGER(Type.INT32, null),
+ TYPE_LONGLONG(Type.INT64, null),
+ TYPE_DOUBLEDOUBLE(Type.DOUBLE, null),
+ TYPE_BOOL(Type.BOOL, null),
+ TYPE_INT(Type.INT32, null),
+ TYPE_LONG(Type.INT64, null),
+ TYPE_DOUBLE(Type.DOUBLE, null),
+ TYPE_STRING(Type.STRING, null),
+ TYPE_TIMESTAMP(Type.FIXED64, null),
+ TYPE_LOCALTIME(Type.FIXED64, null),
+ TYPE_CUSTOMTYPE(Type.MESSAGE, null),
+ TYPE_ENUM(Type.STRING, null),
+ TYPE_ORDINAL_ENUM(Type.INT32, null),
+ TYPE_BYTES(Type.BYTES, null),
+ TYPE_OBJECT(Type.BYTES, null),
+ TYPE_ENTITY(Type.MESSAGE, null);
+
+ private final String type;
+ private final Type protoType;
+
+ FieldType(Type protoType, String type)
+ {
+ this.protoType = protoType;
+ this.type = type;
+ }
+
+ public String getProtoType()
+ {
+ return protoType.equals(Type.MESSAGE) ? type : protoType.toString();
+ }
+
+ public int getWireTypeTag(int fieldNr)
+ {
+ return WireType.makeTag(fieldNr, protoType.getWireType());
+ }
+
+ public static FieldType fieldType(Class> fieldType)
+ {
+ return switch (fieldType.getSimpleName()) {
+ case "Boolean" -> TYPE_BOOLEAN;
+ case "Integer" -> TYPE_INTEGER;
+ case "Long" -> TYPE_LONGLONG;
+ case "Double" -> TYPE_DOUBLEDOUBLE;
+ case "boolean" -> TYPE_BOOL;
+ case "int" -> TYPE_INT;
+ case "long" -> TYPE_LONG;
+ case "double" -> TYPE_DOUBLE;
+ case "String" -> TYPE_STRING;
+ case "Timestamp" -> TYPE_TIMESTAMP;
+ case "LocalDateTime" -> TYPE_LOCALTIME;
+ case "byte[]", "byte[][]" -> TYPE_BYTES;
+ default -> {
+ if (JPAEntity.class.isAssignableFrom(fieldType)) {
+ yield TYPE_ENTITY;
+ }
+
+ yield TYPE_OBJECT;
+ }
+ };
+ }//fieldType
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/JPAEntity.java b/jpalite-core/src/main/java/io/jpalite/JPAEntity.java
new file mode 100644
index 0000000..17e99f6
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/JPAEntity.java
@@ -0,0 +1,314 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.jpalite;
+
+import jakarta.annotation.Nonnull;
+import jakarta.persistence.LockModeType;
+import jakarta.persistence.PersistenceException;
+import jakarta.persistence.spi.LoadState;
+
+import java.io.Serializable;
+import java.sql.ResultSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+
+@SuppressWarnings("java:S100")//have methods starting "_" on purpose
+/**
+ *
+ */
+public interface JPAEntity extends Serializable
+{
+ /**
+ * Return the metadata for the entity
+ *
+ * @return The Entity Metadata
+ */
+ @SuppressWarnings("java:S1452")
+ //generic wildcard is required
+ EntityMetaData> _getMetaData();
+
+ /**
+ * Return a set of all the modified fields
+ *
+ * @return The set
+ */
+ Set _getModifiedFields();
+
+ /**
+ * Check to verify if entity is a legacy entity
+ *
+ * @return True if legacy
+ */
+ boolean _isLegacyEntity();
+
+ /**
+ * Clear both the update and snapshot modification flags.
+ */
+ void _clearModified();
+
+ /**
+ * Return the load state of the entity
+ *
+ * @return {@link LoadState}
+ */
+ LoadState _loadState();
+
+ /**
+ * Test to verify if a specified field was modified
+ *
+ * @param fieldName The field
+ * @return True if the field was modified
+ */
+ boolean _isFieldModified(String fieldName);
+
+ /**
+ * Mark a specified field as modified.
+ *
+ * @param fieldName The field to mark
+ */
+ void _clearField(String fieldName);
+
+ /**
+ * Mark a specified field as modified.
+ *
+ * @param fieldName The field to mark
+ */
+ void _markField(String fieldName);
+
+ /**
+ * Check to verify if there are any modified fields
+ *
+ * @return True any fields were modified
+ */
+ boolean _isEntityModified();
+
+ /**
+ * Get the lock mode for the entity
+ *
+ * @return The {@link LockModeType} value
+ */
+ LockModeType _getLockMode();
+
+ /**
+ * Set the entity's lock modew
+ *
+ * @param lockMode The {@link LockModeType} value assigned to the entity
+ */
+ void _setLockMode(LockModeType lockMode);
+
+ /**
+ * Get the current state of the entity
+ *
+ * @return The entity state {@link EntityState}
+ */
+ EntityState _getEntityState();
+
+ /**
+ * Change the entity's state
+ *
+ * @param newState The new state as per {@link EntityState}
+ */
+ void _setEntityState(EntityState newState);
+
+ /**
+ * Set the PersistenceContext associated with the entity if the state is EntityState.ATTACHED
+ *
+ * @return The PersistenceContext. If the entity state is not EntityState.ATTACHED the result is
+ * undetermined.
+ */
+ PersistenceContext _getPersistenceContext();
+
+ /**
+ * Set the PersistenceContext. Setting this value will change the entity state to ATTACHED. Setting the
+ * PersistenceContext to null will changed the entity state to DETACHED.
+ *
+ * @param persistenceContext The entity manager the entity is attached to
+ */
+ void _setPersistenceContext(PersistenceContext persistenceContext);
+
+ /**
+ * Get the current pending action for the entity
+ *
+ * @return The {@link PersistenceAction} value
+ */
+ PersistenceAction _getPendingAction();
+
+ /**
+ * Set the pending action
+ *
+ * @param pendingAction the {@link PersistenceAction} value to assign
+ */
+ void _setPendingAction(PersistenceAction pendingAction);
+
+ /**
+ * Get the current value of a specific field.
+ *
+ * @param fieldName The field name
+ * @return The value assigned to the value
+ */
+ X _getField(@Nonnull String fieldName);
+
+ /**
+ * Allow the caller to update a restricted field (VERSION and NON-UPDATABLE).
+ * This purpose of this method is for internal use and only be used if you know what you are doing
+ *
+ * @param method
+ */
+ void _updateRestrictedField(Consumer method);
+
+ /**
+ * Merged the supplied entity into the this one.
+ *
+ * @param entity - the entity
+ */
+ void _merge(JPAEntity entity);
+
+ /**
+ * Return the primary key for the entity. The returned object should not be modified and must be seen as immutable.
+ *
+ * @return An instance of the primary key.
+ */
+ Object _getPrimaryKey();
+
+ /**
+ * Set the entity's id fields equal to the primary key object. This can only be done on a new entity that has a
+ * TRANSIENT state After setting the primary key the entity will be marked as lazyFetched.
+ *
+ * @param primaryKey The primary key object
+ */
+ void _setPrimaryKey(Object primaryKey);
+
+ /**
+ * Return the Entity info part of the toString reply
+ *
+ * @return The entity info
+ */
+ String _getEntityInfo();
+
+ /**
+ * Return the State info part of the toString reply
+ *
+ * @return The state info
+ */
+ String _getStateInfo();
+
+ /**
+ * Return the Data info part of the toString reply
+ *
+ * @return The Data info
+ */
+ String _getDataInfo();
+
+ /**
+ * Check if the entity was loaded only by reference.
+ *
+ * @return True if lazy loaded
+ */
+ boolean _isLazyLoaded();
+
+ /**
+ * Determine the load state of a given persistent field of the entity.
+ *
+ * @param fieldName name of field whose load state is to be determined
+ * @return false if the field state has not been loaded, else true
+ */
+ boolean _isLazyLoaded(String fieldName);
+
+ /**
+ * Mark an entity as being loaded lazily.
+ */
+ void _markLazyLoaded();
+
+ /**
+ * Create mark a new entity as a reference
+ *
+ * @param primaryKey The key
+ */
+ void _makeReference(Object primaryKey);
+
+ /**
+ * Force a load of all fields that are lazy loaded
+ */
+ void _lazyFetchAll(boolean forceEagerLoad);
+
+ /**
+ * Reload a specific field or a whole entity from database
+ *
+ * @param fieldName The specific field to refresh, if Null the entity is reloaded
+ */
+ void _lazyFetch(String fieldName);
+
+ /**
+ * Clone the entity into a new entity. The new entity will be in a transient state where all the fields are set to
+ * the values found the cloned entity. Note that identity and version fields are not cloned.
+ *
+ * @return The cloned entity
+ */
+ JPAEntity _clone();
+
+ /**
+ * Copy the content of entity to the current one replacing all values and states. After the copy, entity will be
+ * detached from the context and this entity will be attached to the context. The current entity cannot be attached
+ * and entity must be attached
+ *
+ * @param entity The entity to copy from
+ */
+ void _replaceWith(JPAEntity entity);
+
+ /**
+ * Reload an entity replacing all values
+ *
+ * @param properties The query properties
+ */
+ void _refreshEntity(Map properties);
+
+ /**
+ * Take the given result set and read and set all the fields in the entity from it. The dirty flags for the fields
+ * read from the result set are cleared and any unflushed change to the field is lost. The colPrefix value is used
+ * to map PSQL queries
+ *
+ * @param colPrefix the column prefix
+ * @param resultSet the result set
+ * @throws PersistenceException If there has been an error reading the fields
+ */
+ void _mapResultSet(String colPrefix, ResultSet resultSet);
+
+ /**
+ * Deserialize the entity from a byte array.
+ *
+ * @param bytes the byte array
+ */
+ void _deserialize(byte[] bytes);
+
+ /**
+ * Serialise the entity into a byte array and return the array
+ *
+ * @return the serialised object
+ */
+ byte[] _serialize();
+
+ /**
+ * Compare the primary keys of the to entities
+ *
+ * @param entity The entity to compare with
+ * @return True if the primary keys are the same
+ */
+ boolean _entityEquals(JPAEntity entity);
+
+ Class> get$$EntityClass();
+}//JPAEntity
diff --git a/jpalite-core/src/main/java/io/jpalite/JPALiteEntityManager.java b/jpalite-core/src/main/java/io/jpalite/JPALiteEntityManager.java
new file mode 100644
index 0000000..f0edb7b
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/JPALiteEntityManager.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+import jakarta.annotation.Nonnull;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceException;
+import jakarta.persistence.Query;
+import jakarta.persistence.TransactionRequiredException;
+
+import java.io.Closeable;
+import java.sql.ResultSet;
+
+/**
+ * The JPALite implementation
+ *
+ * @see TradeSwitch Persistence Manager in Confluence
+ */
+public interface JPALiteEntityManager extends EntityManager, Closeable
+{
+ /**
+ * The jpalite.persistence.log.slowQueries hint defines, in milliseconds, the maximum time that query is
+ * expected to run and request the persistence context to log any execution that exceeds that time.
+ *
+ * Note that this will stop the query when the execution limit is reached. If the execution is to be stopped see
+ * {@link #PERSISTENCE_QUERY_TIMEOUT}
+ */
+ String PERSISTENCE_QUERY_LOG_SLOWTIME = "jpalite.persistence.log.slowQueries";
+ /**
+ * The javax.persistence.query.timeout hint defines, in seconds, how long a query is allowed to run before it gets
+ * cancelled. TradeSwitch doesn’t handle this timeout itself but provides it to the JDBC driver via the JDBC
+ * Statement.setTimeout method.
+ */
+ String PERSISTENCE_QUERY_TIMEOUT = "jakarta.persistence.query.timeout";
+ /**
+ * This hint defines the timeout in milliseconds to acquire a pessimistic lock.
+ */
+ String PERSISTENCE_LOCK_TIMEOUT = "jakarta.persistence.lock.timeout";
+ /**
+ * Valid values are USE or BYPASS. If setting is not recognized it defaults to USE.
+ *
+ * The retrieveMode hint supports the values USE and BYPASS and tells TradeSwitch if it shall USE the second-level
+ * cache to retrieve an entity or if it shall BYPASS it and get it directly from the database.
+ */
+ String PERSISTENCE_CACHE_RETRIEVEMODE = "jakarta.persistence.cache.retrieveMode";
+ /**
+ * If set to true entities retrieved in {@link Query#getResultList()} is also cached
+ */
+ String TRADESWITCH_CACHE_RESULTLIST = "org.tradeswitch.cache.resultList";
+ /**
+ * Used to hint persistence layer that the provided name should be used as the connection name, in the case of a
+ * JDBC type connection it will be used as the cursor name.
+ */
+ String TRADESWITCH_CONNECTION_NAME = "org.tradeswitch.connectionName";
+ /**
+ * Hint the JQPL parser to ignore fetchtype setting on basic fields effectively setting all basic fields to be
+ * EAGERly fetched.
+ */
+ String TRADESWITCH_OVERRIDE_BASIC_FETCHTYPE = "org.tradeswitch.override.basicFetchType";
+ /**
+ * Valid values are EAGER or LAZY. If the setting is not recognised it is ignored.
+ *
+ * Hint the JQPL parser to ignore fetchtype settings on all fields and effectively setting all fields to be EAGERly
+ * or LAZYly fetched.
+ */
+ String TRADESWITCH_OVERRIDE_FETCHTYPE = "org.tradeswitch.override.FetchType";
+ /**
+ * Valid values are TRUE or FALSE. If the setting is not recognized it is ignored. A hint that can be passed to the
+ * Entity Manager or any Query to log the actual query that is executed.
+ */
+ String JPALITE_SHOW_SQL = "jpalite.showSql";
+ /**
+ * Valid values are EAGER or LAZY. If the setting is not recognised it is ignored.
+ *
+ * A hint that can be passed to a Native Query that selection is done on the primary key. This will allow the query
+ * executor to use L2 caching.
+ */
+ String TRADESWITCH_ONLY_PRIMARYKEY_USED = "org.tradeswitch.primarykey.used";
+
+ /**
+ * Synchronize the entity to the underlying database.
+ *
+ * @throws TransactionRequiredException if there is no transaction or if the entity manager has not been joined to
+ * the current transaction
+ * @throws PersistenceException if the flush fails
+ * @throws IllegalStateException if the connection is not open
+ */
+ void flushEntity(@Nonnull T entity);
+
+ /**
+ * Synchronize the all entity of a given type belonging to the persistence context to the underlying database.
+ *
+ * @throws TransactionRequiredException if there is no transaction or if the entity manager has not been joined to
+ * the current transaction
+ * @throws PersistenceException if the flush fails
+ * @throws IllegalStateException if the connection is not open
+ */
+ void flushOnType(Class> entityClass);
+
+ /**
+ * Given a ResultSet, map that to the given entity and attach the entity to the persistence context If there is an
+ * active transaction and the entity is already under management of the persistence context, the result will be
+ * merged with the existing entity and that entity will be return.
+ *
+ * @param entity The entity
+ * @param resultSet The result
+ * @return The mapped entity.
+ */
+ X mapResultSet(@Nonnull X entity, ResultSet resultSet);
+
+ /**
+ * Clone a given entity returning an entity in a transient state where all the fields are set to the values found
+ * the given entity. Note that identity and version fields are not cloned.
+ *
+ * @param entity The entity to clone
+ * @return The cloned entity
+ */
+ T clone(@Nonnull T entity);
+}//JPALiteEntityManager
diff --git a/jpalite-core/src/main/java/io/jpalite/JPALitePersistenceUnit.java b/jpalite-core/src/main/java/io/jpalite/JPALitePersistenceUnit.java
new file mode 100644
index 0000000..31b329a
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/JPALitePersistenceUnit.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+import jakarta.persistence.spi.PersistenceUnitInfo;
+import org.infinispan.commons.configuration.BasicConfiguration;
+
+public interface JPALitePersistenceUnit extends PersistenceUnitInfo
+{
+ String getDataSourceName();
+
+ String getCacheName();
+
+ String getCacheProvider();
+
+ BasicConfiguration getCacheConfig();
+
+ Boolean getMultiTenantMode();
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/LazyInitializationException.java b/jpalite-core/src/main/java/io/jpalite/LazyInitializationException.java
new file mode 100644
index 0000000..bbedef9
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/LazyInitializationException.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+import jakarta.persistence.PersistenceException;
+
+public class LazyInitializationException extends PersistenceException
+{
+
+ public LazyInitializationException(String message)
+ {
+ super(message);
+ }
+
+ public LazyInitializationException(String message, Throwable cause)
+ {
+ super(message, cause);
+ }
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/MappingType.java b/jpalite-core/src/main/java/io/jpalite/MappingType.java
new file mode 100644
index 0000000..10296ae
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/MappingType.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+public enum MappingType
+{
+ BASIC,
+ EMBEDDED,
+ ONE_TO_ONE,
+ ONE_TO_MANY,
+ MANY_TO_ONE,
+ MANY_TO_MANY
+}//MappingType
diff --git a/jpalite-core/src/main/java/io/jpalite/MultiTenant.java b/jpalite-core/src/main/java/io/jpalite/MultiTenant.java
new file mode 100644
index 0000000..76d3238
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/MultiTenant.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+public interface MultiTenant
+{
+ JPALitePersistenceUnit getPersistenceUnit(JPALitePersistenceUnit persistenceUnit);
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/PersistenceAction.java b/jpalite-core/src/main/java/io/jpalite/PersistenceAction.java
new file mode 100644
index 0000000..da8c23b
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/PersistenceAction.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+public enum PersistenceAction
+{
+ NONE,
+ INSERT,
+ UPDATE,
+ DELETE
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/PersistenceContext.java b/jpalite-core/src/main/java/io/jpalite/PersistenceContext.java
new file mode 100644
index 0000000..74a4ff6
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/PersistenceContext.java
@@ -0,0 +1,272 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+import jakarta.annotation.Nonnull;
+import jakarta.persistence.EntityTransaction;
+import jakarta.persistence.PersistenceException;
+import jakarta.persistence.SynchronizationType;
+import jakarta.persistence.TransactionRequiredException;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.util.Map;
+
+public interface PersistenceContext extends EntityTransaction, AutoCloseable
+{
+ /**
+ * The tradeswitch.persistence.jta hint is used to indicated that the transaction management is done via JTA.
+ */
+ String PERSISTENCE_JTA_MANAGED = "tradeswitch.persistence.jta";
+
+ /**
+ * The method is used to retrieve the persistence unit used to create the context
+ *
+ * @return The persistence unit
+ */
+ JPALitePersistenceUnit getPersistenceUnit();
+
+ /**
+ * This method is used to check if the context have support for a given {@link EntityType}
+ *
+ * @param pEntityType the entity type
+ * @return True if supported
+ */
+ boolean supportedEntityType(EntityType pEntityType);
+
+ /**
+ * Open a new connection. If already open stack will be maintained to keep track of the number of times the
+ * connection has been opened with closing it. The cursor name is null current thread name is used.
+ *
+ * @param pConnectionName The connection name
+ * @return The connection
+ */
+ @Nonnull
+ Connection getConnection(String pConnectionName);
+
+ /**
+ * Close the connection, if the connection was opened previously the open stack will be popped. If forced the open
+ * stack will be flushed and the connection will be closed.
+ */
+ void close();
+
+ /**
+ * Release the connection regardless of the number of times it was opened
+ */
+ void release();
+
+ /**
+ * Return true if the persistence context has be released from the database pool
+ *
+ * @return true if released
+ */
+ boolean isReleased();
+
+ /**
+ * Register a transaction listener. The listener will be called whenever a new transaction started, committed of
+ * rolled back
+ *
+ * @param pListener The listener
+ */
+ void addTransactionListener(EntityTransactionListener pListener);
+
+ /**
+ * Remove a previously set listener
+ *
+ * @param pListener
+ */
+ void removeTransactionListener(EntityTransactionListener pListener);
+
+ /**
+ * Return number of times the same a transaction has been started on the same connection
+ *
+ * @return The transaction depth
+ */
+ int getTransactionDepth();
+
+ /**
+ * Called by the database undertow to report the query that was execute
+ *
+ * @param pLastQuery The query
+ */
+ void setLastQuery(String pLastQuery);
+
+ /**
+ * Get the last executed query
+ *
+ * @return The query
+ */
+ String getLastQuery();
+
+ /**
+ * Return the open level the connection is at
+ *
+ * @return the open level
+ */
+ int getOpenLevel();
+
+ /**
+ * Get the current cursor name that will be used
+ *
+ * @return The cursor name
+ */
+ String getConnectionName();
+
+ /**
+ * Set the cursor name to be used when request a connection
+ *
+ * @param pConnectionName the cursor name
+ */
+ void setConnectionName(String pConnectionName);
+
+ /**
+ * Return the Level 1 cache instance of {@link EntityLocalCache} linked to the context.
+ *
+ * @return the cache
+ */
+ EntityLocalCache l1Cache();
+
+ /**
+ * Return the Level 2 cache instance of {@link EntityLocalCache} linked to the context.
+ *
+ * @return the cache
+ */
+ EntityCache l2Cache();
+
+ /**
+ * Map the ResultSet to the given entity and the entity to the persistence context
+ *
+ * @param pEntity The entity
+ * @param vResultSet the {@link ResultSet}
+ * @return The entity
+ */
+ X mapResultSet(@Nonnull X pEntity, ResultSet vResultSet);
+
+ /**
+ * Map the ResultSet to the given entity and the entity to the persistence context
+ *
+ * @param pEntity The entity
+ * @param pColPrefix Only used column from the result set that starts with pColPrefix
+ * @param pResultSet the {@link ResultSet}
+ * @return The entity
+ */
+ X mapResultSet(@Nonnull X pEntity, String pColPrefix, ResultSet pResultSet);
+
+ /**
+ * Synchronize the persistence context to the underlying database.
+ *
+ * @throws TransactionRequiredException if there is no transaction or if the entity manager has not been joined to
+ * the current transaction
+ * @throws PersistenceException if the flush fails
+ */
+ void flush();
+
+ /**
+ * Synchronize the entity to the underlying database.
+ *
+ * @throws TransactionRequiredException if there is no transaction or if the entity manager has not been joined to
+ * the current transaction
+ * @throws PersistenceException if the flush fails
+ * @throws IllegalStateException if the connection is not open
+ */
+ void flushEntity(@Nonnull JPAEntity pEntity);
+
+ /**
+ * Synchronize the all entity of a given type belonging to the persistence context to the underlying database.
+ *
+ * @throws TransactionRequiredException if there is no transaction or if the entity manager has not been joined to
+ * the current transaction
+ * @throws PersistenceException if the flush fails
+ * @throws IllegalStateException if the connection is not open
+ */
+ void flushOnType(Class> pEntityClass);
+
+ /**
+ * Return the resource-level EntityTransaction
object. The EntityTransaction
instance may
+ * be used serially to begin and commit multiple transactions.
+ *
+ * @return EntityTransaction instance
+ * @throws IllegalStateException if invoked on a JTA entity manager
+ */
+ EntityTransaction getTransaction();
+
+ /**
+ * Return an object of the specified type to allow access to the provider-specific API.
+ *
+ * @param cls the class of the object to be returned.
+ * @return an instance of the specified class
+ * @throws IllegalArgumentException if the cls is not unwrapped
+ */
+ T unwrap(Class cls);
+
+ /**
+ * Enable the context to detect and join a JTA transaction
+ */
+ void setAutoJoinTransaction();
+
+ /**
+ * return the current configured JTA status
+ *
+ * @return {@link SynchronizationType}
+ */
+ boolean isAutoJoinTransaction();
+
+ /**
+ * Indicate to the persistence context that a JTA transaction is active and join the persistence context to it.
+ * This method should be called on a JTA application managed entity manager that was created outside the scope
+ * of the active transaction or on an entity manager of type
+ * SynchronizationType.UNSYNCHRONIZED
to associate it with the current JTA transaction.
+ *
+ * @throws TransactionRequiredException if there is no transaction
+ */
+ void joinTransaction();
+
+ /**
+ * Determine whether the persistence context is joined to the current transaction. Returns false if the entity
+ * manager is not joined to the current transaction or if no transaction is active
+ *
+ * @return boolean
+ */
+ boolean isJoinedToTransaction();
+
+ /**
+ * Set a persistance context property or hint. If a vendor-specific property or hint is not recognized, it is
+ * silently ignored.
+ *
+ * @param pName name of property or hint
+ * @param pValue value for property or hint
+ * @throws IllegalArgumentException if the second argument is not valid for the implementation
+ */
+ void setProperty(String pName, Object pValue);
+
+ /**
+ * Get the properties and hints and associated values that are in effect for the persistence context. Changing the
+ * contents of the map does not change the configuration in effect.
+ *
+ * @return map of properties and hints in effect for persistence context
+ */
+ Map getProperties();
+
+ /**
+ * This method is called by the transaction
+ * manager after the transaction is committed or rolled back.
+ *
+ * @param status The status of the transaction completion.
+ */
+ void afterCompletion(int status);
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/PersistenceUnitProvider.java b/jpalite-core/src/main/java/io/jpalite/PersistenceUnitProvider.java
new file mode 100644
index 0000000..bc015c0
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/PersistenceUnitProvider.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+
+public interface PersistenceUnitProvider
+{
+ JPALitePersistenceUnit getPersistenceUnit(String pPersistenceUnitName);
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/QueryParsingException.java b/jpalite-core/src/main/java/io/jpalite/QueryParsingException.java
new file mode 100644
index 0000000..d61a7c0
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/QueryParsingException.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+import jakarta.persistence.PersistenceException;
+
+public class QueryParsingException extends PersistenceException
+{
+ public QueryParsingException()
+ {
+ super();
+ }
+
+ public QueryParsingException(String message)
+ {
+ super(message);
+ }
+
+ public QueryParsingException(String message, Throwable cause)
+ {
+ super(message, cause);
+ }
+
+ public QueryParsingException(Throwable cause)
+ {
+ super(cause);
+ }
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/TradeSwitchConvert.java b/jpalite-core/src/main/java/io/jpalite/TradeSwitchConvert.java
new file mode 100644
index 0000000..585889a
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/TradeSwitchConvert.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+import jakarta.persistence.AttributeConverter;
+import org.infinispan.protostream.GeneratedSchema;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+public interface TradeSwitchConvert extends AttributeConverter
+{
+ String getFieldType();
+
+ String prototypeLib();
+
+ GeneratedSchema getSchema();
+
+ default X convertToEntityAttribute(ResultSet pResultSet, int pColumn) throws SQLException
+ {
+ return convertToEntityAttribute((Y) pResultSet.getObject(pColumn));
+ }
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/UnknownFieldException.java b/jpalite-core/src/main/java/io/jpalite/UnknownFieldException.java
new file mode 100644
index 0000000..daec24a
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/UnknownFieldException.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite;
+
+import jakarta.persistence.PersistenceException;
+
+import java.io.Serial;
+
+public class UnknownFieldException extends PersistenceException
+{
+ @Serial
+ private static final long serialVersionUID = -7640891001823058796L;
+
+ public UnknownFieldException(String message)
+ {
+ super(message);
+ }
+
+ public UnknownFieldException(String message, Throwable cause)
+ {
+ super(message, cause);
+ }
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/ConverterClassImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/ConverterClassImpl.java
new file mode 100644
index 0000000..d81fab2
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/ConverterClassImpl.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl;
+
+import io.jpalite.ConverterClass;
+import io.jpalite.TradeSwitchConvert;
+import jakarta.persistence.Converter;
+import jakarta.persistence.PersistenceException;
+import lombok.Data;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+@Data
+public class ConverterClassImpl implements ConverterClass
+{
+ private static final Logger LOG = LoggerFactory.getLogger(ConverterClassImpl.class);
+ private Class> convertClass;
+ private boolean autoApply;
+ private TradeSwitchConvert, ?> converter;
+ private Class> attributeType;
+ private Class> dbType;
+
+ public ConverterClassImpl(Class> convertClass)
+ {
+ this.convertClass = convertClass;
+ Converter converter = this.convertClass.getAnnotation(Converter.class);
+ if (converter == null) {
+ LOG.warn("Missing @Converter annotation on {}", this.convertClass.getSimpleName());
+ autoApply = false;
+ }//if
+ else {
+ autoApply = converter.autoApply();
+ }//else
+
+ for (Method method : this.convertClass.getDeclaredMethods()) {
+ //Not a SYNTHETIC (generated method)
+ if (method.getName().equals("convertToDatabaseColumn") &&
+ ((method.getModifiers() & 0x00001000) != 0x00001000) &&
+ method.getParameterTypes().length == 1) {
+ attributeType = method.getParameterTypes()[0];
+ dbType = method.getReturnType();
+ break;
+ }//if
+ }//for
+ if (attributeType == null) {
+ LOG.warn("Error detecting the attribute type in {}", this.convertClass.getSimpleName());
+ attributeType = Object.class;
+ dbType = Object.class;
+ }//if
+
+ try {
+ this.converter = (TradeSwitchConvert, ?>) this.convertClass.getConstructor().newInstance();
+ }//try
+ catch (InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException ex) {
+ throw new PersistenceException(this.convertClass.getSimpleName() + " failed to instantiate", ex);
+ }//catch
+ }//ConverterClass
+
+ @Override
+ public String getName()
+ {
+ return convertClass.getCanonicalName();
+ }
+}//ConverterClass
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/CustomPersistenceUnit.java b/jpalite-core/src/main/java/io/jpalite/impl/CustomPersistenceUnit.java
new file mode 100644
index 0000000..6d8dfea
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/CustomPersistenceUnit.java
@@ -0,0 +1,189 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl;
+
+import io.jpalite.JPALitePersistenceUnit;
+import io.jpalite.impl.providers.JPALitePersistenceProviderImpl;
+import jakarta.persistence.SharedCacheMode;
+import jakarta.persistence.ValidationMode;
+import jakarta.persistence.spi.ClassTransformer;
+import jakarta.persistence.spi.PersistenceUnitTransactionType;
+import lombok.Setter;
+import org.infinispan.commons.configuration.BasicConfiguration;
+
+import javax.sql.DataSource;
+import java.net.URL;
+import java.util.Collections;
+import java.util.List;
+import java.util.Properties;
+
+@Setter
+public class CustomPersistenceUnit implements JPALitePersistenceUnit
+{
+ private final String persistenceUnitName;
+ private final Properties properties;
+
+ private Boolean multiTenantMode = false;
+ private String dataSourceName;
+ private String cacheName;
+ private String cacheProvider;
+ private BasicConfiguration cacheConfig;
+ private String providerClass = JPALitePersistenceProviderImpl.class.getName();
+ private PersistenceUnitTransactionType transactionType = PersistenceUnitTransactionType.RESOURCE_LOCAL;
+ private ValidationMode validationMode = ValidationMode.NONE;
+ private SharedCacheMode sharedCacheMode = SharedCacheMode.ENABLE_SELECTIVE;
+
+ public CustomPersistenceUnit(String persistenceUnitName)
+ {
+ this.persistenceUnitName = persistenceUnitName;
+ properties = new Properties();
+ }
+
+ @Override
+ public String getDataSourceName()
+ {
+ return dataSourceName;
+ }
+
+ @Override
+ public String getCacheName()
+ {
+ return cacheName;
+ }
+
+ @Override
+ public String getCacheProvider()
+ {
+ return cacheProvider;
+ }
+
+ @Override
+ public BasicConfiguration getCacheConfig()
+ {
+ return cacheConfig;
+ }
+
+ @Override
+ public Boolean getMultiTenantMode()
+ {
+ return multiTenantMode;
+ }
+
+ @Override
+ public String getPersistenceUnitName()
+ {
+ return persistenceUnitName;
+ }
+
+ @Override
+ public String getPersistenceProviderClassName()
+ {
+ return providerClass;
+ }
+
+ @Override
+ public PersistenceUnitTransactionType getTransactionType()
+ {
+ return transactionType;
+ }
+
+ @Override
+ public DataSource getJtaDataSource()
+ {
+ return null;
+ }
+
+ @Override
+ public DataSource getNonJtaDataSource()
+ {
+ return null;
+ }
+
+ @Override
+ public List getMappingFileNames()
+ {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List getJarFileUrls()
+ {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public URL getPersistenceUnitRootUrl()
+ {
+ throw new UnsupportedOperationException("Method not supported");
+ }
+
+ @Override
+ public List getManagedClassNames()
+ {
+ //All classes are managed
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean excludeUnlistedClasses()
+ {
+ return false;
+ }
+
+ @Override
+ public SharedCacheMode getSharedCacheMode()
+ {
+ return sharedCacheMode;
+ }
+
+ @Override
+ public ValidationMode getValidationMode()
+ {
+ return validationMode;
+ }
+
+ @Override
+ public Properties getProperties()
+ {
+ return properties;
+ }
+
+ @Override
+ public String getPersistenceXMLSchemaVersion()
+ {
+ return "3.0";
+ }
+
+ @Override
+ public ClassLoader getClassLoader()
+ {
+ return null;
+ }
+
+ @Override
+ public void addTransformer(ClassTransformer transformer)
+ {
+ //Silently ignore this command
+ }
+
+ @Override
+ public ClassLoader getNewTempClassLoader()
+ {
+ return null;
+ }
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/EntityFieldImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/EntityFieldImpl.java
new file mode 100644
index 0000000..e89c451
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/EntityFieldImpl.java
@@ -0,0 +1,466 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl;
+
+import io.jpalite.*;
+import jakarta.persistence.*;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.graalvm.nativeimage.ImageInfo;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import static jakarta.persistence.GenerationType.AUTO;
+import static jakarta.persistence.GenerationType.SEQUENCE;
+
+@Data
+@Slf4j
+@SuppressWarnings({"unchecked", "java:S3740"})//Cannot use generics here
+public class EntityFieldImpl implements EntityField
+{
+ private static final boolean NATIVE_IMAGE = ImageInfo.inImageCode();
+ /**
+ * The entity class
+ *
+ * @return the entity class
+ */
+ private Class> enityClass;
+ /**
+ * Identifier of the entity
+ *
+ * @param name changes the name of the entity
+ * @return name of the client
+ */
+ private final String name;
+ /**
+ * A unique field number assigned to the field.
+ *
+ * @return the field number
+ */
+ private final int fieldNr;
+ /**
+ * The java class of the field
+ *
+ * @return the java class
+ */
+ private Class> type;
+ /**
+ * The type of field as per {@link FieldType}
+ *
+ * @return The field type
+ */
+ private FieldType fieldType;
+ /**
+ * The SQL column linked to the field
+ *
+ * @return The SQL Column name
+ */
+ private String column;
+ /**
+ * The mapping type specified by the field. See {@link MappingType}.
+ *
+ * @return The {@link MappingType}
+ */
+ private MappingType mappingType;
+ /**
+ * True if the field is to be unique in the table
+ *
+ * @return the unique setting
+ */
+ private boolean unique;
+ /**
+ * True of the field can be null
+ *
+ * @return the nullable setting
+ */
+ private boolean nullable;
+ /**
+ * True if the field is insertable
+ *
+ * @return the insertable setting
+ */
+ private boolean insertable;
+ /**
+ * True if the field is updatable.
+ *
+ * @return the updatable setting
+ */
+ private boolean updatable;
+ /**
+ * True if the field is an ID Field
+ *
+ * @return the idField setting
+ */
+ private boolean idField;
+ /**
+ * True if the field is a Version Field
+ *
+ * @return the version field setting
+ */
+ private boolean versionField;
+ /**
+ * The getter for the field
+ *
+ * @return the setter method for the field
+ */
+ private MethodHandle getter;
+ /**
+ * The getter reflection method for the field
+ */
+ private Method getterMethod;
+ /**
+ * The setter for the field
+ */
+ private MethodHandle setter;
+ /**
+ * The setter reflection method for the field
+ */
+ private Method setterMethod;
+ /**
+ * The {@link CascadeType} assigned to the field.
+ *
+ * @return the {@link CascadeType} setting
+ */
+ private Set cascade;
+ /**
+ * The {@link FetchType} assigned to the field.
+ *
+ * @return the {@link FetchType} setting
+ */
+ private FetchType fetchType;
+ /**
+ * Only applicable to non-Basic fields and indicates that the field is linked the field specified in mappedBy in the
+ * entity represented by the field.
+ *
+ * @return the mappedBy setting
+ */
+ private String mappedBy;
+ /**
+ * The columnDefinition value defined in the JoinColumn annotation linked to the field
+ *
+ * @return the columnDefinition setting
+ */
+ private String columnDefinition;
+ /**
+ * The table value defined in the JoinColumn annotation linked to the field
+ *
+ * @return the table setting
+ */
+ private String table;
+ /**
+ * The converter class used to convert the field to a SQL type
+ */
+ private TradeSwitchConvert converterClass;
+
+ /**
+ * Create a new entity field definition
+ *
+ * @param field The field
+ * @param fieldNr The field number
+ */
+ public EntityFieldImpl(Class> enitityClass, Field field, int fieldNr)
+ {
+ type = field.getType();
+ if (!Map.class.isAssignableFrom(type) && field.getGenericType() instanceof ParameterizedType) {
+ type = (Class>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0];
+ }//if
+
+ enityClass = enitityClass;
+ name = field.getName();
+ this.fieldNr = fieldNr;
+ fieldType = FieldType.fieldType(type);
+ mappingType = MappingType.BASIC;
+ unique = false;
+ nullable = true;
+ insertable = true;
+ updatable = true;
+ fetchType = FetchType.EAGER;
+ cascade = new HashSet<>();
+ mappedBy = null;
+ columnDefinition = null;
+ table = null;
+ idField = false;
+ versionField = false;
+
+ checkForConvert(field);
+ processMappingType(field);
+ findGetterSetter(field);
+ }//EntityField
+
+ private void findGetterSetter(Field field)
+ {
+ String vMethod = field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
+ MethodHandles.Lookup lookup = MethodHandles.lookup();
+ String reflectionMethod = null;
+ try {
+ reflectionMethod = "set" + vMethod;
+ setterMethod = enityClass.getMethod(reflectionMethod, field.getType());
+ setter = lookup.unreflect(setterMethod);
+
+ reflectionMethod = ((field.getType() == Boolean.class || field.getType() == boolean.class) ? "is" : "get") + vMethod;
+ getterMethod = enityClass.getMethod(reflectionMethod);
+ getter = lookup.unreflect(getterMethod);
+ }//try
+ catch (IllegalAccessException | NoSuchMethodException | SecurityException ex) {
+ /*
+ * Special case for Boolean that could be either isXXX or
+ * getXXXX
+ */
+ if (field.getType() == Boolean.class || field.getType() == boolean.class) {
+ try {
+ reflectionMethod = "get" + vMethod;
+ getterMethod = enityClass.getMethod(reflectionMethod);
+ getter = lookup.unreflect(getterMethod);
+ }//try
+ catch (IllegalAccessException | NoSuchMethodException | SecurityException ex1) {
+ throw new IllegalCallerException(String.format("Error finding %s::%s", enityClass.getSimpleName(), reflectionMethod), ex);
+ }//catch
+ }//if
+ else {
+ throw new IllegalCallerException(String.format("Error finding %s::%s", enityClass.getSimpleName(), reflectionMethod), ex);
+ }//else
+ }//catch
+ }//findGetterSetter
+
+ private void processMappingType(Field pField)
+ {
+ if (checkEmbeddedField(pField) || checkOneToOneField(pField) ||
+ checkOneToManyField(pField) || checkManyToOneField(pField) ||
+ checkManyToManyField(pField)) {
+ JoinColumn joinColumn = pField.getAnnotation(JoinColumn.class);
+ if (joinColumn != null) {
+ setInsertable(joinColumn.insertable());
+ setNullable(joinColumn.nullable());
+ setUnique(joinColumn.unique());
+ setUpdatable(joinColumn.updatable());
+ setColumn(joinColumn.name());
+ }//if
+ }//if
+ else {
+ prosesBasicField(pField);
+ }//if
+ }//processMappingType
+
+ private void prosesBasicField(Field field)
+ {
+ Basic basic = field.getAnnotation(Basic.class);
+ if (basic != null) {
+ if (getFieldType() == FieldType.TYPE_ENTITY) {
+ throw new PersistenceException(enityClass.getName() + "::" + getName() + " is referencing an Entity type and cannot be annotated with @Basic.");
+ }//if
+ setFetchType(basic.fetch());
+ setNullable(basic.optional());
+ }//if
+
+ Enumerated enumField = field.getAnnotation(Enumerated.class);
+ if (enumField != null || getType().isEnum()) {
+ if (enumField == null) {
+ LOG.warn("{}: Field '{}' is not annotated as an enum, assuming it to be one - Developers must fix this", enityClass.getName(), field.getName());
+ setFieldType(FieldType.TYPE_ENUM);
+ }//if
+ else {
+ if (getFieldType() == FieldType.TYPE_ENTITY) {
+ throw new PersistenceException(enityClass.getName() + "::" + getName() + " is referencing an Entity type and cannot be annotated with @Enumerated.");
+ }//if
+
+ setFieldType(enumField.value() == EnumType.ORDINAL ? FieldType.TYPE_ORDINAL_ENUM : FieldType.TYPE_ENUM);
+ }//if
+ }//if
+
+ Column col = field.getAnnotation(Column.class);
+ if (col != null) {
+ setColumn(col.name());
+ setInsertable(col.insertable());
+ setNullable(col.nullable());
+ setUnique(col.unique());
+ setUpdatable(col.updatable());
+ setTable(col.table());
+ setColumnDefinition(col.columnDefinition());
+ }//if
+
+ setIdField((field.getAnnotation(Id.class) != null));
+ if (isIdField()) {
+ GeneratedValue generatedValue = field.getAnnotation(GeneratedValue.class);
+ if (generatedValue != null) {
+ if (generatedValue.strategy() != AUTO && generatedValue.strategy() != SEQUENCE) {
+ throw new PersistenceException(enityClass.getName() + "::" + getName() + "@GeneratedValue is not AUTO or SEQUENCE");
+ }//if
+ insertable = false;
+ updatable = false;
+ }//ifated
+ nullable = false;
+ }//if
+
+ setVersionField(field.getAnnotation(Version.class) != null);
+ }//prosesBasicField
+
+ private boolean checkEmbeddedField(Field field)
+ {
+ Embedded embedded = field.getAnnotation(Embedded.class);
+ if (embedded != null) {
+ if (getFieldType() != FieldType.TYPE_ENTITY) {
+ throw new PersistenceException(enityClass.getName() + "::" + getName() + " is NOT referencing an Entity type and cannot NOT be annotated with @Embedded.");
+ }//if
+
+ setMappingType(MappingType.EMBEDDED);
+ return true;
+ }//if
+ return false;
+ }//checkEmbeddedField
+
+ private boolean checkOneToOneField(Field field)
+ {
+ OneToOne oneToOne = field.getAnnotation(OneToOne.class);
+ if (oneToOne != null) {
+ if (getFieldType() != FieldType.TYPE_ENTITY) {
+ throw new PersistenceException(enityClass.getName() + "::" + getName() + " is NOT referencing an Entity type and cannot NOT be annotated with @OneToOne.");
+ }//if
+ setMappingType(MappingType.ONE_TO_ONE);
+ setFetchType(oneToOne.fetch());
+ setCascade(new HashSet<>(Arrays.asList(oneToOne.cascade())));
+ setMappedBy(oneToOne.mappedBy());
+ return true;
+ }//if
+ return false;
+ }//checkOneToOneField
+
+ private boolean checkOneToManyField(Field field)
+ {
+ OneToMany oneToMany = field.getAnnotation(OneToMany.class);
+ if (oneToMany != null) {
+ if (getFieldType() != FieldType.TYPE_ENTITY) {
+ throw new PersistenceException(enityClass.getName() + "::" + getName() + " is NOT referencing an Entity type and cannot NOT be annotated with @OneToMany.");
+ }//if
+
+ setMappingType(MappingType.ONE_TO_MANY);
+ setFetchType(oneToMany.fetch());
+ setCascade(new HashSet<>(Arrays.asList(oneToMany.cascade())));
+ setMappedBy(oneToMany.mappedBy());
+ return true;
+ }//if
+ return false;
+ }//checkOneToManyField
+
+ private boolean checkManyToOneField(Field field)
+ {
+ ManyToOne manyToOne = field.getAnnotation(ManyToOne.class);
+ if (manyToOne != null) {
+ if (getFieldType() != FieldType.TYPE_ENTITY) {
+ throw new PersistenceException(enityClass.getName() + "::" + getName() + " is NOT referencing an Entity type and cannot NOT be annotated with @ManyToOne.");
+ }//if
+
+ setMappingType(MappingType.MANY_TO_ONE);
+ setFetchType(manyToOne.fetch());
+ setCascade(new HashSet<>(Arrays.asList(manyToOne.cascade())));
+ return true;
+ }//if
+ return false;
+ }//checkManyToOneField
+
+ private boolean checkManyToManyField(Field field)
+ {
+ ManyToMany manyToMany = field.getAnnotation(ManyToMany.class);
+ if (manyToMany != null) {
+ if (getFieldType() != FieldType.TYPE_ENTITY) {
+ throw new PersistenceException(enityClass.getName() + "::" + getName() + " is NOT referencing an Entity type and cannot NOT be annotated with @ManyToMany.");
+ }//if
+
+ setMappingType(MappingType.MANY_TO_MANY);
+ setFetchType(manyToMany.fetch());
+ setCascade(new HashSet<>(Arrays.asList(manyToMany.cascade())));
+ setMappedBy(manyToMany.mappedBy());
+ return true;
+ }//if
+ return false;
+ }//checkManyToManyField
+
+ private void checkForConvert(Field field)
+ {
+ Convert customType = field.getAnnotation(Convert.class);
+ if (customType != null) {
+ try {
+ //Check if the converter class was explicitly overridden
+ if (customType.converter() != null) {
+ fieldType = FieldType.TYPE_CUSTOMTYPE;
+ converterClass = (TradeSwitchConvert) customType.converter().getConstructor().newInstance();
+ return;
+ }//if
+ }//try
+ catch (InvocationTargetException | InstantiationException | IllegalAccessException |
+ NoSuchMethodException ex) {
+ throw new IllegalArgumentException(getName() + "::" + field.getName() + " failed to instantiate the referenced converter", ex);
+ }//catch
+
+ //If conversion is not required, exit here
+ if (customType.disableConversion()) {
+ return;
+ }//if
+ }//if
+
+ ConverterClass converterClass = EntityMetaDataManager.getConvertClass(type);
+ if (converterClass != null) {
+ fieldType = FieldType.TYPE_CUSTOMTYPE;
+ this.converterClass = converterClass.getConverter();
+ }//if
+ }//checkForConvert
+
+ @Override
+ public Object invokeGetter(Object entity)
+ {
+ try {
+ if (getter == null) {
+ throw new PersistenceException("No getter method found for " + enityClass.getName() + "::" + getName());
+ }//if
+
+ return NATIVE_IMAGE ? getterMethod.invoke(entity) : getter.invoke(entity);
+ }//try
+ catch (Throwable ex) {
+ throw new PersistenceException("Failed to invoke getter for " + enityClass.getName() + "::" + getName(), ex);
+ }//catch
+ }//invokeGetter
+
+
+ @Override
+ public void invokeSetter(Object entity, Object value)
+ {
+ try {
+ if (setter == null) {
+ throw new PersistenceException("No setter method found for " + enityClass.getName() + "::" + getName());
+ }//if
+
+ if (NATIVE_IMAGE) {
+ setterMethod.invoke(entity, value);
+ }//if
+ else {
+ setter.invoke(entity, value);
+ }//else
+ }//try
+ catch (Throwable ex) {
+ throw new PersistenceException("Failed to invoke setter for " + enityClass.getName() + "::" + getName(), ex);
+ }//catch
+ }//invokeSetter
+}//EntityFieldImpl
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/EntityL1LocalCacheImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/EntityL1LocalCacheImpl.java
new file mode 100644
index 0000000..ef13d26
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/EntityL1LocalCacheImpl.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl;
+
+import io.jpalite.EntityLocalCache;
+import io.jpalite.EntityState;
+import io.jpalite.JPAEntity;
+import io.jpalite.PersistenceContext;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+@Slf4j
+public class EntityL1LocalCacheImpl implements EntityLocalCache
+{
+ private final PersistenceContext persistenceContext;
+
+ /**
+ * All the entity attached to this persistence context
+ */
+ private final List cache = new ArrayList<>();
+
+ public EntityL1LocalCacheImpl(PersistenceContext pPersistenceContext)
+ {
+ persistenceContext = pPersistenceContext;
+ LOG.trace("Creating L1 cache for {}", pPersistenceContext);
+ }//EntityCacheImpl
+
+ @Override
+ public void clear()
+ {
+ cache.forEach(e -> e._setEntityState(EntityState.DETACHED));
+ cache.clear();
+ LOG.trace("Clearing L1 cache for {}", persistenceContext);
+ }//clear
+
+ @Override
+ public T find(Class entityType, Object primaryKey)
+ {
+ return find(entityType, primaryKey, false);
+ }
+
+ @SuppressWarnings("unchecked") //We are doing a valid casting
+ public T find(Class entityType, Object primaryKey, boolean checkIfRemoved)
+ {
+ if (persistenceContext.isActive() && primaryKey != null) {
+ JPAEntity entity = cache.stream()
+ .filter(e -> e._getMetaData().getName().equals(entityType.getName()))
+ .filter(e -> primaryKey.equals(e._getField(e._getMetaData().getIdField().getName())))
+ .findFirst()
+ .orElse(null);
+ if (entity != null) {
+ if (entity._getEntityState() == EntityState.REMOVED) {
+ if (checkIfRemoved) {
+ throw new IllegalArgumentException("Entity has been removed");
+ }//if
+ return null;
+ }//if
+
+ return (T) entity;
+ }//if
+ }//if
+
+ return null;
+ }//find
+
+ @Override
+ @SuppressWarnings("unchecked") //We are doing a valid casting
+ public void foreachType(Class entityType, Consumer action)
+ {
+ if (persistenceContext.isActive()) {
+ cache.stream()
+ .filter(e -> e._getMetaData().getName().equals(entityType.getName()))
+ .forEach(e -> action.accept((T) e));
+ }//if
+ }//foreachType
+
+ @Override
+ public void manage(JPAEntity entity)
+ {
+ entity._setPersistenceContext(persistenceContext);
+
+ //We only manage entities if we are in a transaction and if the entity type is supported by the persistence context
+ if (persistenceContext.isActive() && persistenceContext.supportedEntityType(entity._getMetaData().getEntityType())) {
+ cache.add(entity);
+ entity._setEntityState(EntityState.MANAGED);
+ LOG.trace("Adding Entity to L1 cache. Context [{}], Entity [{}]", persistenceContext, entity);
+ }//if
+ else {
+ entity._setEntityState(EntityState.DETACHED);
+ }//else
+
+ }//manage
+
+ @Override
+ public void detach(JPAEntity entity)
+ {
+ for (JPAEntity cachedEntity : cache) {
+ if (cachedEntity == entity) {
+ LOG.trace("Removing Entity from L1 cache. Context [{}], Entity [{}]", persistenceContext, entity);
+ cache.remove(cachedEntity);
+ break;
+ }//if
+ }//for
+
+ LOG.trace("Detaching Entity from L1 cache. Entity [{}]", entity);
+ if (entity._getEntityState() != EntityState.TRANSIENT) {
+ entity._setEntityState(EntityState.DETACHED);
+ }//if
+ }//remove
+
+ @Override
+ public boolean contains(JPAEntity entity)
+ {
+ return (entity._getEntityState() == EntityState.MANAGED && persistenceContext == entity._getPersistenceContext());
+ }
+
+ @Override
+ public void foreach(Consumer action)
+ {
+ cache.forEach(action);
+ }
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/EntityL2CacheImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/EntityL2CacheImpl.java
new file mode 100644
index 0000000..0020492
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/EntityL2CacheImpl.java
@@ -0,0 +1,443 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl;
+
+import io.jpalite.*;
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.context.Scope;
+import io.quarkus.arc.Arc;
+import io.quarkus.arc.InstanceHandle;
+import io.quarkus.infinispan.client.runtime.InfinispanClientProducer;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import jakarta.persistence.SharedCacheMode;
+import jakarta.transaction.*;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.infinispan.client.hotrod.RemoteCache;
+import org.infinispan.client.hotrod.RemoteCacheManager;
+import org.infinispan.client.hotrod.exceptions.HotRodClientException;
+import org.infinispan.commons.api.query.Query;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+@SuppressWarnings("java:S3740")//Have to work without generics
+@Slf4j
+public class EntityL2CacheImpl implements EntityCache
+{
+ private static final Tracer TRACER = GlobalOpenTelemetry.get().getTracer(EntityL2CacheImpl.class.getName());
+ public static final String NO_TRANSACTION_ACTIVE = "No Transaction active";
+ public static final String ENTITY_ATTR = "entity";
+ private RemoteCacheManager remoteCacheManager;
+ private final JPALitePersistenceUnit persistenceUnit;
+ private static final boolean CACHING_ENABLED = JPAConfig.getValue("tradeswitch.persistence.l2cache", true);
+ private boolean inTransaction;
+ private final List batchQueue = new ArrayList<>();
+
+ private static final int ACTION_ADD = 0;
+ private static final int ACTION_UPDATE = 1;
+ private static final int ACTION_REMOVE = 2;
+
+ @Getter
+ public static class CacheEntry
+ {
+ private final int action;
+
+ private final String key;
+ private final Object value;
+ private final long lifespan;
+ private final TimeUnit lifespanUnit;
+ private final long maxIdleTime;
+ private final TimeUnit maxIdleTimeUnit;
+
+ public CacheEntry(int action, String key, Object value, long lifespan, TimeUnit lifespanUnit, long maxIdleTime, TimeUnit maxIdleTimeUnit)
+ {
+ this.action = action;
+ this.key = key;
+ this.value = value;
+ this.lifespan = lifespan;
+ this.lifespanUnit = lifespanUnit;
+ this.maxIdleTime = maxIdleTime;
+ this.maxIdleTimeUnit = maxIdleTimeUnit;
+ }
+ }
+
+ public EntityL2CacheImpl(JPALitePersistenceUnit persistenceUnit)
+ {
+ this.persistenceUnit = persistenceUnit;
+ remoteCacheManager = null;
+ inTransaction = false;
+ }//EntityCacheImpl
+
+ @Nullable
+ @SuppressWarnings("java:S1168") // Null is expected and indicates that caching is not enabled
+ private RemoteCache getCache()
+ {
+ if (CACHING_ENABLED && !persistenceUnit.getSharedCacheMode().equals(SharedCacheMode.NONE)) {
+ if (remoteCacheManager == null) {
+ InstanceHandle infinispanClientProducer = Arc.container().instance(InfinispanClientProducer.class);
+ if (infinispanClientProducer.isAvailable()) {
+ remoteCacheManager = infinispanClientProducer.get().getNamedRemoteCacheManager(persistenceUnit.getCacheProvider());
+ }//if
+ if (remoteCacheManager == null || !remoteCacheManager.isStarted()) {
+ remoteCacheManager = null;
+ return null;
+ }//if
+ }//if
+
+ RemoteCache cache = remoteCacheManager.getCache(persistenceUnit.getCacheName());
+ if (cache == null) {
+ cache = remoteCacheManager.administration().getOrCreateCache(persistenceUnit.getCacheName(), persistenceUnit.getCacheConfig());
+ }//if
+ return cache;
+ }
+
+ return null;
+ }
+
+ private String makeCacheKey(Class> entityClass, Object key)
+ {
+ return entityClass.getSimpleName() +
+ ":P:" +
+ (key == null ? "" : key.toString());
+ }//makeCacheKey
+
+ private void checkEntityType(Class entityType)
+ {
+ EntityMetaData metaData = EntityMetaDataManager.getMetaData(entityType);
+ if (!metaData.isCacheable()) {
+ throw new IllegalArgumentException("Entity [" + entityType.getName() + "] is not cacheable.");
+ }//if
+ }//checkEntityType
+
+ private void checkEntityInstance(JPAEntity entity)
+ {
+ if (!entity._getMetaData().isCacheable()) {
+ throw new IllegalArgumentException("Entity [" + entity.getClass().getName() + "] is not cacheable.");
+ }//if
+ }//entity
+
+ public T find(Class entityType, Object primaryKey)
+ {
+ Span span = TRACER.spanBuilder("EntityL2CacheImpl::find").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ checkEntityType(entityType);
+
+ long start = System.currentTimeMillis();
+ RemoteCache cache = getCache();
+ if (cache != null) {
+ String key = makeCacheKey(entityType, primaryKey);
+ span.setAttribute("key", key);
+ span.setAttribute(ENTITY_ATTR, entityType.getName());
+ T entityObject = cache.get(key);
+ if (entityObject != null) {
+ LOG.debug("Searching L2 cache for key [{}] - Hit in {}ms", key, System.currentTimeMillis() - start);
+ return entityObject;
+ }//if
+ LOG.debug("Searching L2 cache for key [{}] - Missed in {}ms", key, System.currentTimeMillis() - start);
+ }//if
+ }//try
+ finally {
+ span.end();
+ }//finally
+
+ return null;
+ }//find
+
+ @Override
+ @Nonnull
+ public List search(Class entityType, String query)
+ {
+ Span span = TRACER.spanBuilder("EntityL2CacheImpl::search").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ checkEntityType(entityType);
+ RemoteCache cache = getCache();
+ if (cache != null) {
+ String queryText = "from org.tradeswitch." + entityType.getSimpleName() + " " + query;
+ try {
+ span.setAttribute("query", queryText);
+ span.setAttribute(ENTITY_ATTR, entityType.getName());
+
+ LOG.debug("Querying L2 cache : {}", queryText);
+ Query q = cache.query(queryText);
+ List result = q.execute().list();
+ LOG.debug("Querying L2 cache - Found {} records", result.size());
+ return result;
+ }//try
+ catch (HotRodClientException ex) {
+ LOG.debug("Search error:{}", ex.getMessage(), ex);
+ }//catch
+ }//if
+ return Collections.emptyList();
+ }//try
+ finally {
+ span.end();
+ }//finally
+ }//search
+
+ @Override
+ public void update(JPAEntity entity)
+ {
+ checkEntityInstance(entity);
+
+ if (CACHING_ENABLED && inTransaction) {
+ RemoteCache cache = getCache();
+ if (cache != null) {
+ String key = makeCacheKey(entity.getClass(), entity._getPrimaryKey());
+ batchQueue.add(new CacheEntry(ACTION_UPDATE, key, entity, -1, TimeUnit.SECONDS, entity._getMetaData().getIdleTime(), entity._getMetaData().getCacheTimeUnit()));
+ batchQueue.add(new CacheEntry(ACTION_ADD, entity.getClass().getName(), System.currentTimeMillis(), -1, TimeUnit.SECONDS, -1, TimeUnit.SECONDS));
+ }//if
+ }//if
+ }//update
+
+ @Override
+ public void add(JPAEntity entity)
+ {
+ checkEntityInstance(entity);
+
+ Span span = TRACER.spanBuilder("EntityL2CacheImpl::add").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ if (CACHING_ENABLED) {
+ long start = System.currentTimeMillis();
+ RemoteCache cache = getCache();
+ if (cache != null) {
+ String key = makeCacheKey(entity.getClass(), entity._getPrimaryKey());
+ span.setAttribute("key", key);
+ span.setAttribute(ENTITY_ATTR, entity._getMetaData().getName());
+
+ cache.put(key, entity, -1, TimeUnit.SECONDS, entity._getMetaData().getIdleTime(), entity._getMetaData().getCacheTimeUnit());
+ LOG.debug("Adding/Replacing Entity with key [{}] in L2 cache in {}ms", key, System.currentTimeMillis() - start);
+ }//if
+ }//else
+ }//try
+ finally {
+ span.end();
+ }
+ }//add
+
+ @Override
+ public void remove(JPAEntity entity)
+ {
+ Span span = TRACER.spanBuilder("EntityL2CacheImpl::remove").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ checkEntityInstance(entity);
+
+ if (CACHING_ENABLED) {
+ long start = System.currentTimeMillis();
+ RemoteCache cache = getCache();
+ if (cache != null) {
+ String key = makeCacheKey(entity.getClass(), entity._getPrimaryKey());
+ span.setAttribute("key", key);
+ span.setAttribute(ENTITY_ATTR, entity._getMetaData().getName());
+ if (inTransaction) {
+ batchQueue.add(new CacheEntry(ACTION_REMOVE, key, entity, -1, TimeUnit.SECONDS, -1, TimeUnit.SECONDS));
+ batchQueue.add(new CacheEntry(ACTION_ADD, entity.getClass().getName(), System.currentTimeMillis(), -1, TimeUnit.SECONDS, -1, TimeUnit.SECONDS));
+ }//if
+ else {
+ cache.remove(key);
+ //Write a timestamp for the update
+ cache.put(entity.getClass().getName(), System.currentTimeMillis(), -1, TimeUnit.SECONDS, -1, TimeUnit.SECONDS);
+ LOG.debug("Removed Entity with key [{}] from L2 cache in {}m", key, System.currentTimeMillis() - start);
+ }//else
+ }//if
+ }//if
+ }//try
+ finally {
+ span.end();
+ }//finally
+ }//remove
+
+ public long lastModified(Class entityType)
+ {
+ Long time = -1L;
+ Span span = TRACER.spanBuilder("EntityL2CacheImpl::lastModified").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ checkEntityType(entityType);
+ RemoteCache cache = getCache();
+ if (cache != null) {
+ span.setAttribute(ENTITY_ATTR, entityType.getName());
+ time = cache.get(entityType.getName());
+ if (time != null) {
+ return time;
+ }//if
+
+ time = System.currentTimeMillis();
+ cache.put(entityType.getName(), time, -1, TimeUnit.SECONDS, -1, TimeUnit.SECONDS);
+ }//if
+ }//try
+ finally {
+ span.end();
+ }//finally
+
+ return time;
+ }//lastModified
+
+ @Override
+ public boolean contains(Class entityType, Object primaryKey)
+ {
+ Span span = TRACER.spanBuilder("EntityL2CacheImpl::contains").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ checkEntityType(entityType);
+
+ RemoteCache cache = getCache();
+ if (cache != null) {
+ String key = makeCacheKey(entityType, primaryKey);
+ return cache.containsKey(key);
+ }//if
+ return false;
+ }//try
+ finally {
+ span.end();
+ }//finally
+ }//contains
+
+ @Override
+ public void evict(Class entityType, Object primaryKey)
+ {
+ Span span = TRACER.spanBuilder("EntityL2CacheImpl::evict using Primarykey").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ checkEntityType(entityType);
+ RemoteCache cache = getCache();
+ if (cache != null) {
+ String key = makeCacheKey(entityType, primaryKey);
+ cache.remove(key);
+ }//if
+ }//try
+ finally {
+ span.end();
+ }//finally
+ }//evict
+
+ @Override
+ public void evict(Class entityType)
+ {
+ Span span = TRACER.spanBuilder("EntityL2CacheImpl::evict by type").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ checkEntityType(entityType);
+ RemoteCache cache = getCache();
+ if (cache != null) {
+ Query q = cache.query("delete from " + entityType.getName());
+ q.executeStatement();
+ }//if
+ }//try
+ finally {
+ span.end();
+ }
+ }//evict
+
+ @Override
+ public void evictAll()
+ {
+ Span span = TRACER.spanBuilder("EntityL2CacheImpl::evictAll").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ RemoteCache cache = getCache();
+ if (cache != null) {
+ cache.clear();
+ }//if
+ }//try
+ finally {
+ span.end();
+ }//finally
+ }//evictAll
+
+ @Override
+ public void begin() throws NotSupportedException, SystemException
+ {
+ Span span = TRACER.spanBuilder("EntityL2CacheImpl::begin").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ if (CACHING_ENABLED) {
+ if (inTransaction) {
+ throw new NotSupportedException("Transaction already in progress");
+ }//if
+ inTransaction = true;
+ }//if
+ }//try
+ finally {
+ span.end();
+ }//finally
+ }//begin
+
+ @Override
+ public void commit() throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, IllegalStateException, SystemException
+ {
+ Span span = TRACER.spanBuilder("EntityL2CacheImpl::commit").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ if (CACHING_ENABLED) {
+ if (!inTransaction) {
+ throw new SystemException(NO_TRANSACTION_ACTIVE);
+ }//if
+
+ inTransaction = false;
+ RemoteCache cache = getCache();
+ if (cache != null) {
+ batchQueue.forEach(e -> {
+ if (e.action == ACTION_REMOVE) {
+ cache.remove(e.getKey());
+ }//if
+ else {
+ if (e.action == ACTION_ADD) {
+ cache.put(e.getKey(), e.getValue(), e.getLifespan(), e.getLifespanUnit(), e.getMaxIdleTime(), e.getMaxIdleTimeUnit());
+ }//if
+ else {
+ cache.replace(e.getKey(), e.getValue(), e.getLifespan(), e.getLifespanUnit(), e.getMaxIdleTime(), e.getMaxIdleTimeUnit());
+ }//else
+ }//if
+ });
+ batchQueue.clear();
+ }//if
+ }//if
+ }//try
+ finally {
+ span.end();
+ }//finally
+ }//commit
+
+
+ @Override
+ public void rollback() throws IllegalStateException, SecurityException, SystemException
+ {
+ if (CACHING_ENABLED) {
+ if (!inTransaction) {
+ throw new SystemException(NO_TRANSACTION_ACTIVE);
+ }//if
+
+ inTransaction = false;
+ batchQueue.clear();
+ }//if
+ }//rollback
+
+ @Override
+ public T unwrap(Class cls)
+ {
+ if (cls.isAssignableFrom(this.getClass())) {
+ return (T) this;
+ }
+
+ if (cls.isAssignableFrom(EntityCache.class)) {
+ return (T) this;
+ }
+
+ throw new IllegalArgumentException("Could not unwrap this [" + this + "] as requested Java type [" + cls.getName() + "]");
+ }
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/EntityLifecycleImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/EntityLifecycleImpl.java
new file mode 100644
index 0000000..2e8a0e4
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/EntityLifecycleImpl.java
@@ -0,0 +1,231 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl;
+
+import io.jpalite.EntityLifecycle;
+import io.jpalite.EntityMapException;
+import jakarta.persistence.*;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+@SuppressWarnings("java:S3011")//Changing accessibility mode is needed
+public class EntityLifecycleImpl implements EntityLifecycle
+{
+ private List listeners;
+
+ private class Methods
+ {
+ private Class> listenerClass;
+ private Method postLoad;
+ private Method prePersist;
+ private Method postPersist;
+ private Method preUpdate;
+ private Method postUpdate;
+ private Method preRemove;
+ private Method postRemove;
+
+ public Methods(Class> aClass)
+ {
+ listenerClass = aClass;
+ for (Method method : aClass.getMethods()) {
+ if (method.isAnnotationPresent(PostLoad.class)) {
+ postLoad = method;
+ postLoad.setAccessible(true);
+ }//if
+ else if (method.isAnnotationPresent(PrePersist.class)) {
+ prePersist = method;
+ prePersist.setAccessible(true);
+ }//if
+ else if (method.isAnnotationPresent(PostPersist.class)) {
+ postPersist = method;
+ postPersist.setAccessible(true);
+ }//if
+ else if (method.isAnnotationPresent(PreUpdate.class)) {
+ preUpdate = method;
+ preUpdate.setAccessible(true);
+ }//if
+ else if (method.isAnnotationPresent(PostUpdate.class)) {
+ postUpdate = method;
+ postUpdate.setAccessible(true);
+ }//if
+ else if (method.isAnnotationPresent(PreRemove.class)) {
+ preRemove = method;
+ }//if
+
+ if (method.isAnnotationPresent(PostRemove.class)) {
+ postRemove = method;
+ }//if
+ }//for
+ }
+ }
+
+ @FunctionalInterface
+ private interface LifeCycleFunction
+ {
+ void accept(O o, M m) throws InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException;
+ }
+
+ public EntityLifecycleImpl(Class> entityClass)
+ {
+ listeners = new ArrayList<>();
+
+ EntityListeners listeners = entityClass.getAnnotation(EntityListeners.class);
+ if (listeners != null) {
+ for (Class> aClass : listeners.value()) {
+ this.listeners.add(new Methods(aClass));
+ }//if
+ }//if
+ this.listeners.add(new Methods(entityClass));
+ }//EntityLifecycleImpl
+
+ private void invokeCallback(LifeCycleFunction func)
+ {
+ for (Methods method : listeners) {
+ try {
+ Object listener;
+ if (method.listenerClass != null) {
+ listener = method.listenerClass.getConstructor().newInstance();
+ }//if
+ else {
+ listener = null;
+ }//else
+
+ func.accept(listener, method);
+ }//try
+ catch (InvocationTargetException | InstantiationException | IllegalAccessException |
+ NoSuchMethodException ex) {
+ throw new EntityMapException("Error executing callback handler");
+ }//catch
+ }//for
+ }//invokeCallback
+
+ @Override
+ public void postLoad(Object entity)
+ {
+ invokeCallback((listener, methods) ->
+ {
+ if (methods.postLoad != null) {
+ if (listener == null) {
+ methods.postLoad.invoke(entity);
+ }//if
+ else {
+ methods.postLoad.invoke(listener, entity);
+ }//else
+ }//if
+ });
+ }
+
+ @Override
+ public void prePersist(Object entity)
+ {
+ invokeCallback((listener, methods) ->
+ {
+ if (methods.prePersist != null) {
+ if (listener == null) {
+ methods.prePersist.invoke(entity);
+ }//if
+ else {
+ methods.prePersist.invoke(listener, entity);
+ }//else
+ }//if
+ });
+ }
+
+ @Override
+ public void postPersist(Object entity)
+ {
+ invokeCallback((listener, methods) ->
+ {
+ if (methods.postPersist != null) {
+ if (listener == null) {
+ methods.postPersist.invoke(entity);
+ }//if
+ else {
+ methods.postPersist.invoke(listener, entity);
+ }//else
+ }//if
+ });
+ }
+
+ @Override
+ public void preUpdate(Object entity)
+ {
+ invokeCallback((listener, methods) ->
+ {
+ if (methods.preUpdate != null) {
+ if (listener == null) {
+ methods.preUpdate.invoke(entity);
+ }//if
+ else {
+ methods.preUpdate.invoke(listener, entity);
+ }//else
+ }//if
+ });
+ }
+
+ @Override
+ public void postUpdate(Object entity)
+ {
+ invokeCallback((listener, methods) ->
+ {
+ if (methods.postUpdate != null) {
+ if (listener == null) {
+ methods.postUpdate.invoke(entity);
+ }//if
+ else {
+ methods.postUpdate.invoke(listener, entity);
+ }//else
+ }//if
+ });
+ }
+
+ @Override
+ public void preRemove(Object entity)
+ {
+ invokeCallback((listener, methods) ->
+ {
+ if (methods.preRemove != null) {
+ if (listener == null) {
+ methods.preRemove.invoke(entity);
+ }//if
+ else {
+ methods.preRemove.invoke(listener, entity);
+ }//else
+ }//if
+ });
+ }
+
+ @Override
+ public void postRemove(Object entity)
+ {
+ invokeCallback((listener, methods) ->
+ {
+ if (methods.postRemove != null) {
+ if (listener == null) {
+ methods.postRemove.invoke(entity);
+ }//if
+ else {
+ methods.postRemove.invoke(listener, entity);
+ }//else
+ }//if
+ });
+ }
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/EntityMetaDataImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/EntityMetaDataImpl.java
new file mode 100644
index 0000000..87f7959
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/EntityMetaDataImpl.java
@@ -0,0 +1,437 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl;
+
+import io.jpalite.*;
+import jakarta.annotation.Nonnull;
+import jakarta.annotation.Nullable;
+import jakarta.persistence.*;
+import lombok.extern.slf4j.Slf4j;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+@SuppressWarnings("java:S3740")
+@Slf4j
+public class EntityMetaDataImpl implements EntityMetaData
+{
+ private final String entityName;
+ private final EntityLifecycle lifecycleListeners;
+ private final Class entityClass;
+ private final boolean legacyEntity;
+
+ private final boolean cacheable;
+ private long idleTime = 1;
+ private TimeUnit cacheTimeUnit = TimeUnit.DAYS;
+
+ private final String columns;
+ private String table;
+ private EntityType entityType;
+
+ private EntityMetaData> primaryKey;
+ private final List idFields;
+ private final Map entityFields;
+ private EntityField versionField;
+
+
+ public EntityMetaDataImpl(Class entityClass)
+ {
+ entityType = EntityType.ENTITY_NORMAL;
+ entityFields = new LinkedHashMap<>();
+ idFields = new ArrayList<>();
+
+ this.entityClass = entityClass;
+
+ Entity entity = entityClass.getAnnotation(Entity.class);
+
+ legacyEntity = (entity == null);
+ if (entity != null && !entity.name().isEmpty()) {
+ entityName = entity.name();
+ }//if
+ else {
+ entityName = entityClass.getSimpleName();
+ }//else
+
+ Table tableAnnotation = entityClass.getAnnotation(Table.class);
+ if (tableAnnotation != null) {
+ entityType = EntityType.ENTITY_DATABASE;
+ this.table = tableAnnotation.name();
+ }//if
+
+ Embeddable embeddable = entityClass.getAnnotation(Embeddable.class);
+ if (embeddable != null) {
+ entityType = EntityType.ENTITY_EMBEDDABLE;
+ }//if
+
+ Cacheable cacheableAnnotation = entityClass.getAnnotation(Cacheable.class);
+ if (cacheableAnnotation != null) {
+ this.cacheable = cacheableAnnotation.value();
+ Caching vCaching = entityClass.getAnnotation(Caching.class);
+ if (vCaching != null) {
+ idleTime = vCaching.idleTime();
+ cacheTimeUnit = vCaching.unit();
+ }//if
+ }//if
+ else {
+ this.cacheable = false;
+ }//else
+
+ IdClass idClass = entityClass.getAnnotation(IdClass.class);
+ if (idClass != null) {
+
+ if (!EntityMetaDataManager.isRegistered(idClass.value())) {
+ primaryKey = new EntityMetaDataImpl(idClass.value());
+ ((EntityMetaDataImpl) primaryKey).entityType = EntityType.ENTITY_IDCLASS;
+ EntityMetaDataManager.register(primaryKey);
+ }//if
+
+ if (primaryKey.getEntityType() != EntityType.ENTITY_IDCLASS) {
+ throw new IllegalArgumentException("Illegal IdClass specified. [" + idClass.value() + "] is already registered as an entity of type [" + primaryKey.getEntityType() + "]");
+ }//if
+ }//if
+
+ versionField = null;
+ StringBuilder stringBuilder = new StringBuilder("");
+ for (Field vField : entityClass.getDeclaredFields()) {
+ if (!Modifier.isStatic(vField.getModifiers()) &&
+ !Modifier.isFinal(vField.getModifiers()) &&
+ !Modifier.isTransient(vField.getModifiers()) &&
+ !vField.isAnnotationPresent(Transient.class)) {
+ processEntityField(vField, stringBuilder);
+ }//if
+ }//for
+
+ if (idFields.isEmpty()) {
+ LOG.warn("Developer Warning - Entity [{}] have no ID Fields defined . This needs to be fixed as not having ID fields is not allowed!", entityName);
+ }//if
+
+ //if
+ if (primaryKey == null && idFields.size() > 1) {
+ throw new IllegalArgumentException("Missing @IdClass definition for Entity. @IdClass definition is required if you have more than one ID field");
+ }//if
+
+ lifecycleListeners = new EntityLifecycleImpl(entityClass);
+
+ if (stringBuilder.length() > 1) {
+ columns = stringBuilder.substring(1);
+ }//if
+ else {
+ columns = "";
+ }//else
+ }//EntityMetaDataImpl
+
+ private void processEntityField(Field field, StringBuilder stringBuilder)
+ {
+ EntityField entityField = new EntityFieldImpl(entityClass, field, entityFields.size() + 1);
+
+ if (entityField.getMappingType() == MappingType.BASIC) {
+ if (entityField.getColumn() == null) {
+ return;
+ }//if
+
+ if (!entityField.getColumnDefinition().isEmpty() && !entityField.getTable().isEmpty()) {
+ stringBuilder.append(",");
+ stringBuilder.append(entityField.getTable()).append(".");
+ stringBuilder.append(entityField.getColumnDefinition()).append(" ").append(entityField.getColumn());
+ }//if
+ else {
+ //Ignore columns that have a '-' in the column definition
+ if (!"-".equals(entityField.getColumnDefinition())) {
+ stringBuilder.append(",");
+ stringBuilder.append(entityField.getColumn());
+ }//if
+ }//else
+
+ if (entityField.isIdField()) {
+ idFields.add(entityField);
+ }//if
+
+ if (entityField.isVersionField()) {
+ versionField = entityField;
+ }//if
+ }//if
+ else {
+ //JoinColumn is not required (or used) if getMappedBy is provided
+ if (entityField.getMappingType() != MappingType.EMBEDDED && entityField.getMappedBy() == null && entityField.getColumn() == null) {
+ return;
+ }//if
+ }//if
+
+ entityFields.put(entityField.getName(), entityField);
+ }//processEntityField
+
+ @Override
+ public String toString()
+ {
+ String primKeyClass;
+ if (primaryKey == null) {
+ if (getIdField() != null) {
+ primKeyClass = getIdField().getType().getName();
+ }//if
+ else {
+ primKeyClass = "N/A";
+ }//else
+ }//if
+ else {
+ primKeyClass = primaryKey.getEntityClass().getName();
+ }//else
+ return "[" + entityName + "] Metadata -> Type:" + entityType + ", Entity Class:" + entityClass.getName() + ", Primary Key Class:" + primKeyClass;
+ }//toString
+
+ @Override
+ public String getProtoFile()
+ {
+ StringBuilder protoFile = new StringBuilder("// File name: ")
+ .append(getName()).append(".proto\n")
+ .append("// Generated from : ")
+ .append(getClass().getName())
+ .append("\n")
+ .append("syntax = \"proto2\";\n")
+ .append("package org.tradeswitch;\n");
+
+ Set protoLibs = new HashSet<>();
+ entityFields.values().stream()
+ .filter(f -> f.getFieldType() == FieldType.TYPE_CUSTOMTYPE &&
+ f.getConverterClass().prototypeLib() != null &&
+ !f.getConverterClass().prototypeLib().isBlank())
+ .forEach(f -> protoLibs.add(f.getConverterClass().prototypeLib()));
+
+ protoLibs.forEach(lib -> protoFile.append("import \"").append(lib).append("\";\n"));
+
+ protoFile.append("message ")
+ .append(entityClass.getSimpleName())
+ .append("{\n");
+
+ for (EntityField field : entityFields.values()) {
+ protoFile.append("\t")
+ .append(field.isNullable() ? "optional " : "required ");
+
+ switch (field.getFieldType()) {
+ case TYPE_ENTITY -> {
+ EntityMetaData> vMetaData = EntityMetaDataManager.getMetaData(field.getType());
+ if (vMetaData.getEntityType() == EntityType.ENTITY_EMBEDDABLE) {
+ if (!vMetaData.isCacheable()) {
+ throw new EntityMapException("Entity " + getName() + " is marked as cacheable but embeddable " + vMetaData.getName() + " to not marked as cacheable");
+ }//if
+ protoFile.append(field.getType().getSimpleName());
+ }//if
+ else {
+ protoFile.append(vMetaData.getIdField().getFieldType().getProtoType());
+ }//else
+ }//case
+ case TYPE_ENUM -> protoFile.append("string");
+ case TYPE_ORDINAL_ENUM -> protoFile.append("uint32");
+ case TYPE_CUSTOMTYPE -> protoFile.append(field.getConverterClass().getFieldType());
+ default -> protoFile.append(field.getFieldType().getProtoType());
+ }//switch
+
+ protoFile.append(" ")
+ .append(field.getName())
+ .append(" = ")
+ .append(field.getFieldNr())
+ .append(";\n");
+ }//for
+ protoFile.append("}\n");
+
+ return protoFile.toString();
+ }//getProtoFile
+
+ @Override
+ public EntityType getEntityType()
+ {
+ return entityType;
+ }//getEntityType
+
+ @Override
+ public String getName()
+ {
+ return entityName;
+ }//getName
+
+ @Override
+ public boolean isCacheable()
+ {
+ return cacheable;
+ }//isCacheable
+
+ /**
+ * The time the entity is to remain in cache before expiring it. Only used if cacheable is true
+ *
+ * @return
+ */
+ public long getIdleTime()
+ {
+ return idleTime;
+ }
+
+ /**
+ * The TimeUnit the idle time is expressed in
+ *
+ * @return The time units
+ */
+ public TimeUnit getCacheTimeUnit()
+ {
+ return cacheTimeUnit;
+ }
+
+ @Nonnull
+ @Override
+ public T getNewEntity()
+ {
+ try {
+ return entityClass.getConstructor().newInstance();
+ }//try
+ catch (IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException ex) {
+ throw new EntityMapException("Error instantiating instance of " + entityClass.getSimpleName());
+ }//catch
+ }//getNewEntity
+
+ @Override
+ public Class getEntityClass()
+ {
+ return entityClass;
+ }//getEntityClass
+
+ @Override
+ public EntityLifecycle getLifecycleListeners()
+ {
+ return lifecycleListeners;
+ }//getLifecycleListeners
+
+ @Override
+ public String getTable()
+ {
+ return table;
+ }//getTable
+
+ @Override
+ @Nonnull
+ public EntityField getEntityField(String fieldName)
+ {
+ EntityField entityField = entityFields.get(fieldName);
+ if (entityField == null) {
+ throw new EntityNotFoundException(fieldName + " is not defined as a field in entity " + this.entityName);
+ }//if
+
+ return entityField;
+ }//getEntityField
+
+ @Override
+ public boolean isEntityField(String fieldName)
+ {
+ return entityFields.containsKey(fieldName);
+ }//isEntityField
+
+ @Nullable
+ public EntityField getEntityFieldByColumn(String column)
+ {
+ for (EntityField field : entityFields.values()) {
+ if (column.equalsIgnoreCase(field.getColumn())) {
+ return field;
+ }//if
+ }//for
+
+ return null;
+ }//getEntityFieldByColumn
+
+ @Override
+ @Nonnull
+ public EntityField getEntityFieldByNr(int fieldNr)
+ {
+ Optional entityField = entityFields.values()
+ .stream()
+ .filter(f -> f.getFieldNr() == fieldNr)
+ .findFirst();
+ if (entityField.isEmpty()) {
+ throw new EntityNotFoundException("There is no entity field with a fields number of " + fieldNr + " in entity " + this.entityName);
+ }//if
+
+ return entityField.get();
+ }//getEntityFieldByNr
+
+ @Override
+ public Collection getEntityFields()
+ {
+ return entityFields.values();
+ }//getEntityFields
+
+ @Override
+ public boolean hasMultipleIdFields()
+ {
+ return false;
+ }//hasMultipleIdFields
+
+ @Override
+ public EntityField getIdField()
+ {
+ if (hasMultipleIdFields()) {
+ throw new IllegalArgumentException("Multiple id fields exists");
+ }//if
+
+ if (idFields.isEmpty()) {
+ return null;
+ }//if
+
+ return idFields.getFirst();
+ }//getIdField
+
+ @Override
+ public boolean hasVersionField()
+ {
+ return versionField != null;
+ }//hasVersionField
+
+ @Override
+ public EntityField getVersionField()
+ {
+ if (versionField == null) {
+ throw new IllegalArgumentException("The entity does not have a version field");
+ }//if
+
+ return versionField;
+ }//getVersionField
+
+ @Override
+ @Nullable
+ public EntityMetaData> getIPrimaryKeyMetaData()
+ {
+ return primaryKey;
+ }//getIPrimaryKeyMetaData
+
+ @Override
+ public boolean isLegacyEntity()
+ {
+ return legacyEntity;
+ }
+
+ @Override
+ @Nonnull
+ public List getIdFields()
+ {
+ return idFields;
+ }//getIdFields
+
+ @Override
+ public String getColumns()
+ {
+ return columns;
+ }//getColumns
+}//EntityMetaDataImpl
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/JPAConfig.java b/jpalite-core/src/main/java/io/jpalite/impl/JPAConfig.java
new file mode 100644
index 0000000..ec5e4d0
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/JPAConfig.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl;
+
+import io.smallrye.config.SmallRyeConfigProviderResolver;
+
+import java.util.Optional;
+
+public class JPAConfig
+{
+ public static String getValue(String propertyName, String defaultValue)
+ {
+ SmallRyeConfigProviderResolver config = new SmallRyeConfigProviderResolver();
+ Optional optionalValue = config.getConfig().getOptionalValue(propertyName, String.class);
+ return optionalValue.orElse(defaultValue);
+ }//getValue
+
+
+ public static Boolean getValue(String propertyName, Boolean defaultValue)
+ {
+ SmallRyeConfigProviderResolver config = new SmallRyeConfigProviderResolver();
+ Optional optionalValue = config.getConfig().getOptionalValue(propertyName, Boolean.class);
+ return optionalValue.orElse(defaultValue);
+ }//getValue
+
+
+ public static Long getValue(String propertyName, Long defaultValue)
+ {
+ SmallRyeConfigProviderResolver config = new SmallRyeConfigProviderResolver();
+ Optional optionalValue = config.getConfig().getOptionalValue(propertyName, Long.class);
+ return optionalValue.orElse(defaultValue);
+ }//getValue
+
+
+ public static Integer getValue(String propertyName, Integer defaultValue)
+ {
+ SmallRyeConfigProviderResolver config = new SmallRyeConfigProviderResolver();
+ Optional optionalValue = config.getConfig().getOptionalValue(propertyName, Integer.class);
+ return optionalValue.orElse(defaultValue);
+ }//getValue
+
+ private JPAConfig()
+ {
+ //Hide the constructor
+ }
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/JPAEntityImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/JPAEntityImpl.java
new file mode 100644
index 0000000..a551f02
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/JPAEntityImpl.java
@@ -0,0 +1,1008 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl;
+
+import io.jpalite.PersistenceContext;
+import io.jpalite.*;
+import io.jpalite.impl.queries.JPALiteQueryImpl;
+import io.jpalite.impl.queries.QueryImpl;
+import io.jpalite.impl.serializers.JPAEntityMarshaller;
+import io.jpalite.queries.QueryLanguage;
+import jakarta.annotation.Nonnull;
+import jakarta.persistence.*;
+import jakarta.persistence.spi.LoadState;
+import org.infinispan.protostream.FileDescriptorSource;
+import org.infinispan.protostream.GeneratedSchema;
+import org.infinispan.protostream.SerializationContext;
+import org.slf4j.LoggerFactory;
+
+import java.io.*;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.function.Consumer;
+
+import static jakarta.persistence.LockModeType.*;
+
+/**
+ * This class will be made the super class of all entity classes defined and managed by the TradeSwitch Entity Manager.
+ *
+ * The JPA Maven plugin class will modify the bytecode of all entity classes change the super class to piont to
+ * this class.
+ *
+ * To prevent any mishaps with duplicate method names hiding access to the class all methods here will be prefixed with
+ * '_' and attributes with '$$' knowing that it is considered a bad naming convention and be flagged as such by the IDE
+ * and SonarQube (hoping that, you, the developer, do not pick the same method and variable names as what I have been
+ * using here ;-) )
+ */
+@SuppressWarnings({"java:S100", "java:S116"})
+public class JPAEntityImpl implements JPAEntity, GeneratedSchema
+{
+ public static final String SELECT_CLAUSE = "select ";
+ public static final String FROM_CLAUSE = " from ";
+ public static final String WHERE_CLAUSE = " where ";
+ /**
+ * A set of fields that was modified
+ */
+ private final transient Set $$modifiedList = new HashSet<>();
+ /**
+ * A set of fields that must be loaded on first access
+ */
+ private final transient Set $$fetchLazy = new HashSet<>();
+ /**
+ * The current entity state
+ */
+ private transient EntityState $$state = EntityState.TRANSIENT;
+ /**
+ * The action to perform on this entity when it is flushed by the persistence context
+ */
+ private transient PersistenceAction $$pendingAction = PersistenceAction.NONE;
+ /**
+ * The lock mode for the entity
+ */
+ private transient LockModeType $$lockMode = LockModeType.NONE;
+ /**
+ * The persistence context this entity belongs too.
+ */
+ private transient PersistenceContext $$persistenceContext = null;
+ /**
+ * The metadata for the entity
+ */
+ private final transient EntityMetaData> $$metadata;
+ /**
+ * Set to true if the entity is being mapped
+ */
+ private transient boolean $$mapping = false;
+ /**
+ * Set to true if the entity is lazy loaded.
+ */
+ private transient boolean $$lazyLoaded = false;
+ /**
+ * Indicator that an entity was created but no fields has been set yet.
+ */
+ private transient boolean $$blankEntity = true;
+
+ /**
+ * Control value to prevent recursive iteration by toString
+ */
+ private transient boolean inToString = false;
+
+ protected JPAEntityImpl()
+ {
+ if (EntityMetaDataManager.isRegistered(getClass())) {
+ $$metadata = EntityMetaDataManager.getMetaData(getClass());
+
+ //Find all BASIC and ONE_TO_MANY fields that are flagged as being lazily fetched and add them to our $$fetchLazy list
+ $$metadata.getEntityFields()
+ .stream()
+ .filter(f -> f.getFetchType() == FetchType.LAZY && (f.getMappingType() == MappingType.BASIC || f.getMappingType() == MappingType.ONE_TO_MANY))
+ .forEach(f -> $$fetchLazy.add(f.getName()));
+
+ //Force the default lock mode to OPTIMISTIC_FORCE_INCREMENT if the entity has a version field
+ if ($$metadata.hasVersionField()) {
+ $$lockMode = OPTIMISTIC_FORCE_INCREMENT;
+ }//if
+ }//if
+ else {
+ $$metadata = null;
+ }//else
+ }//JPAEntityImpl
+
+ @Override
+ public Class> get$$EntityClass()
+ {
+ return getClass();
+ }
+
+ @Override
+ public String toString()
+ {
+ if ($$metadata == null) {
+ return super.toString();
+ }//if
+
+ StringBuilder toString = new StringBuilder(_getEntityInfo())
+ .append(" ::")
+ .append(_getStateInfo()).append(", ");
+
+ toString.append(_getDataInfo());
+
+ return toString.toString();
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o) return true;
+ if (o instanceof JPAEntityImpl e) {
+ return _getPrimaryKey() != null && _getPrimaryKey().equals(e._getPrimaryKey());
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hashCode(_getPrimaryKey());
+ }
+
+ @Override
+ public String _getEntityInfo()
+ {
+ return "Entity " + $$metadata.getName();
+ }//_getEntityInfo
+
+ @Override
+ @SuppressWarnings({"java:S3776", "java:S3740"}) //The method cannot be simplified without increasing its complexity
+ public String _getDataInfo()
+ {
+ StringBuilder toString = new StringBuilder();
+
+ if (inToString) {
+ toString.append(" [Circular reference detected]");
+ }//if
+ else {
+ try {
+ inToString = true;
+
+ if ($$lazyLoaded) {
+ toString.append(" [Lazy on PK=")
+ .append(_getPrimaryKey())
+ .append("] ");
+ }//if
+ else {
+ toString.append("Data(");
+
+ boolean first = true;
+ for (EntityField field : _getMetaData().getEntityFields()) {
+ if (!first) {
+ toString.append(", ");
+ }//if
+ first = false;
+
+ if (field.isIdField()) {
+ toString.append("*");
+ }//if
+ toString.append(field.getName()).append("=");
+ if ($$fetchLazy.contains(field.getName())) {
+ toString.append("[Lazy]");
+ }//if
+ else {
+ Object val = _getField(field.getName());
+ if (val instanceof Map mapVal) {
+ val = "[Map " + mapVal.size() + " items]";
+ }//if
+ else if (val instanceof List listVal) {
+ val = "[List " + listVal.size() + " items]";
+ }//else if
+ toString.append(val);
+ }//else
+ }//for
+ toString.append(")");
+ }//if
+ }//try
+ finally {
+ inToString = false;
+ }//finally
+ }//else
+
+ return toString.toString();
+ }//_getDataInfo
+
+ @Override
+ public String _getStateInfo()
+ {
+ return " State:" + $$state + ", " + "Action:" + $$pendingAction;
+ }//_getStateInfo
+
+ @Override
+ public JPAEntity _clone()
+ {
+ JPAEntityImpl clone = (JPAEntityImpl) $$metadata.getNewEntity();
+ clone.$$blankEntity = false;
+ _getMetaData().getEntityFields()
+ .stream()
+ .filter(f -> !f.isIdField() && !f.isVersionField())
+ .forEach(f ->
+ {
+ Object vVal = f.invokeGetter(this);
+ f.invokeSetter(clone, vVal);
+ });
+ clone.$$fetchLazy.addAll($$fetchLazy);
+ return clone;
+ }//_clone
+
+ @Override
+ public void _replaceWith(JPAEntity entity)
+ {
+ if (!_getMetaData().getName().equals(entity._getMetaData().getName())) {
+ throw new IllegalArgumentException("Attempting to replace entities of different types");
+ }//if
+
+ if (_getEntityState() != EntityState.DETACHED && _getEntityState() != EntityState.TRANSIENT) {
+ throw new IllegalArgumentException("The content of an entity can only be replaced if it is DETACHED or TRANSIENT");
+ }//if
+
+ if (entity._getEntityState() != EntityState.MANAGED && entity._getEntityState() != EntityState.DETACHED) {
+ throw new IllegalArgumentException("The provided entity must be in an MANAGED or DETACHED state");
+ }//if
+
+ $$mapping = true;
+ try {
+ _getMetaData().getEntityFields()
+ .stream()
+ .filter(f -> !_isLazyLoaded(f.getName()))
+ .forEach(f -> f.invokeSetter(this, f.invokeGetter(entity)));
+ $$fetchLazy.clear();
+ $$fetchLazy.addAll(((JPAEntityImpl) entity).$$fetchLazy);
+ $$blankEntity = false;
+ _setPendingAction(entity._getPendingAction());
+ $$modifiedList.clear();
+ $$modifiedList.addAll(((JPAEntityImpl) entity).$$modifiedList);
+
+ entity._getPersistenceContext().l1Cache().manage(this);
+ entity._getPersistenceContext().l1Cache().detach(entity);
+ }//try
+ finally {
+ $$mapping = false;
+ }
+ }//_replaceWith
+
+ @Override
+ public void _refreshEntity(Map properties)
+ {
+ if ($$blankEntity) {
+ throw new IllegalStateException("Entity is not initialised");
+ }//if
+
+ if (_getEntityState() == EntityState.TRANSIENT || _getEntityState() == EntityState.REMOVED || _getPersistenceContext() == null) {
+ throw new IllegalStateException("Entity is not managed or detached");
+ }//if
+
+ if (_getPersistenceContext().isReleased()) {
+ throw new LazyInitializationException("Entity is not attached to an active persistence context");
+ }//if
+
+ try {
+ _clearModified();
+
+ //Detach the entity from L1 cache
+ PersistenceContext persistenceContext = _getPersistenceContext();
+ persistenceContext.l1Cache().detach(this);
+
+ String queryStr = SELECT_CLAUSE + $$metadata.getName() + FROM_CLAUSE + $$metadata.getName() + WHERE_CLAUSE + $$metadata.getIdField().getName() + "=:p";
+ JPALiteQueryImpl> query = new JPALiteQueryImpl<>(queryStr,
+ QueryLanguage.JPQL,
+ persistenceContext,
+ $$metadata.getEntityClass(),
+ properties,
+ $$lockMode);
+ query.setParameter("p", _getPrimaryKey());
+ JPAEntity replaceEntity = (JPAEntity) query.getSingleResult();
+ _replaceWith(replaceEntity);
+ $$lazyLoaded = false;
+ }//try
+ catch (NoResultException ex) {
+ throw new EntityNotFoundException(String.format("Lazy load of entity '%s' for key '%s' failed", $$metadata.getName(), _getPrimaryKey()));
+ }
+ catch (PersistenceException ex) {
+ throw new LazyInitializationException("Error lazy fetching entity " + $$metadata.getName(), ex);
+ }//catch
+ }//_refreshEntity
+
+ private void _queryOneToMany(EntityField entityField)
+ {
+ EntityMetaData> metaData = EntityMetaDataManager.getMetaData(entityField.getType());
+ EntityField mappingField = metaData.getEntityField(entityField.getMappedBy());
+
+ JPALiteQueryImpl> query = new JPALiteQueryImpl<>(SELECT_CLAUSE + metaData.getName() + FROM_CLAUSE + metaData.getName() + WHERE_CLAUSE + mappingField.getName() + "=:p",
+ QueryLanguage.JPQL,
+ _getPersistenceContext(),
+ metaData.getEntityClass(),
+ Collections.emptyMap());
+ query.setParameter("p", _getPrimaryKey());
+ entityField.invokeSetter(this, query.getResultList());
+ }//_fetchOneToMany
+
+ private void _queryBasicField(EntityField entityField)
+ {
+ String queryStr = SELECT_CLAUSE + " E." + entityField.getName() + FROM_CLAUSE + $$metadata.getName() + " E " + WHERE_CLAUSE + " E." + $$metadata.getIdField().getName() + "=:p";
+ Query query = new QueryImpl(queryStr,
+ _getPersistenceContext(),
+ entityField.getType(),
+ new HashMap<>());
+ query.setParameter("p", _getPrimaryKey());
+
+ //Will call _markField which will remove the field from the list
+ entityField.invokeSetter(this, query.getSingleResult());
+ }//_queryBasicField
+
+ @Override
+ public void _lazyFetchAll(boolean forceEagerLoad)
+ {
+ Set lazyFields = new HashSet<>($$fetchLazy);
+ lazyFields.forEach(this::_lazyFetch);
+ _getMetaData().getEntityFields()
+ .stream()
+ .filter(f -> f.getMappingType().equals(MappingType.MANY_TO_ONE) && (forceEagerLoad || f.getFetchType() == FetchType.EAGER))
+ .forEach(f -> {
+ JPAEntity manyToOneField = (JPAEntity) f.invokeGetter(this);
+ if (manyToOneField != null) {
+ _getPersistenceContext().l1Cache().manage(manyToOneField);
+ manyToOneField._refreshEntity(Collections.emptyMap());
+ }//if
+ });
+ }//_lazyFetchAll
+
+ @Override
+ public void _lazyFetch(String fieldName)
+ {
+ //Lazy fetching is only applicable for MANAGED and DETACHED entities
+ if (_getEntityState() == EntityState.TRANSIENT || _getEntityState() == EntityState.REMOVED) {
+ return;
+ }//if
+
+ if (_isLazyLoaded()) {
+ //Refresh the entity. Refreshing will also clear the lazy loaded flag
+ _refreshEntity(Collections.emptyMap());
+ }//if
+
+ if ($$fetchLazy.contains(fieldName)) {
+ if (_getPersistenceContext().isReleased()) {
+ throw new LazyInitializationException("Entity is not attached to an active persistence context");
+ }//if
+
+ EntityField entityField = $$metadata.getEntityField(fieldName);
+ if (entityField.getMappingType() == MappingType.BASIC) {
+ _queryBasicField(entityField);
+ }//if
+ else {
+ _queryOneToMany(entityField);
+ }//else
+ }//if
+ }//_lazyFetch
+
+ @Override
+ public boolean _isLazyLoaded()
+ {
+ return $$lazyLoaded;
+ }//_isLazyLoaded
+
+ @Override
+ public boolean _isLazyLoaded(String fieldName)
+ {
+ return $$fetchLazy.contains(fieldName);
+ }
+
+ @Override
+ public void _markLazyLoaded()
+ {
+ $$lazyLoaded = true;
+ }//_markLazyLoaded
+
+ @Override
+ public void _makeReference(Object primaryKey)
+ {
+ if (!$$blankEntity) {
+ throw new IllegalArgumentException("Entity must be blank to be made into a reference");
+ }//if
+
+ _setPrimaryKey(primaryKey);
+ _markLazyLoaded();
+ _clearModified();
+ _setPendingAction(PersistenceAction.NONE);
+ }//_makeReference
+
+ @Override
+ public EntityMetaData> _getMetaData()
+ {
+ if ($$metadata == null) {
+ throw new IllegalArgumentException(getClass() + " is not a known entity or not yet registered");
+ }//if
+
+ return $$metadata;
+ }
+
+ @Override
+ public Set _getModifiedFields()
+ {
+ return $$modifiedList;
+ }
+
+ @Override
+ public void _clearModified()
+ {
+ $$modifiedList.clear();
+ if ($$pendingAction == PersistenceAction.UPDATE) {
+ $$pendingAction = PersistenceAction.NONE;
+ }//if
+ }
+
+ @Override
+ public boolean _isLegacyEntity()
+ {
+ return $$metadata.isLegacyEntity();
+ }
+
+ @Override
+ public LoadState _loadState()
+ {
+ return (_isLazyLoaded() || $$blankEntity) ? LoadState.NOT_LOADED : LoadState.LOADED;
+ }
+
+ @Override
+ public boolean _isFieldModified(String fieldName)
+ {
+ return $$modifiedList.contains(fieldName);
+ }
+
+ @Override
+ public void _clearField(String fieldName)
+ {
+ $$modifiedList.remove(fieldName);
+ if ($$modifiedList.isEmpty() && $$pendingAction == PersistenceAction.UPDATE) {
+ $$pendingAction = PersistenceAction.NONE;
+ }//if
+ }
+
+ @Override
+ public void _markField(String fieldName)
+ {
+ if ($$metadata.isEntityField(fieldName)) {
+ EntityField vEntityField = $$metadata.getEntityField(fieldName);
+
+ if (!$$mapping && !_getEntityState().equals(EntityState.TRANSIENT) && vEntityField.isIdField()) {
+ if (!_isLegacyEntity()) {
+ throw new PersistenceException("The ID field cannot be modified");
+ }//if
+ LoggerFactory.getLogger(JPAEntityImpl.class).warn("Legacy Mode :: Allowing modifying of ID Field {} in Entity {}", vEntityField.getName(), $$metadata.getName());
+ }//if
+
+ if (!$$mapping && !_getEntityState().equals(EntityState.TRANSIENT) && vEntityField.isVersionField()) {
+ throw new PersistenceException("A VERSION field cannot be modified");
+ }//if
+
+ if (!$$mapping && !_getEntityState().equals(EntityState.TRANSIENT) && !vEntityField.isUpdatable()) {
+ if (!_isLegacyEntity()) {
+ throw new PersistenceException("Attempting to updated a field that is marked as NOT updatable");
+ }//if
+ LoggerFactory.getLogger(JPAEntityImpl.class).warn("Legacy Mode :: Allowing modifying of NOT updatable field {} in Entity {}", vEntityField.getName(), $$metadata.getName());
+ }//if
+
+ /*
+ * _markField is call whenever a field is updated
+ * When this happens we can clear the $$blankEntity flag (as it is not true anymore! :-) )
+ * We are also clearing the fetch lazy status for this field, if any
+ * Lastly we are marking this fields as modified
+ */
+ $$blankEntity = false;
+ $$fetchLazy.remove(fieldName);
+
+ /*
+ * ONE_TO_MANY fields is not really part of the current entity and any change to a ONE_TO_MANY field
+ * do not trigger an update to the current entity.
+ */
+ if (!$$mapping && vEntityField.getMappingType() != MappingType.ONE_TO_MANY) {
+ $$modifiedList.add(fieldName);
+ if ($$pendingAction == PersistenceAction.NONE) {
+ _setPendingAction(PersistenceAction.UPDATE);
+ }//if
+ }//if
+ }//if
+ }
+
+ @Override
+ public boolean _isEntityModified()
+ {
+ return !$$modifiedList.isEmpty();
+ }
+
+ @Override
+ public LockModeType _getLockMode()
+ {
+ return $$lockMode;
+ }
+
+ @Override
+ public void _setLockMode(LockModeType lockMode)
+ {
+ if (lockMode == OPTIMISTIC || lockMode == OPTIMISTIC_FORCE_INCREMENT || lockMode == WRITE || lockMode == READ) {
+ if (!_getMetaData().hasVersionField()) {
+ throw new PersistenceException("Entity has not version field");
+ }//if
+
+ /*
+ If the entity is not new and is not dirty but is locked optimistically, we need to update the version
+ The JPA Specification states that for versioned objects, it is permissible for an implementation to use
+ LockMode- Type.OPTIMISTIC_FORCE_INCREMENT where LockModeType.OPTIMISTIC/READ was requested, but not vice versa.
+ We choose to handle Type.OPTIMISTIC/READ) as Type.OPTIMISTIC_FORCE_INCREMENT
+ */
+ lockMode = OPTIMISTIC_FORCE_INCREMENT;
+ }//if
+ if (lockMode == NONE && _getMetaData().hasVersionField()) {
+ throw new PersistenceException("Entity has version field and cannot be locked with LockModeType.NONE");
+ }//if
+
+ $$lockMode = lockMode;
+ }
+
+ @Override
+ public EntityState _getEntityState()
+ {
+ return $$state;
+ }
+
+ @Override
+ public void _setEntityState(EntityState newState)
+ {
+ if ($$state != newState && newState != EntityState.REMOVED) {
+ $$metadata.getEntityFields().stream()
+ .filter(f -> f.getFieldType() == FieldType.TYPE_ENTITY && f.getMappingType() != MappingType.ONE_TO_MANY)
+ .forEach(f -> {
+ JPAEntity vEntity = (JPAEntity) f.invokeGetter(this);
+ if (vEntity != null) {
+ vEntity._setEntityState(newState);
+ }//if
+ });
+ }//if
+ $$state = newState;
+ }
+
+ @Override
+ public PersistenceContext _getPersistenceContext()
+ {
+ return $$persistenceContext;
+ }
+
+ @Override
+ public void _setPersistenceContext(PersistenceContext persistenceContext)
+ {
+ if ($$persistenceContext != persistenceContext) {
+ $$persistenceContext = persistenceContext;
+ $$metadata.getEntityFields().stream()
+ .filter(f -> f.getFieldType() == FieldType.TYPE_ENTITY && f.getMappingType() != MappingType.ONE_TO_MANY)
+ .forEach(f -> {
+ JPAEntity vEntity = (JPAEntity) f.invokeGetter(this);
+ if (vEntity != null) {
+ vEntity._setPersistenceContext(persistenceContext);
+ }//if
+ });
+ }//if
+ }
+
+ @Override
+ public PersistenceAction _getPendingAction()
+ {
+ return $$pendingAction;
+ }
+
+ @Override
+ public void _setPendingAction(PersistenceAction pendingAction)
+ {
+ $$pendingAction = pendingAction;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public X _getField(@Nonnull String fieldName)
+ {
+ EntityField entityField = _getMetaData().getEntityField(fieldName);
+
+ Object value = entityField.invokeGetter(this);
+ if (value == null) {
+ return null;
+ }//if
+
+ return (X) switch (entityField.getFieldType()) {
+ case TYPE_CUSTOMTYPE -> entityField.getConverterClass().convertToDatabaseColumn(value);
+ case TYPE_ENUM -> ((Enum>) value).name();
+ case TYPE_ORDINAL_ENUM -> ((Enum>) value).ordinal();
+
+ default -> value;
+ };
+ }//getField
+
+ @Override
+ public void _updateRestrictedField(Consumer method)
+ {
+ boolean mappingStatus = $$mapping;
+ try {
+ $$mapping = true;
+ method.accept(this);
+ }
+ finally {
+ $$mapping = mappingStatus;
+ }
+ }
+
+ @Override
+ public void _merge(JPAEntity entity)
+ {
+ if (!_getMetaData().getName().equals(entity._getMetaData().getName())) {
+ throw new IllegalArgumentException("Attempting to merge entities of different types");
+ }//if
+
+ if (!entity._getPrimaryKey().equals(_getPrimaryKey())) {
+ throw new EntityMapException("Error merging entities, primary key mismatch. Expected " + _getPrimaryKey() + ", but got " + entity._getPrimaryKey());
+ }//if
+
+ /*
+ * If the entity has a version field, we need to check that the version of the entity
+ * being merged matches the current version, except if the entity was created by reference.
+ */
+ if (!$$lazyLoaded && $$metadata.hasVersionField()) {
+ EntityField field = $$metadata.getVersionField();
+ Object val = field.invokeGetter(entity);
+ if (val != null && !val.equals(field.invokeGetter(this))) {
+ throw new OptimisticLockException("Error merging entities, version mismatch. Expected " + field.invokeGetter(this) + ", but got " + val);
+ }//if
+ }//if
+
+ for (String fieldName : entity._getModifiedFields()) {
+ EntityField field = $$metadata.getEntityField(fieldName);
+ if (!field.isIdField()) {
+ field.invokeSetter(this, field.invokeGetter(entity));
+ }//if
+ }//for
+ $$lazyLoaded = false;
+ }//merge
+
+ @Override
+ public Object _getPrimaryKey()
+ {
+ if ($$metadata == null || $$metadata.getIdFields().isEmpty()) {
+ return null;
+ }//if
+
+
+ if ($$metadata.getIdFields().size() > 1) {
+ EntityMetaData> primaryKey = $$metadata.getIPrimaryKeyMetaData();
+ Object primKey = null;
+ if (primaryKey != null) {
+ primKey = primaryKey.getNewEntity();
+ for (EntityField entityField : $$metadata.getIdFields()) {
+ EntityField keyField = primaryKey.getEntityField(entityField.getName());
+ keyField.invokeSetter(primKey, entityField.invokeGetter(this));
+ }//for
+ }//if
+ return primKey;
+ }//if
+ else {
+ return $$metadata.getIdFields().getFirst().invokeGetter(this);
+ }//else
+ }//_getPrimaryKey
+
+ @Override
+ public void _setPrimaryKey(Object primaryKey)
+ {
+ if (_getEntityState() != EntityState.TRANSIENT) {
+ throw new IllegalStateException("The primary key can only be set for an entity with a TRANSIENT state");
+ }//if
+
+ if ($$metadata.getIdFields().isEmpty()) {
+ throw new IllegalStateException("Entity [" + $$metadata.getName() + "] do not have any ID fields");
+ }//if
+
+
+ if ($$metadata.getIdFields().size() > 1) {
+ EntityMetaData> primaryKeyMetaData = $$metadata.getIPrimaryKeyMetaData();
+ if (primaryKeyMetaData == null) {
+ throw new IllegalStateException("Missing IDClass for Entity [" + $$metadata.getName() + "]");
+ }//if
+
+ for (EntityField entityField : $$metadata.getIdFields()) {
+ EntityField keyField = primaryKeyMetaData.getEntityField(entityField.getName());
+ entityField.invokeSetter(this, keyField.invokeGetter(primaryKey));
+ }//for
+ }//if
+ else {
+ $$metadata.getIdFields().getFirst().invokeSetter(this, primaryKey);
+ }//else
+ }//_setPrimaryKey
+
+ //
+ protected String _JPAReadString(ResultSet resultSet, int column) throws SQLException
+ {
+ String value = resultSet.getString(column);
+ return (resultSet.wasNull()) ? null : value;
+ }//_JPAReadString
+
+ protected Long _JPAReadLong(ResultSet resultSet, int column) throws SQLException
+ {
+ long value = resultSet.getLong(column);
+ return (resultSet.wasNull()) ? null : value;
+ }//_JPAReadLong
+
+ protected Boolean _JPAReadBoolean(ResultSet resultSet, int column) throws SQLException
+ {
+ boolean value = resultSet.getBoolean(column);
+ return (resultSet.wasNull() ? null : value);
+ }//_JPAReadBoolean
+
+ protected Integer _JPAReadInteger(ResultSet resultSet, int column) throws SQLException
+ {
+ int value = resultSet.getInt(column);
+ return (resultSet.wasNull() ? null : value);
+ }//_JPAReadInteger
+
+ protected Double _JPAReadDouble(ResultSet resultSet, int column) throws SQLException
+ {
+ double value = resultSet.getDouble(column);
+ return (resultSet.wasNull() ? null : value);
+ }//_JPAReadDouble
+
+ protected LocalDateTime _JPAReadLocalDateTime(ResultSet resultSet, int column) throws SQLException
+ {
+ Timestamp value = resultSet.getTimestamp(column);
+ return (resultSet.wasNull() ? null : value.toLocalDateTime());
+ }//_JPAReadLocalDateTime
+
+ protected Object _JPAReadCustomType(ResultSet resultSet, EntityField field, int column) throws SQLException
+ {
+ return field.getConverterClass().convertToEntityAttribute(resultSet, column);
+ }//_JPAReadCustomType
+
+ private Object _JPAReadENUM(EntityField field, ResultSet row, int column) throws SQLException
+ {
+ String enumName = row.getString(column);
+ for (Object enumValue : field.getType().getEnumConstants()) {
+ if (((Enum>) enumValue).name().equals(enumName)) {
+ return enumValue;
+ }//if
+ }//for
+
+ return null;
+ }//_JPAReadENUM
+
+ private Object _JPAReadOrdinalENUM(EntityField field, ResultSet row, int column) throws SQLException
+ {
+ int ordinal = row.getInt(column);
+ return field.getType().getEnumConstants()[ordinal];
+ }//_JPAReadOrdinalENUM
+
+ public JPAEntity _JPAReadEntity(EntityField field, ResultSet resultSet, String colPrefix, int col) throws SQLException
+ {
+ EntityMetaData> fieldMetaData = EntityMetaDataManager.getMetaData(field.getType());
+
+ //Read the primary key of the field and then check if the entity is not already managed
+ JPAEntity entity = (JPAEntity) fieldMetaData.getNewEntity();
+ entity._setPersistenceContext(_getPersistenceContext());
+ ((JPAEntityImpl) entity)._JPAReadField(resultSet, fieldMetaData.getIdField(), colPrefix, col);
+
+ JPAEntity managedEntity = null;
+ if (entity._getPrimaryKey() != null) {
+ if (_getPersistenceContext() != null) {
+ managedEntity = (JPAEntity) _getPersistenceContext().l1Cache().find(fieldMetaData.getEntityClass(), entity._getPrimaryKey(), true);
+ }//if
+
+ if (managedEntity == null) {
+ if (field.getFetchType() == FetchType.LAZY && (colPrefix == null || colPrefix.equals(resultSet.getMetaData().getColumnName(col)))) {
+ entity._markLazyLoaded();
+ }//if
+ else {
+ entity._mapResultSet(colPrefix, resultSet);
+ }//else
+
+ if (_getPersistenceContext() != null) {
+ _getPersistenceContext().l1Cache().manage(entity);
+ }//if
+ return entity;
+ }//if
+ }//if
+
+ return managedEntity;
+ }//_JPAReadEntity
+
+ @SuppressWarnings("java:S6205") // False error
+ public void _JPAReadField(ResultSet row, EntityField field, String colPrefix, int columnNr)
+ {
+ try {
+ $$mapping = true;
+ switch (field.getFieldType()) {
+ case TYPE_BOOLEAN -> field.invokeSetter(this, _JPAReadBoolean(row, columnNr));
+ case TYPE_INTEGER -> field.invokeSetter(this, _JPAReadInteger(row, columnNr));
+ case TYPE_LONGLONG -> field.invokeSetter(this, _JPAReadLong(row, columnNr));
+ case TYPE_DOUBLEDOUBLE -> field.invokeSetter(this, _JPAReadDouble(row, columnNr));
+ case TYPE_BOOL -> field.invokeSetter(this, row.getBoolean(columnNr));
+ case TYPE_INT -> field.invokeSetter(this, row.getInt(columnNr));
+ case TYPE_LONG -> field.invokeSetter(this, row.getLong(columnNr));
+ case TYPE_DOUBLE -> field.invokeSetter(this, row.getDouble(columnNr));
+ case TYPE_STRING -> field.invokeSetter(this, _JPAReadString(row, columnNr));
+ case TYPE_TIMESTAMP -> field.invokeSetter(this, row.getTimestamp(columnNr));
+ case TYPE_LOCALTIME -> field.invokeSetter(this, _JPAReadLocalDateTime(row, columnNr));
+ case TYPE_CUSTOMTYPE -> field.invokeSetter(this, _JPAReadCustomType(row, field, columnNr));
+ case TYPE_ENUM -> field.invokeSetter(this, _JPAReadENUM(field, row, columnNr));
+ case TYPE_ORDINAL_ENUM -> field.invokeSetter(this, _JPAReadOrdinalENUM(field, row, columnNr));
+ case TYPE_BYTES -> field.invokeSetter(this, row.getBytes(columnNr));
+ case TYPE_OBJECT -> field.invokeSetter(this, row.getObject(columnNr));
+ case TYPE_ENTITY -> {
+ if (field.getMappingType() == MappingType.ONE_TO_ONE || field.getMappingType() == MappingType.MANY_TO_ONE || field.getMappingType() == MappingType.EMBEDDED) {
+ field.invokeSetter(this, _JPAReadEntity(field, row, colPrefix, columnNr));
+ }//if
+ }//case
+ }//switch
+ }//try
+ catch (SQLException ex) {
+ throw new EntityMapException("Error setting field '" + field.getName() + "'", ex);
+ }//catch
+ finally {
+ $$mapping = false;
+ }//finally
+ }//setField
+
+ public void _mapResultSet(String colPrefix, ResultSet resultSet)
+ {
+ try {
+ ResultSetMetaData resultMetaData = resultSet.getMetaData();
+ int columns = resultMetaData.getColumnCount();
+
+ Set columnsProcessed = new HashSet<>();
+ for (int i = 1; i <= columns; i++) {
+ String column = resultMetaData.getColumnName(i);
+
+ EntityField field = null;
+ String nextColPrefix = null;
+ if (colPrefix == null) {
+ field = $$metadata.getEntityFieldByColumn(column);
+ }//if
+ else {
+ if (column.length() <= colPrefix.length() || !column.startsWith(colPrefix)) {
+ continue;
+ }//if
+
+ String fieldName = column.substring(colPrefix.length() + 1).split("-")[0];
+ if (!fieldName.isEmpty() && !columnsProcessed.contains(fieldName)) {
+ columnsProcessed.add(fieldName);
+ field = $$metadata.getEntityFieldByNr(Integer.parseInt(fieldName));
+ nextColPrefix = colPrefix + "-" + fieldName;
+ }//if
+ }//else
+
+ if (field != null) {
+ _JPAReadField(resultSet, field, nextColPrefix, i);
+ _clearField(field.getName());
+ }//if
+ }//for
+ $$lazyLoaded = false;
+ }//try
+ catch (Exception ex) {
+ throw new EntityMapException("Error extracting the ResultSet Metadata", ex);
+ }//catch
+ }//_maresultSet
+
+ private void writeObjects(ObjectOutput outStream) throws IOException
+ {
+ Collection fieldList = $$metadata.getEntityFields();
+ outStream.writeShort(fieldList.size());
+ for (EntityField field : fieldList) {
+ Object value = field.invokeGetter(this);
+ outStream.writeUTF(field.getName());
+ outStream.writeObject(value);
+ }//for
+ }//writeObjects
+
+ @Override
+ public byte[] _serialize()
+ {
+ try {
+ ByteArrayOutputStream recvOut = new ByteArrayOutputStream();
+ ObjectOutputStream stream = new ObjectOutputStream(recvOut);
+ writeObjects(stream);
+ stream.flush();
+
+ return recvOut.toByteArray();
+ }//try
+ catch (IOException ex) {
+ throw new PersistenceException("Error serialising entity", ex);
+ }//catch
+ }//_serialise
+
+ @SuppressWarnings("java:S112") // Throwable is correct
+ private void readObjects(ObjectInput inputStream) throws Throwable
+ {
+ int nrItems = inputStream.readShort();
+ while (nrItems > 0) {
+ nrItems--;
+ String fieldName = inputStream.readUTF();
+ EntityField field = $$metadata.getEntityField(fieldName);
+ field.invokeSetter(this, inputStream.readObject());
+ }//while
+ }//readObjects
+
+ @Override
+ public void _deserialize(byte[] bytes)
+ {
+ try {
+ ByteArrayInputStream recvOut = new ByteArrayInputStream(bytes);
+ ObjectInputStream stream = new ObjectInputStream(recvOut);
+ readObjects(stream);
+
+ _clearModified();
+ }//try
+ catch (Throwable ex) {
+ throw new PersistenceException("Error de-serialising the entity", ex);
+ }//catch
+ }//_deserialize
+
+ @Override
+ public boolean _entityEquals(JPAEntity entity)
+ {
+ return (entity._getMetaData().getEntityClass().equals(_getMetaData().getEntityClass()) &&
+ entity._getPrimaryKey().equals(_getPrimaryKey()));
+ }
+
+ @Override
+ public String getProtoFileName()
+ {
+ return getClass().getSimpleName() + ".proto";
+ }
+
+ @Override
+ public String getProtoFile() throws UncheckedIOException
+ {
+ return _getMetaData().getProtoFile();
+ }
+
+ @Override
+ public void registerSchema(SerializationContext serCtx)
+ {
+ /*
+ * Register the marshals and schemas for converter classes if not already registered
+ */
+ _getMetaData().getEntityFields().stream()
+ .filter(f -> f.getFieldType() == FieldType.TYPE_CUSTOMTYPE &&
+ f.getConverterClass().prototypeLib() != null &&
+ !f.getConverterClass().prototypeLib().isBlank() &&
+ !serCtx.canMarshall(f.getConverterClass().prototypeLib()))
+ .forEach(f -> {
+ f.getConverterClass().getSchema().registerSchema(serCtx);
+ f.getConverterClass().getSchema().registerMarshallers(serCtx);
+ });
+
+ serCtx.registerProtoFiles(FileDescriptorSource.fromString(getProtoFileName(), getProtoFile()));
+ }
+
+ @Override
+ public void registerMarshallers(SerializationContext serCtx)
+ {
+ serCtx.registerMarshaller(new JPAEntityMarshaller<>(getClass()));
+ }
+}//JPAEntityImpl
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/JPALiteEntityManagerImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/JPALiteEntityManagerImpl.java
new file mode 100755
index 0000000..4ba49ef
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/JPALiteEntityManagerImpl.java
@@ -0,0 +1,798 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.jpalite.impl;
+
+import io.jpalite.PersistenceContext;
+import io.jpalite.*;
+import io.jpalite.impl.queries.*;
+import io.jpalite.queries.EntityQuery;
+import io.jpalite.queries.QueryLanguage;
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.context.Scope;
+import io.quarkus.runtime.BlockingOperationControl;
+import io.quarkus.runtime.BlockingOperationNotAllowedException;
+import jakarta.annotation.Nonnull;
+import jakarta.persistence.*;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaDelete;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.CriteriaUpdate;
+import jakarta.persistence.metamodel.Metamodel;
+import lombok.ToString;
+import lombok.extern.slf4j.Slf4j;
+
+import java.sql.ResultSet;
+import java.util.*;
+
+import static jakarta.persistence.LockModeType.*;
+
+/**
+ * The entity manager implementation
+ */
+@Slf4j
+@ToString(of = {"persistenceContext", "entityManagerFactory", "threadId"})
+public class JPALiteEntityManagerImpl implements JPALiteEntityManager
+{
+ private static final String CRITERIA_QUERY_NOT_SUPPORTED = "CriteriaQuery is not supported";
+ private static final String ENTITY_GRAPH_NOT_SUPPORTED = "EntityGraph is not supported";
+ private static final String STORED_PROCEDURE_QUERY_NOT_SUPPORTED = "StoredProcedureQuery is not supported";
+ private static final Tracer TRACER = GlobalOpenTelemetry.get().getTracer(JPALiteEntityManagerImpl.class.getName());
+ private final EntityManagerFactory entityManagerFactory;
+ private final PersistenceContext persistenceContext;
+ private final long threadId;
+ private final Throwable opened;
+ private final Map properties;
+
+ private boolean entityManagerOpen;
+ private FlushModeType flushMode;
+
+ public JPALiteEntityManagerImpl(PersistenceContext persistenceContext, EntityManagerFactory factory)
+ {
+ this.persistenceContext = persistenceContext;
+ this.entityManagerFactory = factory;
+
+ entityManagerOpen = true;
+ flushMode = FlushModeType.AUTO;
+ properties = new HashMap<>(persistenceContext.getProperties());
+ threadId = Thread.currentThread().threadId();
+
+ if (LOG.isTraceEnabled()) {
+ opened = new Throwable();
+ }//if
+ else {
+ opened = null;
+ }//else
+ }//TradeSwitchEntityManagerImpl
+
+ //
+ @Override
+ @SuppressWarnings("java:S6205") // false error
+ public void setProperty(String name, Object value)
+ {
+ checkOpen();
+
+ persistenceContext.setProperty(name, value);
+ properties.put(name, value);
+ }//setProperty
+
+ @Override
+ public Map getProperties()
+ {
+ checkOpen();
+ return properties;
+ }//getProperties
+
+ private void checkOpen()
+ {
+ if (!isOpen()) {
+ throw new IllegalStateException("EntityManager is closed");
+ }//if
+
+ if (threadId != Thread.currentThread().threadId()) {
+ throw new IllegalStateException("Entity Managers are NOT threadsafe. Opened at ", opened);
+ }//if
+
+ if (!BlockingOperationControl.isBlockingAllowed()) {
+ throw new BlockingOperationNotAllowedException("You have attempted to perform a blocking operation on a IO thread. This is not allowed, as blocking the IO thread will cause major performance issues with your application. If you want to perform blocking EntityManager operations make sure you are doing it from a worker thread.");
+ }//if
+ }//checkOpen
+
+ private void checkEntity(Object entity)
+ {
+ if (entity == null) {
+ throw new IllegalArgumentException("Entity cannot be null");
+ }
+
+ if (!(entity instanceof JPAEntity)) {
+ throw new IllegalArgumentException("Entity is not an instance of JPAEntity");
+ }
+ }
+
+ private void checkEntityClass(Class> entityClass)
+ {
+ if (!(JPAEntity.class.isAssignableFrom(entityClass))) {
+ throw new IllegalArgumentException("Entity " + entityClass.getName() + " is not created using EntityManager");
+ }//if
+
+ EntityMetaData> metaData = EntityMetaDataManager.getMetaData(entityClass);
+ if (!persistenceContext.supportedEntityType(metaData.getEntityType())) {
+ throw new IllegalArgumentException("Entity is of type " + metaData.getEntityType() + " is not supported");
+ }//if
+ }//checkEntityClass
+
+ private void checkEntityAttached(JPAEntity entity)
+ {
+ if (entity._getEntityState() != EntityState.MANAGED) {
+ throw new IllegalArgumentException("Entity is not current attached to a persistence context");
+ }//if
+
+ if (entity._getPersistenceContext() != persistenceContext) {
+ throw new IllegalArgumentException("Entity is not being managed by this Persistence Context");
+ }//if
+ }//checkEntityObject
+
+ private void checkTransactionRequired()
+ {
+ if (!persistenceContext.isActive()) {
+ throw new TransactionRequiredException();
+ }//if
+ }//checkTransactionRequired
+ //
+
+ @Override
+ public EntityTransaction getTransaction()
+ {
+ checkOpen();
+ return persistenceContext.getTransaction();
+ }
+
+ @Override
+ public EntityManagerFactory getEntityManagerFactory()
+ {
+ checkOpen();
+
+ return entityManagerFactory;
+ }
+
+ @Override
+ public void close()
+ {
+ checkOpen();
+ entityManagerOpen = false;
+ }
+
+ @Override
+ public boolean isOpen()
+ {
+ return entityManagerOpen;
+ }
+
+ @Override
+ public X mapResultSet(@Nonnull X entity, ResultSet resultSet)
+ {
+ checkEntity(entity);
+ return persistenceContext.mapResultSet(entity, resultSet);
+ }
+
+ @Override
+ public void setFlushMode(FlushModeType flushMode)
+ {
+ checkOpen();
+ this.flushMode = flushMode;
+ }//setFlushMode
+
+ @Override
+ public FlushModeType getFlushMode()
+ {
+ checkOpen();
+ return flushMode;
+ }//getFlushMode
+
+ @Override
+ public void clear()
+ {
+ checkOpen();
+ persistenceContext.l1Cache().clear();
+ }//clear
+
+ @Override
+ public void detach(Object entity)
+ {
+ checkOpen();
+ checkEntity(entity);
+ checkEntityClass(entity.getClass());
+ checkEntityAttached((JPAEntity) entity);
+ persistenceContext.l1Cache().detach((JPAEntity) entity);
+ }//detach
+
+ @Override
+ public boolean contains(Object entity)
+ {
+ checkOpen();
+ checkEntity(entity);
+ checkEntityClass(entity.getClass());
+ return persistenceContext.l1Cache().contains((JPAEntity) entity);
+ }//contains
+
+ //
+ @Override
+ public EntityGraph createEntityGraph(Class rootType)
+ {
+ checkOpen();
+
+ throw new UnsupportedOperationException(ENTITY_GRAPH_NOT_SUPPORTED);
+ }
+
+ @Override
+ public EntityGraph> createEntityGraph(String graphName)
+ {
+ checkOpen();
+
+ throw new UnsupportedOperationException(ENTITY_GRAPH_NOT_SUPPORTED);
+ }
+
+ @Override
+ @SuppressWarnings("java:S4144")//Not an error
+ public EntityGraph> getEntityGraph(String graphName)
+ {
+ checkOpen();
+
+ throw new UnsupportedOperationException(ENTITY_GRAPH_NOT_SUPPORTED);
+ }
+
+ @Override
+ @SuppressWarnings("java:S4144")//Not an error
+ public List> getEntityGraphs(Class entityClass)
+ {
+ checkOpen();
+
+ throw new UnsupportedOperationException(ENTITY_GRAPH_NOT_SUPPORTED);
+ }
+ //
+
+ //
+ @Override
+ public void flush()
+ {
+ checkOpen();
+ checkTransactionRequired();
+
+ persistenceContext.flush();
+ }//flush
+
+ @Override
+ public void flushOnType(Class> entityClass)
+ {
+ persistenceContext.flushOnType(entityClass);
+ }//flushEntities
+
+ @Override
+ public void flushEntity(@Nonnull T entity)
+ {
+ checkOpen();
+ checkEntity(entity);
+ checkTransactionRequired();
+ checkEntityAttached((JPAEntity) entity);
+
+ persistenceContext.flushEntity((JPAEntity) entity);
+ }//flushEntity
+
+ @Override
+ public void persist(@Nonnull Object entity)
+ {
+ checkOpen();
+ checkEntity(entity);
+ checkTransactionRequired();
+ checkEntityClass(entity.getClass());
+
+ if (((JPAEntity) entity)._getEntityState() == EntityState.MANAGED) {
+ //An existing managed entity is ignored
+ return;
+ }//if
+
+ if (((JPAEntity) entity)._getEntityState() == EntityState.REMOVED) {
+ throw new PersistenceException("Attempting to persist an entity that was removed from the database");
+ }//if
+
+ ((JPAEntity) entity)._setPendingAction(PersistenceAction.INSERT);
+ persistenceContext.l1Cache().manage((JPAEntity) entity);
+
+ if (flushMode == FlushModeType.AUTO) {
+ flushEntity((JPAEntity) entity);
+ }//if
+ }//persist
+
+ /**
+ * Many-to-one fields (entities) might be indirectly attached but contain One-to-Many fields
+ */
+ private void cascadeMerge(JPAEntity entity)
+ {
+ entity._getMetaData()
+ .getEntityFields()
+ .stream()
+ .filter(f -> f.getFieldType() == FieldType.TYPE_ENTITY && !entity._isLazyLoaded(f.getName()))
+ .filter(f -> f.getCascade().contains(CascadeType.ALL) || f.getCascade().contains(CascadeType.MERGE))
+ .forEach(f ->
+ {
+ try {
+ if (f.getMappingType() == MappingType.ONE_TO_MANY) {
+ List entityList = (List) f.invokeGetter(entity);
+
+ for (ListIterator it = entityList.listIterator(); it.hasNext(); ) {
+ it.set(merge(it.next()));
+ }//for
+ }//if
+ }//try
+ catch (PersistenceException ex) {
+ throw ex;
+ }//catch
+ catch (RuntimeException ex) {
+ LOG.error("Error merging ManyToOne field", ex);
+ throw new PersistenceException("Error merging ManyToOne field");
+ }//catch
+ });
+ }
+
+ @Override
+ public X merge(X entity)
+ {
+ Span span = TRACER.spanBuilder("EntityManager::merge").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ checkOpen();
+ checkEntity(entity);
+ checkTransactionRequired();
+ checkEntityClass(entity.getClass());
+
+ JPAEntity jpaEntity = (JPAEntity) entity;
+ return switch (jpaEntity._getEntityState()) {
+ case MANAGED -> {
+ checkEntityAttached(jpaEntity);
+ yield entity;
+ }
+ case DETACHED -> {
+ X latestEntity = (X) find(jpaEntity.getClass(), jpaEntity._getPrimaryKey(), jpaEntity._getLockMode());
+ if (latestEntity == null) {
+ throw new IllegalArgumentException("Original entity not found");
+ }//if
+ ((JPAEntity) latestEntity)._merge(jpaEntity);
+ cascadeMerge(jpaEntity);
+ yield latestEntity;
+ }
+
+ case REMOVED ->
+ throw new PersistenceException("Attempting to merge an entity that was removed from the database");
+
+ case TRANSIENT -> {
+ Object primaryKey = jpaEntity._getPrimaryKey();
+ if (primaryKey != null) {
+ X persistedEntity = find((Class) entity.getClass(), primaryKey);
+ if (persistedEntity != null) {
+ ((JPAEntity) persistedEntity)._merge(jpaEntity);
+ yield persistedEntity;
+ }//if
+ }//if
+
+ persist(entity);
+ yield entity;
+ }
+ };
+ }//try
+ finally {
+ span.end();
+ }
+ }//merge
+
+ @Override
+ public T clone(@Nonnull T entity)
+ {
+ checkOpen();
+ checkEntity(entity);
+ checkEntityClass(entity.getClass());
+
+ return (T) ((JPAEntity) entity)._clone();
+ }//clone
+
+ @Override
+ public void remove(Object entity)
+ {
+ checkOpen();
+ checkEntity(entity);
+ checkTransactionRequired();
+ checkEntityClass(entity.getClass());
+
+ ((JPAEntity) entity)._setPendingAction(PersistenceAction.DELETE);
+
+ if (flushMode == FlushModeType.AUTO) {
+ flushEntity((JPAEntity) entity);
+ }//if
+ }//remove
+ //
+
+ //
+ @Override
+ public void refresh(Object entity)
+ {
+ checkEntity(entity);
+ checkEntityClass(entity.getClass());
+ refresh(entity, ((JPAEntity) entity)._getLockMode(), Collections.emptyMap());
+ }//refresh
+
+ @Override
+ public void refresh(Object entity, Map properties)
+ {
+ checkEntity(entity);
+ checkEntityClass(entity.getClass());
+ refresh(entity, ((JPAEntity) entity)._getLockMode(), properties);
+ }//refresh
+
+ @Override
+ public void refresh(Object entity, LockModeType lockMode)
+ {
+ checkEntity(entity);
+ checkEntityClass(entity.getClass());
+ refresh(entity, lockMode, Collections.emptyMap());
+ }//refresh
+
+ @Override
+ public void refresh(Object entity, LockModeType lockMode, Map properties)
+ {
+ checkOpen();
+ checkEntity(entity);
+ checkTransactionRequired();
+ checkEntityAttached((JPAEntity) entity);
+
+ ((JPAEntity) entity)._setLockMode(lockMode);
+ ((JPAEntity) entity)._refreshEntity(properties);
+ }//refresh
+ //
+
+ //
+ @Override
+ public T find(Class entityClass, Object primaryKey)
+ {
+ return find(entityClass, primaryKey, LockModeType.NONE, null);
+ }
+
+ @Override
+ public T find(Class entityClass, Object primaryKey, Map properties)
+ {
+ return find(entityClass, primaryKey, LockModeType.NONE, properties);
+ }
+
+ @Override
+ public T find(Class entityClass, Object primaryKey, LockModeType lockMode)
+ {
+ return find(entityClass, primaryKey, lockMode, null);
+ }
+
+ @Override
+ public T find(Class entityClass, Object primaryKey, LockModeType lockMode, Map properties)
+ {
+ Span span = TRACER.spanBuilder("EntityManager::find").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ checkOpen();
+ checkEntityClass(entityClass);
+
+ EntityMetaData> metaData = EntityMetaDataManager.getMetaData(entityClass);
+ span.setAttribute("entity", metaData.getName());
+
+ Map hints = new HashMap<>(this.properties);
+ if (properties != null) {
+ hints.putAll(properties);
+ }//if
+
+ EntityQuery entityQuery = new EntitySelectQueryImpl(primaryKey, metaData);
+ JPALiteQueryImpl query = new JPALiteQueryImpl<>(entityQuery.getQuery(),
+ entityQuery.getLanguage(),
+ persistenceContext,
+ entityClass,
+ hints,
+ lockMode);
+ query.setParameter(1, primaryKey);
+ try {
+ return query.getSingleResult();
+ }//try
+ catch (NoResultException ex) {
+ return null;
+ }//catch
+ }//try
+ finally {
+ span.end();
+ }
+ }//find
+
+ @Override
+ public T getReference(Class entityClass, Object primaryKey)
+ {
+ checkEntityClass(entityClass);
+
+ EntityMetaData metaData = EntityMetaDataManager.getMetaData(entityClass);
+ JPAEntity newEntity = (JPAEntity) metaData.getNewEntity();
+ newEntity._makeReference(primaryKey);
+ persistenceContext.l1Cache().manage(newEntity);
+
+ return (T) newEntity;
+ }
+ //
+
+ //
+ @Override
+ public LockModeType getLockMode(Object entity)
+ {
+ checkOpen();
+ checkEntity(entity);
+ checkEntityClass(entity.getClass());
+ checkEntityAttached((JPAEntity) entity);
+
+ return ((JPAEntity) entity)._getLockMode();
+ }
+
+ @Override
+ public void lock(Object entity, LockModeType lockMode)
+ {
+ lock(entity, lockMode, null);
+ }//lock
+
+ @Override
+ public void lock(Object entity, LockModeType lockMode, Map properties)
+ {
+ checkOpen();
+ checkEntity(entity);
+ checkEntityClass(entity.getClass());
+ checkTransactionRequired();
+
+ if (entity instanceof JPAEntity jpaEntity) {
+ jpaEntity._setLockMode(lockMode);
+
+ //For pessimistic locking a select for update query is to be executed
+ if (lockMode == PESSIMISTIC_READ || lockMode == PESSIMISTIC_FORCE_INCREMENT || lockMode == PESSIMISTIC_WRITE) {
+ Map hints = new HashMap<>(this.properties);
+ if (properties != null) {
+ hints.putAll(properties);
+ }//if
+
+ String sqlQuery = "select " +
+ jpaEntity._getMetaData().getIdField().getColumn() +
+ " from " +
+ jpaEntity._getMetaData().getTable() +
+ " where " +
+ jpaEntity._getMetaData().getIdField().getColumn() +
+ "=?";
+
+ JPALiteQueryImpl> query = new JPALiteQueryImpl<>(sqlQuery,
+ QueryLanguage.NATIVE,
+ persistenceContext,
+ jpaEntity._getMetaData().getEntityClass(),
+ hints,
+ lockMode);
+ query.setParameter(1, jpaEntity._getPrimaryKey());
+
+ try {
+ //Lock to row and continue
+ query.getSingleResult();
+ }//try
+ catch (NoResultException ex) {
+ getTransaction().setRollbackOnly();
+ throw new EntityNotFoundException(jpaEntity._getMetaData().getName() + " with key " + jpaEntity._getPrimaryKey() + " not found");
+ }//catch
+ catch (PersistenceException ex) {
+ getTransaction().setRollbackOnly();
+ }//if
+ }//if
+ else {
+ //For optimistic locking we need to flush the entity
+ flush();
+ }//else
+ }//if
+ }//lock
+ //
+
+ //
+ @Override
+ public Query createQuery(String query)
+ {
+ checkOpen();
+ return new QueryImpl(query, persistenceContext, Object[].class, properties);
+ }//createQuery
+
+ @Override
+ public TypedQuery createQuery(String query, Class resultClass)
+ {
+ checkOpen();
+ return new TypedQueryImpl<>(query, QueryLanguage.JPQL, persistenceContext, resultClass, properties);
+ }//createQuery
+
+ @Override
+ public TypedQuery createNamedQuery(String name, Class resultClass)
+ {
+ checkOpen();
+
+ NamedQueries namedQueries = resultClass.getAnnotation(NamedQueries.class);
+ if (namedQueries != null) {
+ for (NamedQuery namedQuery : namedQueries.value()) {
+ if (namedQuery.name().equals(name)) {
+ return new NamedQueryImpl<>(namedQuery, persistenceContext, resultClass, properties);
+ }//if
+ }//for
+ }//if
+
+ NamedQuery namedQuery = resultClass.getAnnotation(NamedQuery.class);
+ if (namedQuery != null && namedQuery.name().equals(name)) {
+ return new NamedQueryImpl<>(namedQuery, persistenceContext, resultClass, properties);
+ }//if
+
+ NamedNativeQueries namedNativeQueries = resultClass.getAnnotation(NamedNativeQueries.class);
+ if (namedNativeQueries != null) {
+ for (NamedNativeQuery nativeQuery : namedNativeQueries.value()) {
+ if (nativeQuery.name().equals(name)) {
+ return new NamedNativeQueryImpl<>(nativeQuery, persistenceContext, resultClass, properties);
+ }//if
+ }//for
+ }//if
+
+ NamedNativeQuery namedNativeQuery = resultClass.getAnnotation(NamedNativeQuery.class);
+ if (namedNativeQuery != null && namedNativeQuery.name().equals(name)) {
+ return new NamedNativeQueryImpl<>(namedNativeQuery, persistenceContext, resultClass, properties);
+ }//if
+
+ throw new IllegalArgumentException("Named query '" + name + "' not found");
+ }//createNamedQuery
+
+ @Override
+ public Query createNativeQuery(String sqlString, Class resultClass)
+ {
+ checkOpen();
+ return new NativeQueryImpl<>(sqlString, persistenceContext, resultClass, properties);
+ }//createNativeQuery
+
+ @Override
+ public Query createNativeQuery(String sqlString)
+ {
+ checkOpen();
+ return new NativeQueryImpl<>(sqlString, persistenceContext, Object.class, properties);
+ }
+
+ @Override
+ public TypedQuery createQuery(CriteriaQuery criteriaQuery)
+ {
+ checkOpen();
+ throw new UnsupportedOperationException(CRITERIA_QUERY_NOT_SUPPORTED);
+ }
+
+ @Override
+ public Query createQuery(CriteriaUpdate updateQuery)
+ {
+ checkOpen();
+ throw new UnsupportedOperationException(CRITERIA_QUERY_NOT_SUPPORTED);
+ }
+
+ @Override
+ public Query createQuery(CriteriaDelete deleteQuery)
+ {
+ checkOpen();
+ throw new UnsupportedOperationException(CRITERIA_QUERY_NOT_SUPPORTED);
+ }
+
+ @Override
+ public Query createNamedQuery(String name)
+ {
+ checkOpen();
+
+ throw new UnsupportedOperationException("Global Named Queries are not supported");
+ }
+
+ @Override
+ public Query createNativeQuery(String sqlString, String resultSetMapping)
+ {
+ checkOpen();
+
+ throw new UnsupportedOperationException("ResultSetMapping is not supported");
+ }//createNativeQuery
+
+ @Override
+ public StoredProcedureQuery createNamedStoredProcedureQuery(String name)
+ {
+ checkOpen();
+
+ throw new UnsupportedOperationException(STORED_PROCEDURE_QUERY_NOT_SUPPORTED);
+ }
+
+ @Override
+ public StoredProcedureQuery createStoredProcedureQuery(String procedureName)
+ {
+ checkOpen();
+
+ throw new UnsupportedOperationException(STORED_PROCEDURE_QUERY_NOT_SUPPORTED);
+ }
+
+ @Override
+ public StoredProcedureQuery createStoredProcedureQuery(String procedureName, Class... resultClasses)
+ {
+ checkOpen();
+
+ throw new UnsupportedOperationException(STORED_PROCEDURE_QUERY_NOT_SUPPORTED);
+ }
+
+ @Override
+ public StoredProcedureQuery createStoredProcedureQuery(String procedureName, String... resultSetMappings)
+ {
+ checkOpen();
+
+ throw new UnsupportedOperationException(STORED_PROCEDURE_QUERY_NOT_SUPPORTED);
+ }
+
+ @Override
+ @SuppressWarnings("java:S4144")//Not an error
+ public CriteriaBuilder getCriteriaBuilder()
+ {
+ checkOpen();
+
+ throw new UnsupportedOperationException(CRITERIA_QUERY_NOT_SUPPORTED);
+ }
+ //
+
+ @Override
+ public void joinTransaction()
+ {
+ checkOpen();
+
+ persistenceContext.joinTransaction();
+ }
+
+ @Override
+ public boolean isJoinedToTransaction()
+ {
+ checkOpen();
+
+ return persistenceContext.isJoinedToTransaction();
+ }
+
+ @Override
+ public Metamodel getMetamodel()
+ {
+ checkOpen();
+ return entityManagerFactory.getMetamodel();
+ }
+
+ @Override
+ public Object getDelegate()
+ {
+ checkOpen();
+ return this;
+ }
+
+ @Override
+ public T unwrap(Class cls)
+ {
+ checkOpen();
+
+ if (cls.isAssignableFrom(this.getClass())) {
+ return (T) this;
+ }
+
+ if (cls.isAssignableFrom(PersistenceContext.class)) {
+ return (T) persistenceContext;
+ }
+
+ throw new IllegalArgumentException("Could not unwrap this [" + this + "] as requested Java type [" + cls.getName() + "]");
+ }
+}//TradeSwitchEntityManagerImpl
+
+//--------------------------------------------------------------------[ End ]---
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/db/ConnectionWrapper.java b/jpalite-core/src/main/java/io/jpalite/impl/db/ConnectionWrapper.java
new file mode 100644
index 0000000..240da72
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/db/ConnectionWrapper.java
@@ -0,0 +1,451 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl.db;
+
+import io.jpalite.DatabasePool;
+import io.jpalite.PersistenceContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.PrintWriter;
+import java.sql.*;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.Executor;
+
+@SuppressWarnings("SqlSourceToSinkFlow")
+public class ConnectionWrapper implements Connection
+{
+ private static final Logger LOG = LoggerFactory.getLogger(ConnectionWrapper.class);
+
+ private final Connection realConnection;
+ private final long slowQueryTimeout;
+ private final PersistenceContext persistenceContext;
+ private PrintWriter auditWriter;
+ private boolean enableLogging;
+ private String connectionName;
+ private final DatabasePool databasePool;
+
+ public ConnectionWrapper(PersistenceContext persistenceContext, Connection realConnection, long slowQueryTimeout)
+ {
+ this.realConnection = realConnection;
+ this.slowQueryTimeout = slowQueryTimeout;
+ this.persistenceContext = persistenceContext;
+ databasePool = this.persistenceContext.unwrap(DatabasePool.class);
+ enableLogging = false;
+ LOG.trace("Opening Connection {}", this.realConnection);
+ }
+
+ @Override
+ public String toString()
+ {
+ return "ConnectionWrapper[" + realConnection + "]";
+ }
+
+ public PersistenceContext getPersistenceContext()
+ {
+ return persistenceContext;
+ }
+
+ public void setName(String name)
+ {
+ connectionName = name;
+ }
+
+ public void realClose() throws SQLException
+ {
+ realConnection.close();
+ }//realClose
+
+ @Override
+ public void close() throws SQLException
+ {
+ persistenceContext.close();
+ }//close
+
+ @Override
+ public void commit() throws SQLException
+ {
+ realConnection.commit();
+ }
+
+ @Override
+ public void rollback() throws SQLException
+ {
+ realConnection.rollback();
+ }
+
+ public long getSlowQueryTimeout()
+ {
+ return slowQueryTimeout;
+ }
+
+ /**
+ * Retrieve the current audit writer
+ *
+ * @return The audit writer or null
+ */
+ public PrintWriter getAuditWriter()
+ {
+ return auditWriter;
+ }
+
+ /**
+ * Set the audit writer to use to record all executed queries in
+ *
+ * @param auditWriter the audit writer to record audit info
+ */
+ public void setAuditWriter(PrintWriter auditWriter)
+ {
+ this.auditWriter = auditWriter;
+ }//setAuditWriter
+
+ public boolean isEnableLogging()
+ {
+ return enableLogging;
+ }
+
+ /**
+ * Set the logging state of the connection returning the previous state
+ *
+ * @param enableLogging The new logging state
+ * @return The previous state
+ */
+ public boolean setEnableLogging(boolean enableLogging)
+ {
+ boolean vPrevState = this.enableLogging;
+ this.enableLogging = enableLogging;
+ return vPrevState;
+ }
+
+ public void setLastQuery(String lastQuery)
+ {
+ persistenceContext.setLastQuery(lastQuery);
+ }
+
+ @Override
+ public Statement createStatement() throws SQLException
+ {
+ return new StatementWrapper(databasePool, connectionName, realConnection.createStatement(), this);
+ }
+
+ @Override
+ public PreparedStatement prepareStatement(String sql) throws SQLException
+ {
+ return new PreparedStatementWrapper(databasePool, connectionName, realConnection.prepareStatement(sql), sql, this);
+ }
+
+ @Override
+ public CallableStatement prepareCall(String sql) throws SQLException
+ {
+ return realConnection.prepareCall(sql);
+ }
+
+ @Override
+ public String nativeSQL(String sql) throws SQLException
+ {
+ return realConnection.nativeSQL(sql);
+ }
+
+ @Override
+ public void setAutoCommit(boolean autoCommit) throws SQLException
+ {
+ realConnection.setAutoCommit(autoCommit);
+ }
+
+ @Override
+ public boolean getAutoCommit() throws SQLException
+ {
+ return realConnection.getAutoCommit();
+ }
+
+ @Override
+ public boolean isClosed() throws SQLException
+ {
+ return realConnection.isClosed();
+ }
+
+ @Override
+ public DatabaseMetaData getMetaData() throws SQLException
+ {
+ return realConnection.getMetaData();
+ }
+
+ @Override
+ public void setReadOnly(boolean readOnly) throws SQLException
+ {
+ realConnection.setReadOnly(readOnly);
+ }
+
+ @Override
+ public boolean isReadOnly() throws SQLException
+ {
+ return realConnection.isReadOnly();
+ }
+
+ @Override
+ public void setCatalog(String catalog) throws SQLException
+ {
+ realConnection.setCatalog(catalog);
+ }
+
+ @Override
+ public String getCatalog() throws SQLException
+ {
+ return realConnection.getCatalog();
+ }
+
+ @Override
+ public void setTransactionIsolation(int level) throws SQLException
+ {
+ realConnection.setTransactionIsolation(level);
+ }
+
+ @Override
+ public int getTransactionIsolation() throws SQLException
+ {
+ return realConnection.getTransactionIsolation();
+ }
+
+ @Override
+ public SQLWarning getWarnings() throws SQLException
+ {
+ return realConnection.getWarnings();
+ }
+
+ @Override
+ public void clearWarnings() throws SQLException
+ {
+ realConnection.clearWarnings();
+ }
+
+ @Override
+ public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException
+ {
+ return new StatementWrapper(databasePool, connectionName, realConnection.createStatement(resultSetType, resultSetConcurrency), this);
+ }
+
+ @Override
+ public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException
+ {
+ return new PreparedStatementWrapper(databasePool, connectionName, realConnection.prepareStatement(sql, resultSetType, resultSetConcurrency), sql, this);
+ }
+
+ @Override
+ public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException
+ {
+ return realConnection.prepareCall(sql, resultSetType, resultSetConcurrency);
+ }
+
+ @Override
+ public Map> getTypeMap() throws SQLException
+ {
+ return realConnection.getTypeMap();
+ }
+
+ @Override
+ public void setTypeMap(Map> map) throws SQLException
+ {
+ realConnection.setTypeMap(map);
+ }
+
+ @Override
+ public void setHoldability(int holdability) throws SQLException
+ {
+ realConnection.setHoldability(holdability);
+ }
+
+ @Override
+ public int getHoldability() throws SQLException
+ {
+ return realConnection.getHoldability();
+ }
+
+ @Override
+ public Savepoint setSavepoint() throws SQLException
+ {
+ return realConnection.setSavepoint();
+ }
+
+ @Override
+ public Savepoint setSavepoint(String name) throws SQLException
+ {
+ return realConnection.setSavepoint();
+ }
+
+ @Override
+ public void rollback(Savepoint savepoint) throws SQLException
+ {
+ realConnection.rollback(savepoint);
+ }
+
+ @Override
+ public void releaseSavepoint(Savepoint savepoint) throws SQLException
+ {
+ realConnection.releaseSavepoint(savepoint);
+ }
+
+ @Override
+ public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException
+ {
+ return new StatementWrapper(databasePool, connectionName, realConnection.createStatement(resultSetType, resultSetConcurrency, resultSetConcurrency), this);
+ }
+
+ @Override
+ public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException
+ {
+ return new PreparedStatementWrapper(databasePool, connectionName, realConnection.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability), sql, this);
+ }
+
+ @Override
+ public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException
+ {
+ return realConnection.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
+ }
+
+ @Override
+ public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException
+ {
+ return new PreparedStatementWrapper(databasePool, connectionName, realConnection.prepareStatement(sql, autoGeneratedKeys), sql, this);
+ }
+
+ @Override
+ public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException
+ {
+ return new PreparedStatementWrapper(databasePool, connectionName, realConnection.prepareStatement(sql, columnIndexes), sql, this);
+ }
+
+ @Override
+ public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException
+ {
+ return new PreparedStatementWrapper(databasePool, connectionName, realConnection.prepareStatement(sql, columnNames), sql, this);
+ }
+
+ @Override
+ public Clob createClob() throws SQLException
+ {
+ return realConnection.createClob();
+ }
+
+ @Override
+ public Blob createBlob() throws SQLException
+ {
+ return realConnection.createBlob();
+ }
+
+ @Override
+ public NClob createNClob() throws SQLException
+ {
+ return realConnection.createNClob();
+ }
+
+ @Override
+ public SQLXML createSQLXML() throws SQLException
+ {
+ return realConnection.createSQLXML();
+ }
+
+ @Override
+ public boolean isValid(int timeout) throws SQLException
+ {
+ return realConnection.isValid(timeout);
+ }
+
+ @Override
+ public void setClientInfo(String name, String value) throws SQLClientInfoException
+ {
+ realConnection.setClientInfo(name, value);
+ }
+
+ @Override
+ public void setClientInfo(Properties properties) throws SQLClientInfoException
+ {
+ realConnection.setClientInfo(properties);
+ }
+
+ @Override
+ public String getClientInfo(String name) throws SQLException
+ {
+ return realConnection.getClientInfo(name);
+ }
+
+ @Override
+ public Properties getClientInfo() throws SQLException
+ {
+ return realConnection.getClientInfo();
+ }
+
+ @Override
+ public Array createArrayOf(String typeName, Object[] elements) throws SQLException
+ {
+ return realConnection.createArrayOf(typeName, elements);
+ }
+
+ @Override
+ public Struct createStruct(String typeName, Object[] attributes) throws SQLException
+ {
+ return realConnection.createStruct(typeName, attributes);
+ }
+
+ @Override
+ public void setSchema(String schema) throws SQLException
+ {
+ realConnection.setSchema(schema);
+ }
+
+ @Override
+ public String getSchema() throws SQLException
+ {
+ return realConnection.getSchema();
+ }
+
+ @Override
+ public void abort(Executor executor) throws SQLException
+ {
+ realConnection.abort(executor);
+ }
+
+ @Override
+ public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException
+ {
+ realConnection.setNetworkTimeout(executor, milliseconds);
+ }
+
+ @Override
+ public int getNetworkTimeout() throws SQLException
+ {
+ return realConnection.getNetworkTimeout();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public T unwrap(Class iface) throws SQLException
+ {
+ if (iface.isAssignableFrom(this.getClass())) {
+ return (T) this;
+ }//if
+
+ return realConnection.unwrap(iface);
+ }
+
+ @Override
+ public boolean isWrapperFor(Class> iface) throws SQLException
+ {
+ return realConnection.isWrapperFor(iface);
+ }
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/db/DatabasePoolFactory.java b/jpalite-core/src/main/java/io/jpalite/impl/db/DatabasePoolFactory.java
new file mode 100644
index 0000000..09c14e6
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/db/DatabasePoolFactory.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl.db;
+
+import io.jpalite.DatabasePool;
+import jakarta.persistence.PersistenceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * The DatabasePoolFactory class is part of the JPALite implementation
+ *
+ * @see TradeSwitch Persistence Manager in Confluence
+ */
+public class DatabasePoolFactory
+{
+ private static final Logger LOG = LoggerFactory.getLogger(DatabasePoolFactory.class);
+
+ private static final Map POOLS = new HashMap<>();
+ private static final ReentrantLock MUTEX = new ReentrantLock();
+
+ private DatabasePoolFactory()
+ {
+ }
+
+ public static DatabasePool getDatabasePool(String dataSourceName)
+ {
+ MUTEX.lock();
+ try {
+ return POOLS.computeIfAbsent(dataSourceName, ds -> {
+ try {
+ return new DatabasePoolImpl(ds);
+ }//try
+ catch (SQLException ex) {
+ LOG.warn("Error loading Database Pool", ex);
+ throw new PersistenceException("Error loading Database Pool");
+ }//catch
+ });
+ }
+ finally {
+ MUTEX.unlock();
+ }
+ }//getDatabasePool
+
+ public static void cleanup()
+ {
+ for (DatabasePool pool : POOLS.values()) {
+ pool.cleanup();
+ }//for
+ }
+}//DatabasePoolFactory
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/db/DatabasePoolImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/db/DatabasePoolImpl.java
new file mode 100644
index 0000000..565f369
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/db/DatabasePoolImpl.java
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl.db;
+
+import io.jpalite.DataSourceProvider;
+import io.jpalite.DatabasePool;
+import io.jpalite.JPALitePersistenceUnit;
+import io.jpalite.PersistenceContext;
+import jakarta.annotation.Nonnull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static io.jpalite.PersistenceContext.PERSISTENCE_JTA_MANAGED;
+
+/**
+ * The DatabasePoolImpl class is part of the TradeSwitch JPA implementation
+ *
+ * @see TradeSwitch Persistence Manager in Confluence
+ */
+public class DatabasePoolImpl implements DatabasePool
+{
+ private static final Logger LOG = LoggerFactory.getLogger(DatabasePoolImpl.class);
+
+ private final ThreadLocal> connections = new ThreadLocal<>();
+ private final String poolName;
+ private final DataSource dataSource;
+ /**
+ * The Database version
+ */
+ private final String dbVersion;
+ /**
+ * The name of the database
+ */
+ private final String dbProductName;
+
+
+ public DatabasePoolImpl(String dataSourceName) throws SQLException
+ {
+ poolName = dataSourceName;
+
+ DataSource workingDataSource = null;
+ ServiceLoader vLoader = ServiceLoader.load(DataSourceProvider.class);
+ for (DataSourceProvider vDataSourceProvider : vLoader) {
+ workingDataSource = vDataSourceProvider.getDataSource(dataSourceName);
+ if (workingDataSource != null) {
+ break;
+ }//if
+ }//for
+ if (workingDataSource == null) {
+ throw new IllegalArgumentException("The data source name '" + dataSourceName + "' is not defined");
+ }//if
+
+ dataSource = workingDataSource;
+ try (Connection connection = dataSource.getConnection()) {
+ dbProductName = connection.getMetaData().getDatabaseProductName();
+ dbVersion = connection.getMetaData().getDatabaseProductVersion();
+ }//try
+ }//DatabasePoolImpl
+
+ @Override
+ public String toString()
+ {
+ return "DatabasePool[" + poolName + "]";
+ }
+
+ @Override
+ public String getPoolName()
+ {
+ return poolName;
+ }//getPoolname
+
+ @Override
+ public PersistenceContext getPersistenceContext(@Nonnull JPALitePersistenceUnit persistenceUnit) throws SQLException
+ {
+ if (persistenceUnit.getProperties().containsKey(PERSISTENCE_JTA_MANAGED) && Boolean.TRUE.equals(persistenceUnit.getProperties().get(PERSISTENCE_JTA_MANAGED))) {
+ LOG.trace("Creating a container managed Persistence Context for thread {}", Thread.currentThread().getName());
+ return new PersistenceContextImpl(this, persistenceUnit);
+ }//if
+
+ Map connectionList = connections.get();
+ PersistenceContext manager = null;
+ if (connectionList == null || connectionList.get(persistenceUnit.getPersistenceUnitName()) == null) {
+ LOG.trace("Creating a new Persistence Context for thread {}", Thread.currentThread().getName());
+ manager = new PersistenceContextImpl(this, persistenceUnit);
+ if (connectionList == null) {
+ connectionList = new ConcurrentHashMap<>();
+ }//if
+ connectionList.put(persistenceUnit.getPersistenceUnitName(), manager);
+ connections.set(connectionList);
+ }//if
+ else {
+ manager = connectionList.get(persistenceUnit.getPersistenceUnitName());
+ LOG.trace("Resuming Persistence Context created for thread {}", Thread.currentThread().getName());
+ }//else
+
+ return manager;
+ }//getConnectionManager
+
+ @Override
+ public Connection getConnection() throws SQLException
+ {
+ return dataSource.getConnection();
+ }//getConnection
+
+ @Override
+ public void cleanup()
+ {
+ Map contextList = connections.get();
+ if (contextList != null) {
+ LOG.trace("Releasing Persistence Context created for thread {}", Thread.currentThread().getName());
+ connections.remove();
+ contextList.values().forEach(PersistenceContext::release);
+ }//if
+ }//cleanup
+
+ @Override
+ public String getDbVersion()
+ {
+ return dbVersion;
+ }
+
+ @Override
+ public String getDbProductName()
+ {
+ return dbProductName;
+ }
+}//DatabasePool
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/db/PersistenceContextImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/db/PersistenceContextImpl.java
new file mode 100644
index 0000000..babb2ff
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/db/PersistenceContextImpl.java
@@ -0,0 +1,1151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl.db;
+
+import io.jpalite.PersistenceContext;
+import io.jpalite.*;
+import io.jpalite.impl.EntityL1LocalCacheImpl;
+import io.jpalite.impl.EntityL2CacheImpl;
+import io.jpalite.impl.queries.EntityDeleteQueryImpl;
+import io.jpalite.impl.queries.EntityInsertQueryImpl;
+import io.jpalite.impl.queries.EntityUpdateQueryImpl;
+import io.jpalite.queries.EntityQuery;
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.StatusCode;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.context.Scope;
+import io.quarkus.runtime.Application;
+import jakarta.annotation.Nonnull;
+import jakarta.enterprise.inject.spi.CDI;
+import jakarta.persistence.RollbackException;
+import jakarta.persistence.TransactionRequiredException;
+import jakarta.persistence.*;
+import jakarta.transaction.*;
+import org.eclipse.microprofile.config.ConfigProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.PrintWriter;
+import java.sql.*;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static io.jpalite.JPALiteEntityManager.JPALITE_SHOW_SQL;
+import static io.jpalite.JPALiteEntityManager.PERSISTENCE_QUERY_LOG_SLOWTIME;
+import static io.jpalite.PersistenceAction.*;
+
+/**
+ * The persistence context is responsible for managing the connection, persisting entities to the database and keeps
+ * tract of transaction blocks started and needs to do the cleanup on close.
+ */
+public class PersistenceContextImpl implements PersistenceContext
+{
+ private static final Logger LOG = LoggerFactory.getLogger(PersistenceContextImpl.class);
+ private static final Tracer TRACER = GlobalOpenTelemetry.get().getTracer(PersistenceContextImpl.class.getName());
+ /**
+ * The database pool we belong to
+ */
+ private final DatabasePool pool;
+ /**
+ * Control counter to manage transaction depth. Every call to {@link #begin()} will increment it and calls to
+ * {@link #commit()} and {@link #rollback()} will decrement it.
+ */
+ private final AtomicInteger transactionDepth;
+ /**
+ * Control variable to record the current {@link #transactionDepth}.
+ */
+ private final Deque openStack;
+ /**
+ * The connection name used to open a new connection
+ */
+ private final Deque connectionNames;
+ /**
+ * Stack for all save points created by beginTrans()
+ */
+ private final Deque savepoints;
+ /**
+ * The level 1 cache
+ */
+ private final EntityLocalCache entityL1Cache;
+ /**
+ * The level 2 cache
+ */
+ private final EntityCache entityL2Cache;
+ /**
+ * List of all callback listeners
+ */
+ private final List listeners;
+ /**
+ * List of callback listeners to add. This list is populated if a new listener is removed form with in a callback.
+ */
+ private final List pendingAdd;
+ /**
+ * List of callback listeners to delete. This list is populated if a listener is removed form with in a callback.
+ */
+ private final List pendingRemoval;
+ /**
+ * The connection assigned to the manager
+ */
+ private ConnectionWrapper connection;
+ /**
+ * The last query executed in by the connection
+ */
+ private String lastQuery;
+ /**
+ * The current connection name assigned to the connection
+ */
+ private String connectionName;
+ /**
+ * The execution time after which queries are considered run too slowly
+ */
+ long slowQueryTime;
+ /**
+ * If true create a connection that shows the SQL
+ */
+ boolean showSql;
+ /**
+ * Control variable to indicated that we have forced rollback
+ */
+ private boolean rollbackOnly = false;
+ /**
+ * Read only indicator
+ */
+ private boolean readOnly;
+ /**
+ * Control variable to make sure that a transaction callback does not call begin, commit or rollback
+ */
+ private boolean inCallbackHandler;
+ /**
+ * The JTA transaction manager
+ */
+ private TransactionManager transactionManager;
+ /**
+ * True if join to a JTA transaction
+ */
+ private boolean joinedToTransaction;
+ /**
+ * True if the context should automatically detect and join a JTA managed transaction.
+ */
+ private boolean autoJoinTransaction;
+ /**
+ * The persistence context properties
+ */
+ private final Map properties;
+ /**
+ * The persistence unit used to create the context
+ */
+ private final JPALitePersistenceUnit persistenceUnit;
+ private final long threadId;
+ private final long instanceNr;
+ private static final AtomicLong instanceCount = new AtomicLong(0);
+ private boolean released;
+ private final String hostname;
+
+ private enum CallbackMethod
+ {
+ PRE_BEGIN,
+ POST_BEGIN,
+ PRE_COMMIT,
+ POST_COMMIT,
+ PRE_ROLLBACK,
+ POST_ROLLBACK
+ }
+
+ public PersistenceContextImpl(DatabasePool pool, JPALitePersistenceUnit persistenceUnit)
+ {
+ this.pool = pool;
+ readOnly = false;
+ this.persistenceUnit = persistenceUnit;
+ properties = new HashMap<>();
+ listeners = new ArrayList<>();
+ pendingAdd = new ArrayList<>();
+ pendingRemoval = new ArrayList<>();
+ transactionDepth = new AtomicInteger(0);
+ instanceNr = instanceCount.incrementAndGet();
+ openStack = new ArrayDeque<>();
+ connectionNames = new ArrayDeque<>();
+ savepoints = new ArrayDeque<>();
+ connectionName = Thread.currentThread().getName();
+ slowQueryTime = 500L;
+ joinedToTransaction = false;
+ autoJoinTransaction = false;
+ transactionManager = null;
+ showSql = false;
+ released = false;
+
+ hostname = ConfigProvider.getConfig().getOptionalValue("HOSTNAME", String.class).orElse("localhost");
+ threadId = Thread.currentThread().threadId();
+ persistenceUnit.getProperties().forEach((k, v) -> setProperty(k.toString(), v));
+
+ entityL1Cache = new EntityL1LocalCacheImpl(this);
+
+ entityL2Cache = new EntityL2CacheImpl(this.persistenceUnit);
+
+
+ LOG.debug("Created {}", this);
+ }//PersistenceContextImpl
+
+ @Override
+ public JPALitePersistenceUnit getPersistenceUnit()
+ {
+ return persistenceUnit;
+ }//getPersistenceUnit
+
+ @Override
+ public void setProperty(String name, Object value)
+ {
+ switch (name) {
+ case PERSISTENCE_JTA_MANAGED -> {
+ if (value instanceof String strValue) {
+ value = Boolean.parseBoolean(strValue);
+ }//if
+ if (value instanceof Boolean jtaManaged) {
+ autoJoinTransaction = jtaManaged;
+ }//if
+ }
+ case PERSISTENCE_QUERY_LOG_SLOWTIME -> {
+ if (value instanceof String strValue) {
+ value = Long.parseLong(strValue);
+ }//if
+ if (value instanceof Long slowQuery) {
+ slowQueryTime = slowQuery;
+ }//if
+ }
+ case JPALITE_SHOW_SQL -> {
+ if (value instanceof String strValue) {
+ value = Boolean.parseBoolean(strValue);
+ }//if
+ if (value instanceof Boolean showQuerySql) {
+ this.showSql = showQuerySql;
+ if (connection != null) {
+ connection.setEnableLogging(this.showSql);
+ }//if
+ }//if
+ }
+ default -> {
+ //ignore the rest
+ }
+ }//switch
+
+ properties.put(name, value);
+ }
+
+ @Override
+ public Map getProperties()
+ {
+ return properties;
+ }
+
+ @Override
+ public boolean supportedEntityType(EntityType entityType)
+ {
+ //We only support database entities
+ return EntityType.ENTITY_DATABASE.equals(entityType);
+ }//supportEntityType
+
+ @Override
+ public EntityLocalCache l1Cache()
+ {
+ return entityL1Cache;
+ }//l1Cache
+
+ @Override
+ public EntityCache l2Cache()
+ {
+ return entityL2Cache;
+ }//l2Cache
+
+ private void checkEntityAttached(JPAEntity entity)
+ {
+ if (entity._getEntityState() != EntityState.MANAGED) {
+ throw new IllegalArgumentException("Entity is not current attached to a Persistence Context");
+ }//if
+
+ if (entity._getPersistenceContext() != this) {
+ throw new IllegalArgumentException("Entity is not being managed by this Persistence Context");
+ }//if
+ }//checkEntityAttached
+
+ private void checkRecursiveCallback()
+ {
+ if (inCallbackHandler) {
+ throw new PersistenceException("The EntityTransaction methods begin, commit and rollback cannot be called from within a EntityListener callback");
+ }//if
+ }//checkRecursiveCallback
+
+ private void checkThread()
+ {
+ if (threadId != Thread.currentThread().threadId()) {
+ throw new IllegalStateException("Persistence Context is assigned different thread. Expected " + threadId + ", calling thread is " + Thread.currentThread().threadId());
+ }//if
+ }//checkThread
+
+ private void checkReleaseState()
+ {
+ if (released) {
+ throw new PersistenceException("Persistence Context has detached from the database pool cannot be used");
+ }//if
+ }//checkReleaseState
+
+ private void checkOpen()
+ {
+ checkReleaseState();
+
+ if (connection == null) {
+ throw new IllegalStateException("Persistence Context is closed.");
+ }//if
+ }//checkOpen
+
+ @Override
+ public String toString()
+ {
+ return "Persistence Context " + instanceNr + " [Stack " + openStack.size() + ", " + pool + "]";
+ }//toString
+
+ @Override
+ public void addTransactionListener(EntityTransactionListener listener)
+ {
+ if (inCallbackHandler) {
+ pendingAdd.add(listener);
+ }//if
+ else {
+ listeners.add(listener);
+ }//else
+ }//addTransactionListener
+
+ @Override
+ public void removeTransactionListener(EntityTransactionListener listener)
+ {
+ if (inCallbackHandler) {
+ pendingRemoval.add(listener);
+ }//if
+ else {
+ listeners.remove(listener);
+ }//else
+ }//removeTransactionListener
+
+ @Override
+ public void setLastQuery(String lastQuery)
+ {
+ this.lastQuery = lastQuery;
+ }
+
+ @Override
+ public String getLastQuery()
+ {
+ return lastQuery;
+ }
+
+ @Override
+ public int getTransactionDepth()
+ {
+ return transactionDepth.get();
+ }
+
+ @Override
+ public int getOpenLevel()
+ {
+ return openStack.size();
+ }
+
+ @Override
+ public String getConnectionName()
+ {
+ return connectionName;
+ }
+
+ @Override
+ public void setConnectionName(String connectionName)
+ {
+ this.connectionName = connectionName;
+ }
+
+ @SuppressWarnings({"java:S1141", "java:S2077"})
+ //Having try-resource in a bigger try block is allowed. Dynamically formatted SQL is verified to be safe
+ @Override
+ @Nonnull
+ public Connection getConnection(String connectionName)
+ {
+ checkReleaseState();
+ checkThread();
+
+ openStack.push(transactionDepth.get());
+ connectionNames.push(this.connectionName);
+
+ if (connectionName == null) {
+ if (this.connectionName == null) {
+ this.connectionName = Thread.currentThread().getName();
+ }//if
+ }//if
+ else {
+ this.connectionName = connectionName;
+ }//else
+ LOG.trace("Opening persistence context. Level: {} with cursor {}", openStack.size(), this.connectionName);
+
+ if (connection == null) {
+ try {
+ connection = new ConnectionWrapper(this, pool.getConnection(), slowQueryTime);
+
+ try (Statement writeStmt = connection.createStatement()) {
+ String applicationName = Application.currentApplication().getName() + "@" + hostname;
+ if (applicationName.length() > 61) {
+ applicationName = applicationName.substring(0, 61);
+ }//if
+ writeStmt.execute("set application_name to '" + applicationName + "'");
+ }//try
+ catch (SQLException ex) {
+ LOG.error("Error setting the JDBC application name", ex);
+ }//catch
+
+ connection.setEnableLogging(showSql);
+ }//try
+ catch (SQLException ex) {
+ throw new PersistenceException("Error configuring database connection", ex);
+ }//catch
+ }//if
+
+ connection.setName(this.connectionName);
+
+ if (isAutoJoinTransaction()) {
+ joinTransaction();
+ }//if
+
+ return connection;
+ }//getConnection
+
+ @Override
+ public boolean isReleased()
+ {
+ return released;
+ }//if
+
+ @Override
+ public void release()
+ {
+ checkThread();
+
+ if (connection != null) {
+ LOG.warn("Closing unexpected open transaction on {}", connection, new PersistenceException("Possible unhandled exception"));
+ openStack.clear();
+ connectionNames.clear();
+
+ close();
+ }//if
+
+ released = true;
+ }//release
+
+ @Override
+ public void close()
+ {
+ checkOpen();
+ checkThread();
+
+ LOG.trace("Closing connection level: {}", openStack.size());
+ if (!connectionNames.isEmpty()) {
+ connectionName = connectionNames.pop();
+ }//if
+
+ if (!openStack.isEmpty()) {
+ int transDepth = openStack.pop();
+ if (transDepth < this.transactionDepth.get()) {
+ LOG.warn("Closing unexpected open transaction", new PersistenceException("Possible unhandled exception"));
+ rollbackToDepth(transDepth);
+
+ //Check if the rollback closed the connection, if so we are done
+ if (connection == null) {
+ return;
+ }//if
+ }//if
+ }//if
+
+ if (openStack.isEmpty()) {
+ LOG.trace("At level 0, releasing connection {}", connection);
+
+ l1Cache().clear();
+ openStack.clear();
+ connectionNames.clear();
+ savepoints.clear();
+ transactionDepth.set(0);
+ rollbackOnly = false;
+ readOnly = false;
+ try {
+ if (!connection.isClosed() && !connection.getAutoCommit()) {
+ connection.rollback();
+ connection.setAutoCommit(true);
+ }//if
+ connection.realClose();
+ connection = null;
+ }//try
+ catch (SQLException ex) {
+ LOG.error("Error closing connection", ex);
+ }//catch
+ }//if
+
+ }//close
+
+ @Override
+ public X mapResultSet(@Nonnull X entity, ResultSet resultSet)
+ {
+ return mapResultSet(entity, null, resultSet);
+ }
+
+ @Override
+ public X mapResultSet(@Nonnull X entity, String colPrefix, ResultSet resultSet)
+ {
+ ((JPAEntity) entity)._setPersistenceContext(this);
+ ((JPAEntity) entity)._mapResultSet(colPrefix, resultSet);
+ l1Cache().manage((JPAEntity) entity);
+ return entity;
+ }
+
+ private boolean doesNeedFlushing(JPAEntity entity)
+ {
+ if (entity._getPersistenceContext() != this) {
+ throw new PersistenceException("Entity belongs to another persistence context and cannot be updated. I am [" + this + "], Entity [" + entity + "]");
+ }//if
+
+ return entity._getEntityState() == EntityState.MANAGED && (entity._getPendingAction() != PersistenceAction.NONE || entity._getLockMode() == LockModeType.OPTIMISTIC_FORCE_INCREMENT);
+ }//doesNeedFlushing
+
+ @Override
+ public void flush()
+ {
+ checkOpen();
+ checkThread();
+
+ l1Cache().foreach(e ->
+ {
+ if (doesNeedFlushing((JPAEntity) e)) {
+ flushEntityInternal((JPAEntity) e);
+ }//if
+ });
+ }//flush
+
+ @Override
+ public void flushOnType(Class> entityClass)
+ {
+ checkOpen();
+ checkThread();
+
+ l1Cache().foreachType(entityClass, e ->
+ {
+ if (doesNeedFlushing((JPAEntity) e)) {
+ flushEntityInternal((JPAEntity) e);
+ }//if
+ });
+ }//flushOnType
+
+ @Override
+ public void flushEntity(@Nonnull JPAEntity entity)
+ {
+ checkOpen();
+ checkThread();
+ checkEntityAttached(entity);
+
+ flushEntityInternal(entity);
+ }
+
+ @SuppressWarnings("java:S6205") //Not a redundant block
+ private void invokeCallbackHandlers(PersistenceAction action, boolean preAction, Object entity)
+ {
+ /*
+ * Callback are not invoked if the transaction is marked for rollback
+ */
+ if (!getRollbackOnly()) {
+ EntityMetaData> metaData = EntityMetaDataManager.getMetaData(entity.getClass());
+ try {
+ switch (action) {
+ case INSERT -> {
+ if (preAction) {
+ metaData.getLifecycleListeners().prePersist(entity);
+ }
+ else {
+ metaData.getLifecycleListeners().postPersist(entity);
+ }
+ }
+ case UPDATE -> {
+ if (preAction) {
+ metaData.getLifecycleListeners().preUpdate(entity);
+ }
+ else {
+ metaData.getLifecycleListeners().postUpdate(entity);
+ }
+ }
+ case DELETE -> {
+ if (preAction) {
+ metaData.getLifecycleListeners().preRemove(entity);
+ }
+ else {
+ metaData.getLifecycleListeners().postRemove(entity);
+ }
+ }
+ default -> {//do nothing
+ }
+ }//switch
+ }//try
+ catch (PersistenceException ex) {
+ setRollbackOnly();
+ throw ex;
+ }//catch
+ }//if
+ }//invokeCallbackHandlers
+
+ private void bindParameters(PreparedStatement statement, Object... params)
+ {
+ if (params != null) {
+ int startAt = 0;
+
+ for (Object param : params) {
+ try {
+ startAt++;
+
+ if (param instanceof Boolean) {
+ param = param == Boolean.TRUE ? 1 : 0;
+ }//if
+ if (param instanceof byte[] vBytes) {
+ statement.setBytes(startAt, vBytes);
+ }//if
+ else {
+ statement.setObject(startAt, param, Types.OTHER);
+ }//else
+ }//try
+ catch (SQLException ex) {
+ throw new PersistenceException("Error setting parameter (" + startAt + "=" + param, ex);
+ }//catch
+ }//for
+ }//if
+ }//bindParameters
+
+ private boolean isOptimisticLocked(JPAEntity entity)
+ {
+ return (entity._getLockMode() == LockModeType.OPTIMISTIC || entity._getLockMode() == LockModeType.OPTIMISTIC_FORCE_INCREMENT);
+ }//isOptimisticLocked
+
+ @SuppressWarnings("unchecked")
+ private void cascadePersist(Set mappings, @Nonnull JPAEntity entity)
+ {
+ try {
+ for (EntityField field : entity._getMetaData().getEntityFields()) {
+ if ((field.getCascade().contains(CascadeType.ALL) || field.getCascade().contains(CascadeType.PERSIST))) {
+
+ if (field.getMappingType() == MappingType.ONE_TO_MANY && mappings.contains(MappingType.ONE_TO_MANY)) {
+ List entityList = (List) field.invokeGetter(entity);
+ if (entityList != null) {
+ entityList.stream()
+ //Check if the entity is new and unattached or was persisted but not flushed
+ .filter(e -> (e._getEntityState() == EntityState.TRANSIENT || e._getPendingAction() == PersistenceAction.INSERT))
+ .forEach(e ->
+ {
+ try {
+ EntityField entityField = e._getMetaData().getEntityField(field.getMappedBy());
+ entityField.invokeSetter(e, entity);
+ e._setPendingAction(PersistenceAction.INSERT);
+ l1Cache().manage(e);
+ flushEntity(e);
+ }//try
+ catch (RuntimeException ex) {
+ setRollbackOnly();
+ throw new PersistenceException("Error cascading persist entity", ex);
+ }//catch
+ });
+ }//if
+ entity._clearField(field.getName());
+ }//if
+ else if ((field.getMappingType() == MappingType.MANY_TO_ONE && mappings.contains(MappingType.MANY_TO_ONE) || (field.getMappingType() == MappingType.ONE_TO_ONE && mappings.contains(MappingType.ONE_TO_ONE)))) {
+ JPAEntity jpaEntity = (JPAEntity) field.invokeGetter(entity);
+ flushEntity(jpaEntity);
+ }//else if
+
+ }//if
+ }//for
+ }//try
+ catch (RuntimeException ex) {
+ setRollbackOnly();
+ throw new PersistenceException("Error cascading persist entity", ex);
+ }//catch
+ }//cascadePersist
+
+ @SuppressWarnings("unchecked")
+ private void cascadeRemove(Set mappings, @Nonnull JPAEntity entity)
+ {
+ try {
+ for (EntityField field : entity._getMetaData().getEntityFields()) {
+
+ if (mappings.contains(field.getMappingType()) && (field.getCascade().contains(CascadeType.ALL) || field.getCascade().contains(CascadeType.REMOVE))) {
+ if (mappings.contains(MappingType.MANY_TO_ONE) || mappings.contains(MappingType.ONE_TO_ONE)) {
+ JPAEntity entityValue = (JPAEntity) field.invokeGetter(entity);
+ if (entityValue != null && !entityValue._isLazyLoaded()) {
+ entityValue._setPendingAction(DELETE);
+ flushEntity(entityValue);
+ }//if
+ }//if
+ else if (mappings.contains(MappingType.ONE_TO_MANY)) {
+ List entityList = (List) field.invokeGetter(entity);
+ if (entityList != null) {
+ entityList.stream()
+ .filter(e -> (!e._isLazyLoaded()))
+ .forEach(e ->
+ {
+ e._setPendingAction(DELETE);
+ flushEntity(e);
+ });
+ }//if
+ }//else if
+ }//if
+ }//for
+ }//try
+ catch (RuntimeException ex) {
+ setRollbackOnly();
+ throw new PersistenceException("Error cascading remove entity", ex);
+ }//catch
+ }//cascadeRemove
+
+ private EntityQuery getFlushQuery(PersistenceAction action, @Nonnull JPAEntity entity)
+ {
+ EntityMetaData> metaData = EntityMetaDataManager.getMetaData(entity.getClass());
+ return switch (action) {
+ case INSERT -> {
+ cascadePersist(Set.of(MappingType.MANY_TO_ONE), entity);
+ yield new EntityInsertQueryImpl(entity, metaData);
+ }
+ case UPDATE -> new EntityUpdateQueryImpl(entity, metaData);
+ case DELETE -> {
+ cascadeRemove(Set.of(MappingType.ONE_TO_MANY, MappingType.ONE_TO_ONE), entity);
+ yield new EntityDeleteQueryImpl(entity, metaData);
+ }
+ default -> throw new IllegalStateException("Unexpected value: " + action);
+ };
+ }//getFlushQuery
+
+ private void flushEntityInternal(@Nonnull JPAEntity entity)
+ {
+ PersistenceAction action = entity._getPendingAction();
+ if (action == NONE) {
+ /*
+ If the entity is not new and is not dirty but is locked optimistically, we need to update the version
+ */
+ if (entity._getLockMode() == LockModeType.OPTIMISTIC_FORCE_INCREMENT) {
+ action = UPDATE;
+ }//if
+ else {
+ return;
+ }//else
+ }//if
+
+ Span span = TRACER.spanBuilder("PersistenceContextImpl::flushEntity").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ span.setAttribute("action", action.name());
+ invokeCallbackHandlers(action, true, entity);
+ if (!getRollbackOnly()) {
+ entity._setPendingAction(NONE);
+ EntityQuery flushQuery = getFlushQuery(action, entity);
+
+ if (flushQuery.getQuery() != null && !flushQuery.getQuery().isBlank()) {
+ String sqlQuery = flushQuery.getQuery();
+ span.setAttribute("query", sqlQuery);
+
+ //noinspection SqlSourceToSinkFlow
+ try (PreparedStatement statement = connection.prepareStatement(sqlQuery, Statement.RETURN_GENERATED_KEYS)) {
+ bindParameters(statement, flushQuery.getParameters());
+
+ int rows = statement.executeUpdate();
+ if (rows > 0) {
+ if (action == PersistenceAction.DELETE) {
+ entity._setEntityState(EntityState.REMOVED);
+ if (entity._getMetaData().isCacheable()) {
+ l2Cache().remove(entity);
+ }//if
+
+ cascadeRemove(Set.of(MappingType.MANY_TO_ONE), entity);
+ }//if
+ else {
+ if (action == PersistenceAction.INSERT) {
+ try (ResultSet vResultSet = statement.getGeneratedKeys()) {
+ if (vResultSet.next()) {
+ entity._setPersistenceContext(this);
+ entity._mapResultSet(null, vResultSet);
+ }//if
+ }//try
+ }//if
+ else if (entity._getMetaData().isCacheable()) {
+ l2Cache().update(entity);
+ }//else if
+
+ cascadePersist(Set.of(MappingType.ONE_TO_MANY, MappingType.ONE_TO_ONE), entity);
+ }//else
+ }//if
+ /*
+ * If zero rows were updated or deleted and the entity was optimistic locked, then throw an exception
+ */
+ else if (action != INSERT && isOptimisticLocked(entity)) {
+ setRollbackOnly();
+ throw new OptimisticLockException(entity);
+ }//else if
+ }//try
+ catch (SQLException ex) {
+ setRollbackOnly();
+
+ LOG.error("Failed to flush entity {}, Query: {}", entity._getMetaData().getName(), flushQuery.getQuery(), ex);
+ throw new PersistenceException("Error persisting entity in database");
+ }//catch
+ }//if
+ }//if
+
+ entity._clearModified();
+ invokeCallbackHandlers(action, false, entity);
+ }//try
+ finally {
+ span.end();
+ }//finally
+ }//flushEntity
+
+
+ //
+ @Override
+ public void setAutoJoinTransaction()
+ {
+ autoJoinTransaction = true;
+ }//setAutoJoinTransaction
+
+ @Override
+ public boolean isAutoJoinTransaction()
+ {
+ return autoJoinTransaction;
+ }//isAutoJoinTransactions
+
+ @Override
+ public void joinTransaction()
+ {
+ if (!joinedToTransaction) {
+ try {
+ if (transactionManager == null) {
+ transactionManager = (TransactionManager) CDI.current().select(getClass().getClassLoader().loadClass(TransactionManager.class.getName())).get();
+ if (transactionManager == null) {
+ throw new ClassNotFoundException("Transaction Manager not set");
+ }//if
+ }//if
+
+ //If we not in a JTA transaction, escape here
+ if (!isInJTATransaction()) {
+ return;
+ }//if
+
+ joinedToTransaction = true;
+
+ switch (transactionManager.getStatus()) {
+ case Status.STATUS_ACTIVE, Status.STATUS_PREPARED, Status.STATUS_PREPARING -> begin();
+ case Status.STATUS_MARKED_ROLLBACK -> {
+ begin();
+ setRollbackOnly();
+ }
+ default ->
+ throw new TransactionRequiredException("Explicitly joining a JTA transaction requires a JTA transaction be currently active");
+ }//switch
+ }//try
+ catch (ClassNotFoundException ex) {
+ throw new PersistenceException("No JTA TransactionManager found, mostly likely this is not an EE application", ex);
+ }//catch
+ catch (SystemException ex) {
+ throw new PersistenceException(ex.getMessage(), ex);
+ }//catch
+ }//if
+ }//joinTransaction
+
+ @Override
+ public boolean isJoinedToTransaction()
+ {
+ return joinedToTransaction;
+ }
+
+ private boolean isInJTATransaction()
+ {
+ if (transactionManager != null) {
+ try {
+ int status = transactionManager.getStatus();
+ return (status == Status.STATUS_ACTIVE || status == Status.STATUS_COMMITTING || status == Status.STATUS_MARKED_ROLLBACK || status == Status.STATUS_PREPARED || status == Status.STATUS_PREPARING);
+ }
+ catch (Exception ex) {
+ throw new PersistenceException(ex);
+ }
+ }//if
+ return false;
+ }//joinTransaction
+
+
+ @Override
+ public void afterCompletion(int status)
+ {
+ if (isActive() && status == Status.STATUS_ROLLEDBACK) {
+ setRollbackOnly();
+ rollback();
+ }//if
+ }//afterCompletion
+
+ private void rollbackToDepth(int depth)
+ {
+ while (transactionDepth.get() > depth) {
+ rollback();
+ }//while
+ }//rollbackToDepth
+
+ @Override
+ public EntityTransaction getTransaction()
+ {
+ if (isAutoJoinTransaction() || isJoinedToTransaction()) {
+ throw new IllegalStateException("Transaction is not accessible when using JTA with JPA-compliant transaction access enabled");
+ }//if
+
+ return this;
+ }//getTransaction
+
+ private void transactionCallback(CallbackMethod callback)
+ {
+ checkRecursiveCallback();
+
+ inCallbackHandler = true;
+ for (EntityTransactionListener listener : listeners) {
+ switch (callback) {
+ case PRE_BEGIN -> listener.preTransactionBeginEvent();
+ case POST_BEGIN -> listener.postTransactionBeginEvent();
+ case PRE_COMMIT -> listener.preTransactionCommitEvent();
+ case POST_COMMIT -> listener.postTransactionCommitEvent();
+ case PRE_ROLLBACK -> listener.preTransactionRollbackEvent();
+ case POST_ROLLBACK -> listener.postTransactionRollbackEvent();
+ }//switch
+ }//for
+ inCallbackHandler = false;
+
+ if (!pendingRemoval.isEmpty()) {
+ pendingRemoval.forEach(listeners::remove);
+ }//if
+
+ if (!pendingAdd.isEmpty()) {
+ listeners.addAll(pendingAdd);
+ }//if
+ }//transactionCallback
+
+ @Override
+ public void begin()
+ {
+ checkReleaseState();
+ checkThread();
+ Span span = TRACER.spanBuilder("PersistenceContextImpl::begin").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignore = span.makeCurrent()) {
+ checkRecursiveCallback();
+
+ if (isActive()) {
+ if (getRollbackOnly()) {
+ throw new IllegalStateException("Transaction is current in a rollback only state");
+ }//if
+ LOG.trace("Set a savepoint at depth {}", transactionDepth.get());
+ savepoints.add(connection.setSavepoint());
+ transactionDepth.incrementAndGet();
+ LOG.debug("Legacy support - Transaction is already active, using depth counter");
+ }//if
+ else {
+ LOG.trace("Beginning a new transaction on {}", this);
+ transactionCallback(CallbackMethod.PRE_BEGIN);
+ rollbackOnly = false;
+ getConnection(connectionName).setAutoCommit(false);
+ transactionDepth.set(1);
+ l2Cache().begin();
+ transactionCallback(CallbackMethod.POST_BEGIN);
+ }//else
+ }//try
+ catch (SQLException ex) {
+ throw new PersistenceException("Error beginning a transaction", ex);
+ }//catch
+ catch (SystemException | NotSupportedException ex) {
+ throw new PersistenceException("Error beginning a transaction in TransactionManager", ex);
+ }//catch
+ finally {
+ span.end();
+ }
+ }//begin
+
+ @Override
+ public void commit()
+ {
+ checkThread();
+
+ if (isActive()) {
+ Span span = TRACER.spanBuilder("PersistenceContextImpl::commit").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ if (getRollbackOnly()) {
+ span.setStatus(StatusCode.ERROR, "Transaction marked for rollback and cannot be committed");
+ throw new RollbackException("Transaction marked for rollback and cannot be committed");
+ }//if
+
+ if (transactionDepth.decrementAndGet() > 0) {
+ if (LOG.isTraceEnabled()) {
+ LOG.trace("Commit savepoint at depth {}", transactionDepth.get());
+ }//if
+ savepoints.removeLast();
+ return;
+ }//if
+
+ transactionCallback(CallbackMethod.PRE_COMMIT);
+
+ flush();
+ connection.commit();
+ connection.setAutoCommit(true);
+ l1Cache().clear();
+ l2Cache().commit();
+
+ transactionCallback(CallbackMethod.POST_COMMIT);
+ close();
+ LOG.trace("Transaction Committed on {}", this);
+ }//try
+ catch (SQLException ex) {
+ setRollbackOnly();
+ throw new PersistenceException("Error committing transaction", ex);
+ }//catch
+ catch (SystemException | jakarta.transaction.RollbackException |
+ HeuristicMixedException | HeuristicRollbackException ex) {
+ setRollbackOnly();
+ throw new PersistenceException("Error committing transaction in TransactionManager", ex);
+ }//catch
+ finally {
+ span.end();
+ }//finally
+ }//if
+ transactionDepth.set(0);
+ }//commit
+
+ @Override
+ public void rollback()
+ {
+ checkThread();
+ Span span = TRACER.spanBuilder("PersistenceContextImpl::rollback").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ if (isActive()) {
+ if (transactionDepth.decrementAndGet() > 0) {
+ connection.rollback(savepoints.pop());
+ if (LOG.isTraceEnabled()) {
+ LOG.trace("Rolling back to savepoint at depth {}", transactionDepth.get());
+ }//if
+ rollbackOnly = false;
+ return;
+ }//if
+
+ transactionCallback(CallbackMethod.PRE_ROLLBACK);
+
+ rollbackOnly = false;
+ connection.rollback();
+ l2Cache().rollback();
+ connection.setAutoCommit(true);
+
+ l1Cache().clear();
+ transactionCallback(CallbackMethod.POST_ROLLBACK);
+
+ close();
+ LOG.trace("Transaction rolled back on {}", this);
+ }//if
+ }//try
+ catch (SQLException ex) {
+ throw new PersistenceException("Error rolling transaction back", ex);
+ }//catch
+ catch (SystemException ex) {
+ throw new PersistenceException("Error rolling transaction back in TransactionManager", ex);
+ }//catch
+ finally {
+ span.end();
+ }//finally
+ }//rollback
+
+ @Override
+ public void setRollbackOnly()
+ {
+ rollbackOnly = true;
+ }
+
+ @Override
+ public boolean getRollbackOnly()
+ {
+ return rollbackOnly;
+ }
+
+ @Override
+ public boolean isActive()
+ {
+ return transactionDepth.get() > 0;
+ }
+ //
+
+ //
+ public long getSlowQueryTime()
+ {
+ return slowQueryTime;
+ }
+
+ public void setSlowQueryTime(long pSlowQueryTime)
+ {
+ slowQueryTime = pSlowQueryTime;
+ }
+
+ public boolean isEnableLogging()
+ {
+ return connection.isEnableLogging();
+ }
+
+ public void setEnableLogging(boolean pEnableLogging)
+ {
+ checkOpen();
+ connection.setEnableLogging(pEnableLogging && showSql);
+ }//setEnableLogging
+
+ public boolean isReadonly()
+ {
+ return readOnly;
+ }
+
+ public void setReadonly(boolean pReadonly)
+ {
+ readOnly = pReadonly;
+ }
+
+ public void setAuditWriter(PrintWriter pAuditWriter)
+ {
+ connection.setAuditWriter(pAuditWriter);
+ }
+
+ public PrintWriter getAuditWriter()
+ {
+ return connection.getAuditWriter();
+ }
+ //
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public T unwrap(Class cls)
+ {
+ if (cls.isAssignableFrom(this.getClass())) {
+ return (T) this;
+ }
+
+ if (cls.isAssignableFrom(pool.getClass())) {
+ return (T) pool;
+ }//if
+
+ throw new IllegalArgumentException("Could not unwrap this [" + this + "] as requested Java type [" + cls.getName() + "]");
+ }//unwrap
+}//PersistenceContextImpl
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/db/PreparedStatementWrapper.java b/jpalite-core/src/main/java/io/jpalite/impl/db/PreparedStatementWrapper.java
new file mode 100644
index 0000000..f9f7365
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/db/PreparedStatementWrapper.java
@@ -0,0 +1,522 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl.db;
+
+import io.jpalite.DatabasePool;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.net.URL;
+import java.sql.*;
+import java.util.Calendar;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+
+public class PreparedStatementWrapper extends StatementWrapper implements PreparedStatement
+{
+ private static final String NULL_STR = "(null)";
+ private static final String ASCII_STREAM = "(ascii stream)";
+ private static final String UNICODE_STREAM = "(unicode stream)";
+ private static final String BINARY_STREAM = "(binary stream)";
+ private static final String CHAR_STREAM = "(char stream)";
+ private static final String BLOB = "(blob)";
+ private static final String CLOB = "(clob)";
+ private static final String ARRAY = "(array)";
+ private static final String NCLOB = "(nclob)";
+ private static final String NCHAR_STREAM = "(nchar stream)";
+ private static final String ROWID = "(rowid)";
+
+ private final PreparedStatement realPreparedStatement;
+ protected String queryStr;
+ protected Map params = new TreeMap<>();
+
+ public PreparedStatementWrapper(DatabasePool pool, String connectName, PreparedStatement preparedStatement, String sql, ConnectionWrapper wrapper)
+ {
+ super(pool, connectName, preparedStatement, wrapper);
+ queryStr = sql;
+ connection.setLastQuery(sql);
+ realPreparedStatement = preparedStatement;
+ }
+
+ private String buildParamList()
+ {
+ StringBuilder paramsStr = new StringBuilder();
+ if (params.isEmpty()) {
+ paramsStr.append(",");
+ }//if
+ else {
+ for (Iterator iterator = params.keySet().iterator(); iterator.hasNext(); ) {
+ Integer key = iterator.next();
+ Object value = params.get(key);
+ if (value == null) {
+ value = NULL_STR;
+ }//if
+
+ paramsStr.append(",:").append(key).append("=").append(value);
+ }
+ }//else
+ return paramsStr.substring(1);
+ }//buildParamList
+
+ @Override
+ protected void logError(String pmethod, String queryStr, Throwable exception)
+ {
+ if (connection.isEnableLogging()) {
+ super.logError(pmethod, queryStr + " - (" + buildParamList() + ")", exception);
+ }//if
+ }//logError
+
+ @Override
+ protected void logExecution(String method, String queryStr, long executeTime, boolean update)
+ {
+ if (connection.isEnableLogging() || connection.getAuditWriter() != null) {
+ StringBuilder paramsStr = new StringBuilder();
+ if (params.isEmpty()) {
+ paramsStr.append(",");
+ }//if
+ else {
+ for (Iterator iterator = params.keySet().iterator(); iterator.hasNext(); ) {
+ Integer key = iterator.next();
+ Object value = params.get(key);
+ if (value == null) {
+ value = NULL_STR;
+ }//if
+
+ paramsStr.append(",:").append(key).append("=").append(value);
+ }
+ }//else
+
+ super.logExecution(method, queryStr + " - (" + buildParamList() + ")", executeTime, update);
+ }//if
+ }
+
+ @Override
+ public ResultSet executeQuery() throws SQLException
+ {
+ try {
+ long start = System.currentTimeMillis();
+ ResultSet resultSet = realPreparedStatement.executeQuery();
+ logExecution(EXECUTE_QUERY_METHOD, queryStr, System.currentTimeMillis() - start, false);
+ return resultSet;
+ }//try
+ catch (SQLException ex) {
+ logError(EXECUTE_QUERY_METHOD, queryStr, ex);
+ throw ex;
+ }//catch
+ }
+
+ @Override
+ public int executeUpdate() throws SQLException
+ {
+ try {
+ long start = System.currentTimeMillis();
+ int result = 0;
+ if (!connection.getPersistenceContext().unwrap(PersistenceContextImpl.class).isReadonly()) {
+ result = realPreparedStatement.executeUpdate();
+ }//if
+ logExecution(EXECUTE_UPDATE_METHOD, queryStr, System.currentTimeMillis() - start, true);
+ return result;
+ }//try
+ catch (SQLException ex) {
+ logError(EXECUTE_UPDATE_METHOD, queryStr, ex);
+ throw ex;
+ }//catch
+ }
+
+ @Override
+ public void setNull(int parameterIndex, int sqlType) throws SQLException
+ {
+ realPreparedStatement.setNull(parameterIndex, sqlType);
+ params.put(parameterIndex, NULL_STR);
+ }
+
+ @Override
+ public void setBoolean(int parameterIndex, boolean x) throws SQLException
+ {
+ realPreparedStatement.setBoolean(parameterIndex, x);
+ params.put(parameterIndex, x);
+ }
+
+ @Override
+ public void setByte(int parameterIndex, byte x) throws SQLException
+ {
+ realPreparedStatement.setByte(parameterIndex, x);
+ params.put(parameterIndex, x);
+ }
+
+ @Override
+ public void setShort(int parameterIndex, short x) throws SQLException
+ {
+ realPreparedStatement.setShort(parameterIndex, x);
+ params.put(parameterIndex, x);
+ }
+
+ @Override
+ public void setInt(int parameterIndex, int x) throws SQLException
+ {
+ realPreparedStatement.setInt(parameterIndex, x);
+ params.put(parameterIndex, x);
+ }
+
+ @Override
+ public void setLong(int parameterIndex, long x) throws SQLException
+ {
+ realPreparedStatement.setLong(parameterIndex, x);
+ params.put(parameterIndex, x);
+ }
+
+ @Override
+ public void setFloat(int parameterIndex, float x) throws SQLException
+ {
+ realPreparedStatement.setFloat(parameterIndex, x);
+ params.put(parameterIndex, x);
+ }
+
+ @Override
+ public void setDouble(int parameterIndex, double x) throws SQLException
+ {
+ realPreparedStatement.setDouble(parameterIndex, x);
+ params.put(parameterIndex, x);
+ }
+
+ @Override
+ public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException
+ {
+ realPreparedStatement.setBigDecimal(parameterIndex, x);
+ params.put(parameterIndex, x);
+ }
+
+ @Override
+ public void setString(int parameterIndex, String x) throws SQLException
+ {
+ realPreparedStatement.setString(parameterIndex, x);
+ params.put(parameterIndex, x);
+ }
+
+ @Override
+ public void setBytes(int parameterIndex, byte[] x) throws SQLException
+ {
+ realPreparedStatement.setBytes(parameterIndex, x);
+ params.put(parameterIndex, x);
+ }
+
+ @Override
+ public void setDate(int parameterIndex, Date x) throws SQLException
+ {
+ realPreparedStatement.setDate(parameterIndex, x);
+ params.put(parameterIndex, x);
+ }
+
+ @Override
+ public void setTime(int parameterIndex, Time x) throws SQLException
+ {
+ realPreparedStatement.setTime(parameterIndex, x);
+ params.put(parameterIndex, x);
+ }
+
+ @Override
+ public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException
+ {
+ realPreparedStatement.setTimestamp(parameterIndex, x);
+ params.put(parameterIndex, x);
+ }
+
+ @Override
+ public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException
+ {
+ realPreparedStatement.setAsciiStream(parameterIndex, x, length);
+ params.put(parameterIndex, ASCII_STREAM);
+ }
+
+ @Override
+ @SuppressWarnings("java:S1874")
+ public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException
+ {
+ realPreparedStatement.setUnicodeStream(parameterIndex, x, length);
+ params.put(parameterIndex, UNICODE_STREAM);
+ }
+
+ @Override
+ public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException
+ {
+ realPreparedStatement.setBinaryStream(parameterIndex, x, length);
+ params.put(parameterIndex, BINARY_STREAM);
+ }
+
+ @Override
+ public void clearParameters() throws SQLException
+ {
+ realPreparedStatement.clearParameters();
+ params.clear();
+ }
+
+ @Override
+ public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException
+ {
+ realPreparedStatement.setObject(parameterIndex, x, targetSqlType);
+ params.put(parameterIndex, x);
+ }
+
+ @Override
+ public void setObject(int parameterIndex, Object x) throws SQLException
+ {
+ realPreparedStatement.setObject(parameterIndex, x);
+ params.put(parameterIndex, x);
+ }
+
+ @Override
+ public boolean execute() throws SQLException
+ {
+ try {
+ long vStart = System.currentTimeMillis();
+ boolean vResult = false;
+ if (!connection.getPersistenceContext().unwrap(PersistenceContextImpl.class).isReadonly()) {
+ vResult = realPreparedStatement.execute();
+ }//if
+ logExecution(EXECUTE_METHOD, queryStr, System.currentTimeMillis() - vStart, true);
+ return vResult;
+ }//try
+ catch (SQLException ex) {
+ logError(EXECUTE_METHOD, queryStr, ex);
+ throw ex;
+ }//catch
+ }
+
+ @Override
+ public void addBatch() throws SQLException
+ {
+ realPreparedStatement.addBatch();
+ }
+
+ @Override
+ public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException
+ {
+ realPreparedStatement.setCharacterStream(parameterIndex, reader, length);
+ params.put(parameterIndex, CHAR_STREAM);
+ }
+
+ @Override
+ public void setRef(int parameterIndex, Ref x) throws SQLException
+ {
+ realPreparedStatement.setRef(parameterIndex, x);
+ params.put(parameterIndex, x);
+ }
+
+ @Override
+ public void setBlob(int parameterIndex, Blob x) throws SQLException
+ {
+ realPreparedStatement.setBlob(parameterIndex, x);
+ params.put(parameterIndex, BLOB);
+ }
+
+ @Override
+ public void setClob(int parameterIndex, Clob x) throws SQLException
+ {
+ realPreparedStatement.setClob(parameterIndex, x);
+ params.put(parameterIndex, CLOB);
+ }
+
+ @Override
+ public void setArray(int parameterIndex, Array x) throws SQLException
+ {
+ realPreparedStatement.setArray(parameterIndex, x);
+ params.put(parameterIndex, ARRAY);
+ }
+
+ @Override
+ public ResultSetMetaData getMetaData() throws SQLException
+ {
+ return realPreparedStatement.getMetaData();
+ }
+
+ @Override
+ public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException
+ {
+ realPreparedStatement.setDate(parameterIndex, x, cal);
+ params.put(parameterIndex, x);
+ }
+
+ @Override
+ public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException
+ {
+ realPreparedStatement.setTime(parameterIndex, x, cal);
+ params.put(parameterIndex, x);
+ }
+
+ @Override
+ public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException
+ {
+ realPreparedStatement.setTimestamp(parameterIndex, x, cal);
+ params.put(parameterIndex, x);
+ }
+
+ @Override
+ public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException
+ {
+ realPreparedStatement.setNull(parameterIndex, sqlType, typeName);
+ params.put(parameterIndex, NULL_STR);
+ }
+
+ @Override
+ public void setURL(int parameterIndex, URL x) throws SQLException
+ {
+ realPreparedStatement.setURL(parameterIndex, x);
+ params.put(parameterIndex, x);
+ }
+
+ @Override
+ public ParameterMetaData getParameterMetaData() throws SQLException
+ {
+ return realPreparedStatement.getParameterMetaData();
+ }
+
+ @Override
+ public void setRowId(int parameterIndex, RowId x) throws SQLException
+ {
+ realPreparedStatement.setRowId(parameterIndex, x);
+ params.put(parameterIndex, ROWID);
+ }
+
+ @Override
+ public void setNString(int parameterIndex, String value) throws SQLException
+ {
+ realPreparedStatement.setNString(parameterIndex, value);
+ params.put(parameterIndex, value);
+ }
+
+ @Override
+ public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException
+ {
+ realPreparedStatement.setNCharacterStream(parameterIndex, value, length);
+ params.put(parameterIndex, NCHAR_STREAM);
+ }
+
+ @Override
+ public void setNClob(int parameterIndex, NClob value) throws SQLException
+ {
+ realPreparedStatement.setNClob(parameterIndex, value);
+ params.put(parameterIndex, NCLOB);
+ }
+
+ @Override
+ public void setClob(int parameterIndex, Reader reader, long length) throws SQLException
+ {
+ realPreparedStatement.setClob(parameterIndex, reader, length);
+ params.put(parameterIndex, CLOB);
+ }
+
+ @Override
+ public void setBlob(int parameterIndex, InputStream inputStream, long length) throws SQLException
+ {
+ realPreparedStatement.setBlob(parameterIndex, inputStream, length);
+ params.put(parameterIndex, BLOB);
+ }
+
+ @Override
+ public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException
+ {
+ realPreparedStatement.setNClob(parameterIndex, reader, length);
+ params.put(parameterIndex, NCLOB);
+ }
+
+ @Override
+ public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException
+ {
+ realPreparedStatement.setSQLXML(parameterIndex, xmlObject);
+ params.put(parameterIndex, xmlObject.getString());
+ }
+
+ @Override
+ public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException
+ {
+ realPreparedStatement.setObject(parameterIndex, x, targetSqlType, scaleOrLength);
+ params.put(parameterIndex, x.toString());
+ }
+
+ @Override
+ public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException
+ {
+ realPreparedStatement.setAsciiStream(parameterIndex, x, length);
+ params.put(parameterIndex, ASCII_STREAM);
+ }
+
+ @Override
+ public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException
+ {
+ realPreparedStatement.setBinaryStream(parameterIndex, x, length);
+ params.put(parameterIndex, BINARY_STREAM);
+ }
+
+ @Override
+ public void setCharacterStream(int parameterIndex, Reader reader, long length) throws SQLException
+ {
+ realPreparedStatement.setCharacterStream(parameterIndex, reader, length);
+ params.put(parameterIndex, CHAR_STREAM);
+ }
+
+ @Override
+ public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException
+ {
+ realPreparedStatement.setAsciiStream(parameterIndex, x);
+ params.put(parameterIndex, ASCII_STREAM);
+ }
+
+ @Override
+ public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException
+ {
+ realPreparedStatement.setBinaryStream(parameterIndex, x);
+ params.put(parameterIndex, BINARY_STREAM);
+ }
+
+ @Override
+ public void setCharacterStream(int parameterIndex, Reader reader) throws SQLException
+ {
+ realPreparedStatement.setCharacterStream(parameterIndex, reader);
+ params.put(parameterIndex, CHAR_STREAM);
+ }
+
+ @Override
+ public void setNCharacterStream(int parameterIndex, Reader value) throws SQLException
+ {
+ realPreparedStatement.setNCharacterStream(parameterIndex, value);
+ params.put(parameterIndex, NCHAR_STREAM);
+ }
+
+ @Override
+ public void setClob(int parameterIndex, Reader reader) throws SQLException
+ {
+ realPreparedStatement.setClob(parameterIndex, reader);
+ params.put(parameterIndex, CLOB);
+ }
+
+ @Override
+ public void setBlob(int parameterIndex, InputStream inputStream) throws SQLException
+ {
+ realPreparedStatement.setBlob(parameterIndex, inputStream);
+ params.put(parameterIndex, BLOB);
+ }
+
+ @Override
+ public void setNClob(int parameterIndex, Reader reader) throws SQLException
+ {
+ realPreparedStatement.setNClob(parameterIndex, reader);
+ params.put(parameterIndex, NCLOB);
+ }
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/db/StatementWrapper.java b/jpalite-core/src/main/java/io/jpalite/impl/db/StatementWrapper.java
new file mode 100644
index 0000000..8c1384b
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/db/StatementWrapper.java
@@ -0,0 +1,541 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl.db;
+
+import io.jpalite.DatabasePool;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.*;
+
+@SuppressWarnings({"resource", "SqlSourceToSinkFlow"})
+public class StatementWrapper implements Statement
+{
+ protected static final String EXECUTE_UPDATE_METHOD = "executeUpdate";
+ protected static final String EXECUTE_QUERY_METHOD = "executeQuery";
+ protected static final String EXECUTE_METHOD = "execute";
+ private static final Logger LOG = LoggerFactory.getLogger(StatementWrapper.class);
+ protected ConnectionWrapper connection;
+ private final Statement realStatement;
+ private final DatabasePool databasePool;
+
+ public StatementWrapper(DatabasePool pool, String cursorName, Statement realStatement, ConnectionWrapper wrapper)
+ {
+ this.realStatement = realStatement;
+ connection = wrapper;
+ databasePool = pool;
+ try {
+ realStatement.setCursorName(cursorName);
+ }//try
+ catch (SQLException ex) {
+ LOG.warn("Failed to set the cursor name to {}", cursorName, ex);
+ }//catch
+ }
+
+ protected void logError(String method, String queryStr, Throwable exception)
+ {
+ if (connection.isEnableLogging()) {
+ LOG.info("{} > {}: Query execution failed {}. Query {}", databasePool.getPoolName(), method, exception.getMessage(), queryStr);
+ }//if
+ }//logError
+
+ protected void logExecution(String method, String queryStr, long executeTime, boolean update)
+ {
+ String vReadonlyStr = "";
+
+ if (connection.getPersistenceContext().unwrap(PersistenceContextImpl.class).isReadonly()) {
+ vReadonlyStr = " (Readonly) ";
+ }//if
+
+ if (update && connection.getAuditWriter() != null) {
+ connection.getAuditWriter().printf("%s > %s%s: %s%n", databasePool.getPoolName(), method, vReadonlyStr, queryStr);
+ }//if
+
+ if (executeTime > connection.getSlowQueryTimeout()) {
+ LOG.warn("{} > {}{}: Long running query detected [{} ms]: {}", databasePool.getPoolName(), method, vReadonlyStr, executeTime, queryStr);
+ }//if
+ else {
+ if (connection.isEnableLogging()) {
+ LOG.info("{} > {}{}: Query completed [{} ms]: {}", databasePool.getPoolName(), method, vReadonlyStr, executeTime, queryStr);
+ }//else
+ }//else
+ }//logExecution
+
+ @Override
+ public ResultSet executeQuery(String sql) throws SQLException
+ {
+ try {
+ long start = System.currentTimeMillis();
+ connection.setLastQuery(sql);
+ ResultSet result = realStatement.executeQuery(sql);
+ logExecution(EXECUTE_QUERY_METHOD, sql, System.currentTimeMillis() - start, false);
+ return result;
+ }//try
+ catch (SQLException ex) {
+ logError(EXECUTE_QUERY_METHOD, sql, ex);
+ throw ex;
+ }//catch
+ }
+
+ @Override
+ public int executeUpdate(String sql) throws SQLException
+ {
+ try {
+ long start = System.currentTimeMillis();
+ int result = 0;
+ connection.setLastQuery(sql);
+ if (!connection.getPersistenceContext().unwrap(PersistenceContextImpl.class).isReadonly()) {
+ result = realStatement.executeUpdate(sql);
+ }//if
+ logExecution(EXECUTE_UPDATE_METHOD, sql, System.currentTimeMillis() - start, true);
+ return result;
+ }//try
+ catch (SQLException ex) {
+ logError(EXECUTE_UPDATE_METHOD, sql, ex);
+ throw ex;
+ }//catch
+ }
+
+ @Override
+ public void close() throws SQLException
+ {
+ realStatement.close();
+ }
+
+ @Override
+ public int getMaxFieldSize() throws SQLException
+ {
+ return realStatement.getMaxFieldSize();
+ }
+
+ @Override
+ public void setMaxFieldSize(int max) throws SQLException
+ {
+ realStatement.setMaxFieldSize(max);
+ }
+
+ @Override
+ public int getMaxRows() throws SQLException
+ {
+ return realStatement.getMaxRows();
+ }
+
+ @Override
+ public void setMaxRows(int max) throws SQLException
+ {
+ realStatement.setMaxRows(max);
+ }
+
+ @Override
+ public void setEscapeProcessing(boolean enable) throws SQLException
+ {
+ realStatement.setEscapeProcessing(enable);
+ }
+
+ @Override
+ public int getQueryTimeout() throws SQLException
+ {
+ return realStatement.getQueryTimeout();
+ }
+
+ @Override
+ public void setQueryTimeout(int seconds) throws SQLException
+ {
+ realStatement.setQueryTimeout(seconds);
+ }
+
+ @Override
+ public void cancel() throws SQLException
+ {
+ realStatement.cancel();
+ }
+
+ @Override
+ public SQLWarning getWarnings() throws SQLException
+ {
+ return realStatement.getWarnings();
+ }
+
+ @Override
+ public void clearWarnings() throws SQLException
+ {
+ realStatement.clearWarnings();
+ }
+
+ @Override
+ public void setCursorName(String name) throws SQLException
+ {
+ realStatement.setCursorName(name);
+ }
+
+ @Override
+ public boolean execute(String sql) throws SQLException
+ {
+ try {
+ long start = System.currentTimeMillis();
+ boolean result = false;
+ if (!connection.getPersistenceContext().unwrap(PersistenceContextImpl.class).isReadonly()) {
+ connection.setLastQuery(sql);
+ result = realStatement.execute(sql);
+ }//if
+
+ logExecution(EXECUTE_METHOD, sql, System.currentTimeMillis() - start, true);
+ return result;
+ }//try
+ catch (SQLException ex) {
+ logError(EXECUTE_METHOD, sql, ex);
+ throw ex;
+ }//catch
+ }
+
+ @Override
+ public ResultSet getResultSet() throws SQLException
+ {
+ return realStatement.getResultSet();
+ }
+
+ @Override
+ public int getUpdateCount() throws SQLException
+ {
+ return realStatement.getUpdateCount();
+ }
+
+ @Override
+ public boolean getMoreResults() throws SQLException
+ {
+ return realStatement.getMoreResults();
+ }
+
+ @Override
+ public void setFetchDirection(int direction) throws SQLException
+ {
+ realStatement.setFetchDirection(direction);
+ }
+
+ @Override
+ public int getFetchDirection() throws SQLException
+ {
+ return realStatement.getFetchDirection();
+ }
+
+ @Override
+ public void setFetchSize(int rows) throws SQLException
+ {
+ realStatement.setFetchSize(rows);
+ }
+
+ @Override
+ public int getFetchSize() throws SQLException
+ {
+ return realStatement.getFetchSize();
+ }
+
+ @Override
+ public int getResultSetConcurrency() throws SQLException
+ {
+ return realStatement.getResultSetConcurrency();
+ }
+
+ @Override
+ public int getResultSetType() throws SQLException
+ {
+ return realStatement.getResultSetType();
+ }
+
+ @Override
+ public void addBatch(String sql) throws SQLException
+ {
+ realStatement.addBatch(sql);
+ }
+
+ @Override
+ public void clearBatch() throws SQLException
+ {
+ realStatement.clearBatch();
+ }
+
+ @Override
+ public int[] executeBatch() throws SQLException
+ {
+ return realStatement.executeBatch();
+ }
+
+ @Override
+ public Connection getConnection() throws SQLException
+ {
+ return realStatement.getConnection();
+ }
+
+ @Override
+ public boolean getMoreResults(int current) throws SQLException
+ {
+ return realStatement.getMoreResults(current);
+ }
+
+ @Override
+ public ResultSet getGeneratedKeys() throws SQLException
+ {
+ return realStatement.getGeneratedKeys();
+ }
+
+ @Override
+ public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException
+ {
+ try {
+ long start = System.currentTimeMillis();
+ int result = 0;
+
+ if (!connection.getPersistenceContext().unwrap(PersistenceContextImpl.class).isReadonly()) {
+ connection.setLastQuery(sql);
+ result = realStatement.executeUpdate(sql, autoGeneratedKeys);
+ }//if
+
+ logExecution(EXECUTE_UPDATE_METHOD, sql, System.currentTimeMillis() - start, true);
+ return result;
+ }//try
+ catch (SQLException ex) {
+ logError(EXECUTE_UPDATE_METHOD, sql, ex);
+ throw ex;
+ }//catch
+ }
+
+ @Override
+ public int executeUpdate(String sql, int[] columnIndexes) throws SQLException
+ {
+ try {
+ long start = System.currentTimeMillis();
+ int result = 0;
+
+ if (!connection.getPersistenceContext().unwrap(PersistenceContextImpl.class).isReadonly()) {
+ connection.setLastQuery(sql);
+ result = realStatement.executeUpdate(sql, columnIndexes);
+ }//if
+
+ logExecution(EXECUTE_UPDATE_METHOD, sql, System.currentTimeMillis() - start, true);
+ return result;
+ }//try
+ catch (SQLException ex) {
+ logError(EXECUTE_UPDATE_METHOD, sql, ex);
+ throw ex;
+ }//catch
+ }
+
+ @Override
+ public int executeUpdate(String sql, String[] columnNames) throws SQLException
+ {
+ try {
+ long start = System.currentTimeMillis();
+ int result = 0;
+
+ if (!connection.getPersistenceContext().unwrap(PersistenceContextImpl.class).isReadonly()) {
+ connection.setLastQuery(sql);
+ result = realStatement.executeUpdate(sql, columnNames);
+ }//if
+
+ logExecution(EXECUTE_UPDATE_METHOD, sql, System.currentTimeMillis() - start, true);
+ return result;
+ }//try
+ catch (SQLException ex) {
+ logError(EXECUTE_UPDATE_METHOD, sql, ex);
+ throw ex;
+ }//catch
+ }
+
+ @Override
+ public boolean execute(String sql, int autoGeneratedKeys) throws SQLException
+ {
+ try {
+ long start = System.currentTimeMillis();
+ boolean result = false;
+
+ if (!connection.getPersistenceContext().unwrap(PersistenceContextImpl.class).isReadonly()) {
+ connection.setLastQuery(sql);
+ result = realStatement.execute(sql, autoGeneratedKeys);
+ }//if
+
+ logExecution(EXECUTE_METHOD, sql, System.currentTimeMillis() - start, true);
+ return result;
+ }//try
+ catch (SQLException ex) {
+ logError(EXECUTE_METHOD, sql, ex);
+ throw ex;
+ }//catch
+ }
+
+ @Override
+ public boolean execute(String sql, int[] columnIndexes) throws SQLException
+ {
+ try {
+ long start = System.currentTimeMillis();
+ boolean result = false;
+
+ if (!connection.getPersistenceContext().unwrap(PersistenceContextImpl.class).isReadonly()) {
+ connection.setLastQuery(sql);
+ result = realStatement.execute(sql, columnIndexes);
+ }//if
+
+ logExecution(EXECUTE_METHOD, sql, System.currentTimeMillis() - start, true);
+ return result;
+ }//try
+ catch (SQLException ex) {
+ logError(EXECUTE_METHOD, sql, ex);
+ throw ex;
+ }//catch
+ }
+
+ @Override
+ public boolean execute(String sql, String[] columnNames) throws SQLException
+ {
+ try {
+ long start = System.currentTimeMillis();
+ boolean result = false;
+
+ if (!connection.getPersistenceContext().unwrap(PersistenceContextImpl.class).isReadonly()) {
+ connection.setLastQuery(sql);
+ result = realStatement.execute(sql, columnNames);
+ }//if
+
+ logExecution(EXECUTE_METHOD, sql, System.currentTimeMillis() - start, true);
+ return result;
+ }//try
+ catch (SQLException ex) {
+ logError(EXECUTE_METHOD, sql, ex);
+ throw ex;
+ }//catch
+ }
+
+ @Override
+ public int getResultSetHoldability() throws SQLException
+ {
+ return realStatement.getResultSetHoldability();
+ }
+
+ @Override
+ public boolean isClosed() throws SQLException
+ {
+ return realStatement.isClosed();
+ }
+
+ @Override
+ public void setPoolable(boolean poolable) throws SQLException
+ {
+ realStatement.setPoolable(poolable);
+ }
+
+ @Override
+ public boolean isPoolable() throws SQLException
+ {
+ return realStatement.isPoolable();
+ }
+
+ @Override
+ public void closeOnCompletion() throws SQLException
+ {
+ realStatement.closeOnCompletion();
+ }
+
+ @Override
+ public boolean isCloseOnCompletion() throws SQLException
+ {
+ return realStatement.isCloseOnCompletion();
+ }
+
+ @Override
+ public long getLargeUpdateCount() throws SQLException
+ {
+ return realStatement.getLargeUpdateCount();
+ }
+
+ @Override
+ public void setLargeMaxRows(long max) throws SQLException
+ {
+ realStatement.setLargeMaxRows(max);
+ }
+
+ @Override
+ public long getLargeMaxRows() throws SQLException
+ {
+ return realStatement.getLargeMaxRows();
+ }
+
+ @Override
+ public long[] executeLargeBatch() throws SQLException
+ {
+ return realStatement.executeLargeBatch();
+ }
+
+ @Override
+ public long executeLargeUpdate(String sql) throws SQLException
+ {
+ return realStatement.executeLargeUpdate(sql);
+ }
+
+ @Override
+ public long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLException
+ {
+ return realStatement.executeLargeUpdate(sql, autoGeneratedKeys);
+ }
+
+ @Override
+ public long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLException
+ {
+ return realStatement.executeLargeUpdate(sql, columnIndexes);
+ }
+
+ @Override
+ public long executeLargeUpdate(String sql, String[] columnNames) throws SQLException
+ {
+ return realStatement.executeLargeUpdate(sql, columnNames);
+ }
+
+ @Override
+ public String enquoteLiteral(String val) throws SQLException
+ {
+ return realStatement.enquoteLiteral(val);
+ }
+
+ @Override
+ public String enquoteIdentifier(String identifier, boolean alwaysQuote) throws SQLException
+ {
+ return realStatement.enquoteIdentifier(identifier, alwaysQuote);
+ }
+
+ @Override
+ public boolean isSimpleIdentifier(String identifier) throws SQLException
+ {
+ return realStatement.isSimpleIdentifier(identifier);
+ }
+
+ @Override
+ public String enquoteNCharLiteral(String val) throws SQLException
+ {
+ return realStatement.enquoteNCharLiteral(val);
+ }
+
+ @Override
+ public T unwrap(Class iface) throws SQLException
+ {
+ return realStatement.unwrap(iface);
+ }
+
+ @Override
+ public boolean isWrapperFor(Class> iface) throws SQLException
+ {
+ return realStatement.isWrapperFor(iface);
+ }
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/parsers/BooleanValue.java b/jpalite-core/src/main/java/io/jpalite/impl/parsers/BooleanValue.java
new file mode 100644
index 0000000..55cbd18
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/parsers/BooleanValue.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl.parsers;
+
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.ExpressionVisitor;
+import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
+
+public class BooleanValue extends ASTNodeAccessImpl implements Expression
+{
+ private final String stringValue;
+
+ public BooleanValue(String value)
+ {
+ stringValue = value.toLowerCase();
+ }
+
+ public BooleanValue(boolean value)
+ {
+ stringValue = String.valueOf(value);
+ }
+
+ @Override
+ public void accept(ExpressionVisitor expressionVisitor)
+ {
+ ((ExtraExpressionVisitor) expressionVisitor).visit(this);
+ }
+
+ public String getStringValue()
+ {
+ return stringValue;
+ }
+
+ @Override
+ public String toString()
+ {
+ return getStringValue();
+ }
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/parsers/ExtraExpressionVisitor.java b/jpalite-core/src/main/java/io/jpalite/impl/parsers/ExtraExpressionVisitor.java
new file mode 100644
index 0000000..91d8599
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/parsers/ExtraExpressionVisitor.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl.parsers;
+
+import net.sf.jsqlparser.expression.ExpressionVisitor;
+
+public interface ExtraExpressionVisitor extends ExpressionVisitor
+{
+ void visit(BooleanValue aThis);
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/parsers/JPQLParser.java b/jpalite-core/src/main/java/io/jpalite/impl/parsers/JPQLParser.java
new file mode 100644
index 0000000..fcf9157
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/parsers/JPQLParser.java
@@ -0,0 +1,1072 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl.parsers;
+
+import io.jpalite.*;
+import io.jpalite.impl.queries.QueryParameterImpl;
+import io.jpalite.parsers.QueryParser;
+import io.jpalite.parsers.QueryStatement;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.PersistenceException;
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.expression.*;
+import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
+import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
+import net.sf.jsqlparser.expression.operators.relational.NotEqualsTo;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.schema.Column;
+import net.sf.jsqlparser.schema.Table;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.delete.Delete;
+import net.sf.jsqlparser.statement.insert.Insert;
+import net.sf.jsqlparser.statement.select.*;
+import net.sf.jsqlparser.statement.update.Update;
+import net.sf.jsqlparser.statement.update.UpdateSet;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@SuppressWarnings("java:S1452") //generic wildcard is required
+public class JPQLParser extends JsqlVistorBase implements QueryParser
+{
+
+ private enum Phase
+ {
+ FROM,
+ JOIN,
+ SELECT,
+ WHERE,
+ GROUP_BY,
+ HAVING,
+ ORDERBY
+ }
+
+ /**
+ * The parsed query
+ */
+ private final String query;
+ private QueryStatement queryStatement = QueryStatement.OTHER;
+
+ /**
+ * Starting number to generate unique tables aliases
+ */
+ private int tableNr = 1;
+ /**
+ * List of return types
+ */
+ private final Map> returnTypes;
+ /**
+ * List of tables used
+ */
+ private final List entityInfoList;
+ /**
+ * We may use either positional or named parameters, but we cannot mix them within the same query.
+ */
+ private boolean usingNamedParameters;
+ /**
+ * Map of parameters used in the query
+ */
+ private final List> queryParameters;
+ /**
+ * Instance of the defined joins in the query
+ */
+ private List joins;
+ /**
+ * State variable used to indicate that in section we are processing
+ */
+ private Phase currentPhase = Phase.FROM;
+ /**
+ * The "from" table in the select statement
+ */
+ private Table fromTable = null;
+ /**
+ * If not null the fetchtype settings on the basic fields are ignored and this value is used
+ */
+ private FetchType overrideBasicFetchType = null;
+ /**
+ * If not null the fetchtype settings on the ALL fields are ignored and this value is used
+ */
+ private FetchType overrideAllFetchType = null;
+ private boolean selectUsingPrimaryKey = false;
+ private boolean usingSubSelect = false;
+ private String tableAlias = null;
+
+ public class EntityInfo
+ {
+ private final List aliases;
+ private final EntityMetaData> metadata;
+ private final String tableAlias;
+
+ public EntityInfo(String alias, EntityMetaData> metaData)
+ {
+ aliases = new ArrayList<>();
+ aliases.add(alias);
+ metadata = metaData;
+ tableAlias = "t" + tableNr;
+ tableNr++;
+ }
+
+ public EntityInfo(String alias, EntityMetaData> metaData, String tableAlias)
+ {
+ aliases = new ArrayList<>();
+ aliases.add(alias);
+ metadata = metaData;
+ this.tableAlias = tableAlias;
+ }
+
+ @Override
+ public String toString()
+ {
+ return aliases.getFirst() + "->" + metadata + ", " + metadata.getTable() + " " + tableAlias;
+ }
+
+ public String getColumnAlias()
+ {
+ return aliases.getFirst();
+ }//getColumnAlias
+
+ public void addColAlias(String alias)
+ {
+ aliases.add(alias);
+ }
+
+ public boolean containsEntityAlias(String alias)
+ {
+ return aliases.contains(alias);
+ }
+
+ public String getTableAlias()
+ {
+ return tableAlias;
+ }
+
+ public EntityMetaData> getMetadata()
+ {
+ return metadata;
+ }
+ }//EntityInfo
+
+ /**
+ * Constructor for the class. The method takes as input a JQPL Statement and convert it to a Native Statement. Note
+ * that the original pStatement is modified
+ *
+ * @param rawQuery The JQPL query
+ * @param queryHints The query hints
+ */
+ public JPQLParser(String rawQuery, Map queryHints)
+ {
+ returnTypes = new LinkedHashMap<>();
+ entityInfoList = new ArrayList<>();
+ usingNamedParameters = false;
+ queryParameters = new ArrayList<>();
+
+ if (queryHints.get(JPALiteEntityManager.TRADESWITCH_OVERRIDE_FETCHTYPE) != null) {
+ overrideAllFetchType = (FetchType) queryHints.get(JPALiteEntityManager.TRADESWITCH_OVERRIDE_FETCHTYPE);
+ }//if
+
+ if (queryHints.get(JPALiteEntityManager.TRADESWITCH_OVERRIDE_BASIC_FETCHTYPE) != null) {
+ overrideBasicFetchType = (FetchType) queryHints.get(JPALiteEntityManager.TRADESWITCH_OVERRIDE_BASIC_FETCHTYPE);
+ }//if
+
+ try {
+ Statement vStatement = CCJSqlParserUtil.parse(rawQuery);
+ vStatement.accept(this);
+ query = vStatement.toString().replace(":?", "?");
+ }//try
+ catch (JSQLParserException ex) {
+ throw new PersistenceException("Error parsing query", ex);
+ }//catch
+ }//JpqlToNative
+
+ @Override
+ public boolean isSelectUsingPrimaryKey()
+ {
+ return selectUsingPrimaryKey;
+ }//isSelectUsingPrimaryKey
+
+ EntityInfo findEntityInfoWithTableAlias(String tableAlias)
+ {
+ for (EntityInfo vInfo : entityInfoList) {
+ if (vInfo.getTableAlias().equals(tableAlias)) {
+ return vInfo;
+ }//if
+ }//for
+ return null;
+ }//findEntityInfoWithTableAlias
+
+ EntityInfo findEntityInfoWithColAlias(String colAlias)
+ {
+ for (EntityInfo vInfo : entityInfoList) {
+ if (vInfo.containsEntityAlias(colAlias)) {
+ return vInfo;
+ }//if
+ }//for
+ return null;
+ }//findEntityInfoWithColAlias
+
+ EntityInfo findEntityInfoWithEntity(Class> entityClass)
+ {
+ for (EntityInfo info : entityInfoList) {
+ if (info.getMetadata().getEntityClass().equals(entityClass)) {
+ return info;
+ }//if
+ }//for
+ return null;
+ }//findEntityInfoWithEntity
+
+ @Override
+ public QueryStatement getStatement()
+ {
+ return queryStatement;
+ }
+
+ /**
+ * Return the Native query
+ *
+ * @return the SQL query
+ */
+ @Override
+ public String getQuery()
+ {
+ return query;
+ }//getNativeStatement
+
+ /**
+ * Return the type of parameter that is used.
+ *
+ * @return True if using named parameters
+ */
+ @Override
+ public boolean isUsingNamedParameters()
+ {
+ return usingNamedParameters;
+ }//isUsingNamedParameters
+
+ @Override
+ public int getNumberOfParameters()
+ {
+ return queryParameters.size();
+ }//getNumberOfParameters
+
+ /**
+ * Return a map of the query parameters defined.
+ *
+ * @return The query parameters
+ */
+ @Override
+ public List> getQueryParameters()
+ {
+ return queryParameters;
+ }
+
+ /**
+ * Return a list of all the return type in the select
+ *
+ * @return the list
+ */
+ @Override
+ public List> getReturnTypes()
+ {
+ return new ArrayList<>(returnTypes.values());
+ }//getReturnTypes
+
+ /**
+ * Check if the given return type is provided by the JQPL guery. If not an IllegalArgumentException exception is
+ * generated
+ *
+ * @param typeToCheck The class to check
+ * @throws IllegalArgumentException if the type is not provided
+ */
+ @Override
+ public void checkType(Class> typeToCheck)
+ {
+ if (queryStatement == QueryStatement.SELECT) {
+ if (!typeToCheck.isArray()) {
+ if (returnTypes.size() > 1) {
+ throw new IllegalArgumentException("Type specified for Query [" + typeToCheck.getName() + "] does not support multiple result set.");
+ }//if
+ if (!returnTypes.get("c1").isAssignableFrom(typeToCheck)) {
+ throw new IllegalArgumentException("Type specified for Query [" + typeToCheck.getName() + "] is incompatible with query return type [" + returnTypes.get("c1").getName() + "]");
+ }//if
+ }//if
+ else {
+ if (typeToCheck != byte[].class && typeToCheck != Object[].class) {
+ throw new IllegalArgumentException("Cannot create TypedQuery for query with more than one return using requested result type " + typeToCheck.arrayType().getName() + "[]");
+ }//if
+ }//else
+ }//if
+ }//checkType
+
+ private void joinAccept(Join join)
+ {
+ if (!join.getOnExpressions().isEmpty()) {
+ throw new IllegalArgumentException("JOIN ON is not supported in JQPL - " + join);
+ }//if
+
+ //JOIN . eg e.department d
+ Table joinTable = (Table) join.getRightItem();
+ String joinField = joinTable.getName(); //=department
+ String fromEntity = joinTable.getSchemaName(); //=e
+
+ String joinAlias;
+ if (joinTable.getAlias() != null) {
+ joinAlias = joinTable.getAlias().getName(); // =d
+ }//if
+ else {
+ //No Alias was set. Make it the same as the schema.table value
+ joinAlias = joinTable.getFullyQualifiedName(); //.=e.department
+ joinTable.setAlias(new Alias(joinAlias, false));
+ }//else
+
+ joinTable.accept(this);
+ EntityInfo joinEntityInfo = findEntityInfoWithColAlias(joinAlias); // =d or .=e.department
+
+ /*
+ * If the schema name is not present we are busy with a new Cartesian style join
+ * eg select d from Employee e, Department d where ...
+ * This case we just process the table
+ */
+ if (fromEntity != null) {
+ EntityInfo fromEntityInfo = findEntityInfoWithColAlias(fromEntity);
+ EntityField fromEntityField = fromEntityInfo.getMetadata().getEntityField(joinField); //=department
+ EntityField joinEntityField;
+ if (fromEntityField.getMappedBy() != null) {
+ joinEntityField = joinEntityInfo.getMetadata().getEntityField(fromEntityField.getMappedBy());
+ if (fromEntityInfo.getMetadata().hasMultipleIdFields()) {
+ throw new IllegalArgumentException("Cannot JOIN on multiple id fields");
+ }//if
+ fromEntityField = fromEntityInfo.getMetadata().getIdField();
+ }//if
+ else {
+ if (joinEntityInfo.getMetadata().hasMultipleIdFields()) {
+ throw new IllegalArgumentException("Cannot JOIN on multiple id fields");
+ }//if
+ joinEntityField = joinEntityInfo.getMetadata().getIdField();
+ }//else
+
+ BinaryExpression expression = new EqualsTo();
+ expression.setLeftExpression(new Column(new Table(fromEntityInfo.getTableAlias()), fromEntityField.getColumn()));
+ expression.setRightExpression(new Column(new Table(joinEntityInfo.getTableAlias()), joinEntityField.getColumn()));
+ join.getOnExpressions().add(expression);
+
+ if (fromEntityField.getMappingType() == MappingType.MANY_TO_ONE || fromEntityField.getMappingType() == MappingType.ONE_TO_ONE) {
+ join.withInner(!fromEntityField.isNullable())
+ .withLeft(fromEntityField.isNullable())
+ .withRight(false)
+ .withOuter(false)
+ .withCross(false)
+ .withFull(false)
+ .withStraight(false)
+ .withNatural(false);
+ }//if
+ }//if
+ }//joinAccept
+
+ private void addJoin(Table table)
+ {
+ Join join = new Join();
+ join.setInner(true);
+ join.setRightItem(table);
+
+ joins.add(join);
+ joinAccept(join);
+ }//addJoin
+
+ private EntityInfo findMappedBy(String fieldName)
+ {
+ for (EntityInfo info : entityInfoList) {
+ for (EntityField vField : info.getMetadata().getEntityFields()) {
+ if (fieldName.equals(vField.getMappedBy())) {
+ //Yes, we have winner :-)
+ return info;
+ }//if
+ }//for
+ }//for
+ return null;
+ }//findMappedBy
+
+ private void expandEntity(boolean root, EntityMetaData> entity, String selectNr, String colAlias, EntityField entityField, String tableAlias, List newList)
+ {
+ String newTableAlias = tableAlias + "." + entityField.getName();
+ if (!root) {
+ colAlias += "-" + entityField.getFieldNr();
+ }//if
+
+ //only XXXX_TO_ONE type mappings can be expanded
+ if (entityField.getMappingType() == MappingType.ONE_TO_ONE || entityField.getMappingType() == MappingType.MANY_TO_ONE) {
+ //Check if we already have a JOIN for the entity
+ EntityInfo entityInfo = findEntityInfoWithEntity(entityField.getType());
+ //We will expand if FetchType is EAGER or if we have an existing JOIN on the Entity
+ if (entityInfo != null || (overrideAllFetchType != null && overrideAllFetchType == FetchType.EAGER) || (overrideAllFetchType == null && entityField.getFetchType() == FetchType.EAGER)) {
+ if (entityInfo == null) {
+ //if where have many to one mapping on the field, check if one of the other tables ( FROM and JOIN) have an ONE_TO_MANY link
+ //back to this entity
+ if (entityField.getMappingType() == MappingType.MANY_TO_ONE) {
+ EntityInfo info = findMappedBy(entityField.getName());
+ if (info != null) {
+ getAllColumns(selectNr, colAlias, info.getMetadata(), info.getColumnAlias(), newList);
+ return;
+ }//if
+ }//if
+
+ Table table = new Table(tableAlias, entityField.getName());
+ table.setAlias(new Alias(tableAlias + "." + entityField.getName(), false));
+ addJoin(table);
+ entityInfo = findEntityInfoWithEntity(entityField.getType());
+ }//if
+ else {
+ if (!entityInfo.containsEntityAlias(newTableAlias)) {
+ entityInfo.addColAlias(newTableAlias);
+ }//if
+ }//else
+
+ getAllColumns(selectNr, colAlias, entityInfo.getMetadata(), newTableAlias, newList);
+ }//if
+ else {
+ newList.add(createSelectColumn(entityField.getName(), selectNr + colAlias, tableAlias));
+ }//else
+ }//if
+ else if (entityField.getMappingType() == MappingType.EMBEDDED) {
+ EntityInfo entityInfo = findEntityInfoWithTableAlias(newTableAlias);
+ if (entityInfo == null) {
+ EntityInfo parentEntityInfo = findEntityInfoWithEntity(entity.getEntityClass());
+ EntityMetaData> metaData = EntityMetaDataManager.getMetaData(entityField.getType());
+ entityInfo = new EntityInfo(tableAlias + "." + entityField.getName(), metaData, parentEntityInfo.getTableAlias());
+ entityInfoList.add(entityInfo);
+ }//if
+
+ getAllColumns(selectNr, colAlias, entityInfo.getMetadata(), newTableAlias, newList);
+ }//else
+ }//expandEntity
+
+ private SelectItem createSelectColumn(String field, String colField, String tableAlias)
+ {
+ Column newColumn = createColumn(field, tableAlias);
+ SelectExpressionItem newItem = new SelectExpressionItem(newColumn);
+ if (colField != null) {
+ newItem.setAlias(new Alias("\"" + colField + "\"", false));
+ }//if
+ return newItem;
+ }//createSelectColumn
+
+ private Column createColumn(String field, String tableAlias)
+ {
+ Table table = new Table();
+ String[] parts = tableAlias.split("\\.");
+ if (parts.length > 1) {
+ table.setSchemaName(parts[0]);
+ table.setName(tableAlias.substring(parts[1].length() + 2));
+ }//if
+ else {
+ table.setName(parts[0]);
+ }//else
+
+ table.setAlias(new Alias(tableAlias, false));
+ return new Column(table, field);
+ }//createColumn
+
+ private void getAllColumns(String selectNr, String colAlias, EntityMetaData> entity, String tableAlias, List newList)
+ {
+ for (EntityField field : entity.getEntityFields()) {
+ if (field.getMappingType() == MappingType.BASIC) {
+ if (field.isIdField() || (overrideBasicFetchType != null && overrideBasicFetchType == FetchType.EAGER) || (overrideBasicFetchType == null && field.getFetchType() == FetchType.EAGER)) {
+ newList.add(createSelectColumn(field.getName(), selectNr + colAlias + "-" + field.getFieldNr(), tableAlias));
+ }//if
+ }//if
+ else {
+ expandEntity(false, entity, selectNr, colAlias, field, tableAlias, newList);
+ }//else
+ }//for
+ }//getAllColumns
+
+ private EntityInfo findEntity(String selectPath)
+ {
+ EntityInfo entityInfo = findEntityInfoWithColAlias(selectPath);
+ if (entityInfo != null) {
+ return entityInfo;
+ }//if
+
+ int vDot = selectPath.lastIndexOf(".");
+ if (vDot == -1) {
+ throw new IllegalStateException("Invalid fields specified");
+ }//if
+
+ String path = selectPath.substring(0, vDot);
+ String field = selectPath.substring(vDot + 1);
+ entityInfo = findEntityInfoWithColAlias(path);
+ if (entityInfo == null) {
+ entityInfo = findEntity(path);
+ }//if
+
+ EntityField entityField = entityInfo.getMetadata().getEntityField(field);
+ if (entityField.getMappingType() == MappingType.EMBEDDED) {
+ EntityMetaData> metaData = EntityMetaDataManager.getMetaData(entityField.getType());
+ entityInfo = new EntityInfo(path + "." + entityField.getName(), metaData, entityInfo.getTableAlias());
+ entityInfoList.add(entityInfo);
+ }//if
+ else {
+ Table table = new Table(path, field);
+ table.setAlias(new Alias(path + "." + entityField.getName(), false));
+ addJoin(table);
+ }//else
+
+ return findEntityInfoWithColAlias(selectPath);
+ }//findEntity
+
+ private void processSelectItem(String colLabel, SelectItem item, List newList)
+ {
+ /*
+ * case 1. select e from Employee e
+ * Only one select item, selecting specifically the entity
+ *
+ * case 2. select e.name, e.department from Employee e
+ * Only one or more items from the entity
+ * The fields can either be entity, embedded class or basic field.
+ *
+ * processSelectItem() will be called for each item found
+ */
+ SelectExpressionItem selectItem = (SelectExpressionItem) item;
+ selectItem.setAlias(new Alias(colLabel, false));
+ if (selectItem.getExpression() instanceof Column column) {
+ if ("NEW".equalsIgnoreCase(column.getColumnName())) {
+ throw new IllegalArgumentException("JPQL Constructor Expressions are not supported - " + column);
+ }//if
+
+ //Check if we are working with a full entity or a field in an entity
+ if (column.getTable() == null) {
+ /*
+ * We will get here for any field being specified. eg select e.name | select e.department | select e.department.name
+ */
+
+ EntityInfo entityInfo = findEntityInfoWithColAlias(column.getColumnName());
+ if (entityInfo == null) {
+ throw new IllegalArgumentException("Unknown column - " + column);
+ }//if
+
+
+ addResultType(colLabel, entityInfo.getMetadata().getEntityClass());
+ getAllColumns(colLabel, "", entityInfo.getMetadata(), column.getColumnName(), newList);
+ }//if
+ else {
+ /*
+ * We will get here for selectItem where a field was specified. Eg select e.department from Employee e
+ */
+ String fieldName = column.getColumnName();
+ String fullPath = column.getTable().getFullyQualifiedName();
+
+ //Find the Entity from the path
+ EntityInfo entityInfo = findEntity(fullPath);
+ EntityField entityField = entityInfo.getMetadata().getEntityField(fieldName);
+ addResultType(colLabel, entityField.getType());
+
+ if (entityField.getMappingType() == MappingType.BASIC) {
+ newList.add(createSelectColumn(entityField.getName(), colLabel, fullPath));
+ }//if
+ else {
+ expandEntity(true, entityInfo.getMetadata(), colLabel, "", entityField, fullPath, newList);
+ }//else
+ }//else
+ }//if
+ else {
+ selectItem.setAlias(new Alias("\"" + colLabel + "\"", false));
+ newList.add(item);
+ }//else
+ }//processSelectItem
+
+ private void processSelectItems(List selectItems, List newList)
+ {
+ for (int nr = 0; nr < selectItems.size(); nr++) {
+ String colLabel = "c" + (nr + 1);
+ SelectItem item = selectItems.get(nr);
+ if (item instanceof SelectExpressionItem) {
+ processSelectItem(colLabel, item, newList);
+ }//if
+ else {
+ newList.add(item);
+ }//else
+ }//for
+ }//processSelectItems
+
+ private void processUpdateSet(List updateSets)
+ {
+ for (UpdateSet item : updateSets) {
+ ArrayList newColList = new ArrayList<>();
+ for (Column column : item.getColumns()) {
+ if (column.getTable() == null) {
+ column.setTable(new Table("X"));
+ }//if
+ String fieldName = column.getColumnName();
+ String fullPath = column.getTable().getFullyQualifiedName();
+ EntityInfo entityInfo = findEntity(fullPath);
+ EntityField entityField = entityInfo.getMetadata().getEntityField(fieldName);
+ if (entityField.getMappingType() == MappingType.EMBEDDED) {
+ throw new PersistenceException("Embedded field are not supported in update sets");
+ }//if
+ else {
+ Column newCol = createColumn(fieldName, fullPath);
+ newColList.add(newCol);
+ }//if
+ }//for
+ item.setColumns(newColList);
+ }//for
+ }//processUpdateSet
+
+ @Override
+ public void visit(Update update)
+ {
+ queryStatement = QueryStatement.UPDATE;
+ if (update.getTable().getAlias() == null) {
+ update.getTable().setAlias(new Alias("X", false));
+ fromTable = new Table(update.getTable().getName());
+ fromTable.setAlias(new Alias("X", false));
+ }//if
+ update.getTable().accept(this);
+ currentPhase = Phase.SELECT;
+ processUpdateSet(update.getUpdateSets());
+ for (UpdateSet updateSet : update.getUpdateSets()) {
+ for (Column column : updateSet.getColumns()) {
+ column.accept(this);
+ }//for
+ for (Expression expression : updateSet.getExpressions()) {
+ expression.accept(this);
+ }//for
+ }//for
+
+ currentPhase = Phase.WHERE;
+ if (update.getWhere() != null) {
+ update.getWhere().accept(this);
+ }//if
+ }//visit
+
+ @Override
+ public void visit(Delete delete)
+ {
+ queryStatement = QueryStatement.DELETE;
+ if (delete.getTable().getAlias() == null) {
+ delete.getTable().setAlias(new Alias(delete.getTable().getName(), false));
+ fromTable = new Table(delete.getTable().getName());
+ fromTable.setAlias(new Alias(delete.getTable().getName(), false));
+ }//if
+ delete.getTable().accept(this);
+
+ currentPhase = Phase.WHERE;
+ if (delete.getWhere() != null) {
+ delete.getWhere().accept(this);
+ }//if
+ }
+
+ @Override
+ public void visit(Insert insert)
+ {
+ queryStatement = QueryStatement.INSERT;
+ throw new PersistenceException("INSERT queries are not valid in JPQL");
+ }
+
+ private void addResultType(String column, Class> classType)
+ {
+ if (!usingSubSelect) {
+ returnTypes.put(column, classType);
+ }//if
+ }
+
+ @Override
+ public void visit(SubSelect subSelect)
+ {
+ usingSubSelect = true;
+
+ if (subSelect.getSelectBody() != null) {
+ subSelect.getSelectBody().accept(this);
+ }//if
+
+ if (subSelect.getWithItemsList() != null) {
+ for (WithItem item : subSelect.getWithItemsList()) {
+ item.accept(this);
+ }//for
+ }//if
+
+ usingSubSelect = false;
+ }
+
+ @Override
+ public void visit(PlainSelect plainSelect)
+ {
+ queryStatement = QueryStatement.SELECT;
+ currentPhase = Phase.FROM;
+ if (plainSelect.getFromItem() instanceof Table table) {
+ if (table.getAlias() == null) {
+ tableAlias = table.getName();
+ table.setAlias(new Alias(table.getName(), false));
+ }//if
+ fromTable = new Table(table.getName());
+ fromTable.setAlias(new Alias(table.getAlias().getName(), false));
+
+ plainSelect.getFromItem().accept(this);
+ }
+
+ currentPhase = Phase.JOIN;
+ if (plainSelect.getJoins() == null) {
+ joins = new ArrayList<>();
+ plainSelect.setJoins(joins);
+ }//if
+ else {
+ joins = plainSelect.getJoins();
+ }//else
+
+ for (Join join : joins) {
+ joinAccept(join);
+ }//for
+
+ currentPhase = Phase.SELECT;
+ if (plainSelect.getSelectItems() != null) {
+
+ List selectItemList = plainSelect.getSelectItems();
+ List newList = new ArrayList<>();
+ plainSelect.setSelectItems(newList);
+ processSelectItems(selectItemList, newList);
+ for (SelectItem item : plainSelect.getSelectItems()) {
+ item.accept(this);
+ }//for
+ }
+
+ currentPhase = Phase.WHERE;
+ selectUsingPrimaryKey = false; //Catch the case where there are no WHERE clause
+ if (plainSelect.getWhere() != null) {
+ //Set to true, if a tableColumn referencing a non-ID field is found it will be changed to false
+ selectUsingPrimaryKey = true;
+ plainSelect.getWhere().accept(this);
+ }
+
+ currentPhase = Phase.HAVING;
+ if (plainSelect.getHaving() != null) {
+ plainSelect.getHaving().accept(this);
+ }
+
+ currentPhase = Phase.GROUP_BY;
+ if (plainSelect.getGroupBy() != null) {
+ plainSelect.getGroupBy().accept(this);
+ }//if
+
+ currentPhase = Phase.ORDERBY;
+ if (plainSelect.getOrderByElements() != null) {
+ for (OrderByElement vElement : plainSelect.getOrderByElements()) {
+ vElement.accept(this);
+ }
+ }//if
+ }//visitPlainSelect
+
+ private void addQueryParameter(Expression expression, Class parameterType)
+ {
+ if (expression instanceof JdbcParameter parameter) {
+ if (queryParameters.isEmpty()) {
+ usingNamedParameters = false;
+ }//if
+ else if (usingNamedParameters) {
+ throw new IllegalArgumentException("Mixing positional and named parameters are not allowed");
+ }//else if
+
+ queryParameters.add(new QueryParameterImpl<>(parameter.getIndex(), parameterType));
+ }//if
+ else {
+ if (queryParameters.isEmpty()) {
+ usingNamedParameters = true;
+ }//if
+ else if (!usingNamedParameters) {
+ throw new IllegalArgumentException("Mixing positional and named parameters are not allowed");
+ }//else if
+
+ JdbcNamedParameter newParameter = (JdbcNamedParameter) expression;
+
+ queryParameters.add(new QueryParameterImpl<>(newParameter.getName(), queryParameters.size() + 1, parameterType));
+ }//else
+ }//addQueryParameter
+
+ private boolean processWhereColumn(BinaryExpression expression, Expression parameter, Column tableColumn)
+ {
+ EntityInfo entityInfo = findEntityInfoWithColAlias(tableColumn.getFullyQualifiedName());
+
+ if (entityInfo == null && tableColumn.getTable() == null) {
+ tableColumn.setTable(fromTable);
+ entityInfo = findEntityInfoWithColAlias(tableColumn.getFullyQualifiedName());
+ }//if
+
+ if (entityInfo == null) {
+ entityInfo = findEntityInfoWithColAlias(tableColumn.getTable().getName());
+ if (entityInfo != null) {
+ EntityField field = entityInfo.getMetadata().getEntityField(tableColumn.getColumnName());
+ tableColumn.setColumnName(field.getName());
+ entityInfo = findEntityInfoWithColAlias(tableColumn.getFullyQualifiedName());
+ }//if
+ else {
+ entityInfo = findEntityInfoWithColAlias(fromTable.getAlias().getName());
+ if (entityInfo != null) {
+ if (tableColumn.getTable().getAlias() == null && !tableColumn.getFullyQualifiedName().startsWith(entityInfo.getColumnAlias())) {
+ String schema = entityInfo.getColumnAlias();
+ if (tableColumn.getTable().getSchemaName() != null) {
+ schema += "." + tableColumn.getTable().getSchemaName();
+ }//if
+ tableColumn.getTable().setSchemaName(schema);
+ }//if
+ String path = tableColumn.getFullyQualifiedName();
+ int dot = path.lastIndexOf('.');
+ String field = path.substring(dot + 1);
+ path = path.substring(0, dot);
+
+ EntityInfo foundInfo = entityInfo;
+ entityInfo = findJoins(path, entityInfo);
+ EntityField entityField = entityInfo.getMetadata().getEntityField(field);
+ if (entityField.getFieldType() == FieldType.TYPE_ENTITY) {
+ entityInfo = findJoins(tableColumn.getFullyQualifiedName(), foundInfo);
+ tableColumn.setColumnName(entityInfo.getMetadata().getIdField().getName());
+ }
+ tableColumn.getTable().setAlias(new Alias(entityInfo.getColumnAlias(), false));
+ }//if
+ }//else
+ }//if
+
+ if (entityInfo != null && (entityInfo.getMetadata().getEntityType() == EntityType.ENTITY_EMBEDDABLE || entityInfo.getMetadata().getEntityType() == EntityType.ENTITY_IDCLASS)) {
+ addQueryParameter(parameter, entityInfo.getMetadata().getEntityClass());
+
+ List colList = new ArrayList<>();
+ List paramList = new ArrayList<>();
+ for (EntityField entityField : entityInfo.getMetadata().getEntityFields()) {
+ Table table = new Table();
+ table.setName(tableColumn.getTable().getFullyQualifiedName() + "." + tableColumn.getColumnName());
+ colList.add(new Column(table, entityField.getName()));
+ paramList.add(new JdbcParameter());
+ }//for
+ ValueListExpression leftList = new ValueListExpression();
+ leftList.setExpressionList(new ExpressionList(colList));
+ expression.setLeftExpression(leftList);
+
+ ValueListExpression rightList = new ValueListExpression();
+ rightList.setExpressionList(new ExpressionList(paramList));
+ expression.setRightExpression(rightList);
+
+ //Only visit the left tableColumn expression, we have already processed the parameters
+ expression.getLeftExpression().accept(this);
+
+ return false;
+ }//if
+
+ return true;
+ }
+
+ @SuppressWarnings("java:S6201") //instanceof check variable cannot be used here
+ private void visitEntity(BinaryExpression expression)
+ {
+ Column tableColumn = null;
+ Expression parameter = null;
+
+ if (expression.getLeftExpression() instanceof Column && (expression.getRightExpression() instanceof JdbcParameter || expression.getRightExpression() instanceof JdbcNamedParameter)) {
+ tableColumn = (Column) expression.getLeftExpression();
+ parameter = expression.getRightExpression();
+ }//if
+ else if (expression.getRightExpression() instanceof Column && (expression.getLeftExpression() instanceof JdbcParameter || expression.getLeftExpression() instanceof JdbcNamedParameter)) {
+ tableColumn = (Column) expression.getRightExpression();
+ parameter = expression.getLeftExpression();
+ }//else
+
+ if (tableColumn != null && !processWhereColumn(expression, parameter, tableColumn)) {
+ return;
+ }//if
+
+ expression.getLeftExpression().accept(this);
+ if (expression.getRightExpression() instanceof Column vCol) {
+ String s = vCol.getColumnName().toLowerCase();
+ if (s.equals("true") || s.equals("false")) {
+ expression.setRightExpression(new BooleanValue(vCol.getColumnName()));
+ }//if
+ }//if
+ expression.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(EqualsTo pExpression)
+ {
+ visitEntity(pExpression);
+ }
+
+ @Override
+ public void visit(NotEqualsTo pExpression)
+ {
+ visitEntity(pExpression);
+ }
+
+ @Override
+ public void visit(Table tableName)
+ {
+ if (tableName.getAlias() == null) {
+ throw new IllegalArgumentException("Missing alias for " + tableName.getName());
+ }//if
+
+ EntityInfo entityInfo;
+ EntityMetaData> metaData;
+ if (tableName.getSchemaName() != null) {
+ entityInfo = findEntityInfoWithColAlias(tableName.getSchemaName());
+ if (entityInfo == null) {
+ throw new IllegalArgumentException("Invalid schema - " + tableName);
+ }//if
+
+ EntityField field = entityInfo.getMetadata().getEntityField(tableName.getName());
+ metaData = EntityMetaDataManager.getMetaData(field.getType());
+ }//if
+ else {
+ metaData = EntityMetaDataManager.getMetaData(tableName.getName());
+ tableName.setName(tableName.getAlias().getName());
+ }//else
+
+ entityInfo = new EntityInfo(tableName.getAlias().getName(), metaData);
+ entityInfoList.add(entityInfo);
+
+ tableName.setAlias(new Alias(entityInfo.getTableAlias(), false));
+ tableName.setName(metaData.getTable());
+ tableName.setSchemaName(null);
+ }//visitTable
+
+ @SuppressWarnings("java:S1643") //StringBuilder cannot be used here
+ private EntityInfo findJoins(String path, EntityInfo entityInfo)
+ {
+ String[] pathElements = path.split("\\.");
+ String pathElement = pathElements[0];
+ for (int i = 1; i < pathElements.length; i++) {
+ EntityField field = entityInfo.getMetadata().getEntityField(pathElements[i]);
+ if (field.getFieldType() != FieldType.TYPE_ENTITY) {
+ break;
+ }//if
+
+ pathElement = pathElement + "." + field.getName();
+ entityInfo = findEntity(pathElement);
+ }//for
+
+ return entityInfo;
+ }//findJoins
+
+ @Override
+ public void visit(Column tableColumn)
+ {
+ /*
+ * A Column can either be point to an entity in which case we need to use the primary key field or to the actual field
+ */
+ EntityInfo entityInfo = findEntityInfoWithColAlias(tableColumn.getFullyQualifiedName());
+ if (entityInfo == null) {
+ if (tableColumn.getTable() == null) {
+ tableColumn.setTable(fromTable);
+ }//if
+ String colPath = tableColumn.getName(true);
+ int dot = colPath.lastIndexOf('.');
+ if (dot == -1) {
+ throw new IllegalArgumentException("Missing alias on column '" + tableColumn + "'");
+ }//if
+ colPath = colPath.substring(0, dot);
+
+ entityInfo = findEntityInfoWithColAlias(colPath);
+ if (entityInfo == null) {
+ if (this.tableAlias != null) {
+ String[] fullName = tableColumn.getFullyQualifiedName().split("\\.");
+ List parts = new ArrayList<>();
+ parts.add(tableAlias);
+ parts.addAll(List.of(fullName));
+ tableColumn.setTable(new Table(parts.subList(0, parts.size() - 1)));
+ tableColumn.setColumnName(parts.getLast());
+ colPath = tableColumn.getName(true);
+ dot = colPath.lastIndexOf('.');
+ colPath = colPath.substring(0, dot);
+
+ entityInfo = findEntity(colPath);
+ }//if
+
+ if (entityInfo == null) {
+ throw new IllegalArgumentException("Missing entity alias prefix on column '" + tableColumn + "'");
+ }//if
+ }//if
+
+ EntityField field = entityInfo.getMetadata().getEntityField(tableColumn.getColumnName());
+ tableColumn.setColumnName(field.getColumn());
+ tableColumn.setTable(new Table(entityInfo.getTableAlias()));
+ if (currentPhase == Phase.WHERE && (!entityInfo.getTableAlias().equals("t1") || !field.isIdField())) {
+ selectUsingPrimaryKey = false;
+ }
+ }//if
+ else {
+ if (entityInfo.getMetadata().hasMultipleIdFields()) {
+ throw new IllegalArgumentException("WHERE on Entity columns with multiple ID fields are not supported - " + tableColumn);
+ }//if
+
+ tableColumn.setTable(new Table(entityInfo.getTableAlias()));
+ tableColumn.setColumnName(entityInfo.getMetadata().getIdField().getColumn());
+ if (currentPhase == Phase.WHERE && !entityInfo.getTableAlias().equals("t1")) {
+ selectUsingPrimaryKey = false;
+ }//if
+ }//else
+ }//visitColumn
+
+ @Override
+ public void visit(SelectExpressionItem selectExpressionItem)
+ {
+ selectExpressionItem.getExpression().accept(this);
+ }//visitSelectExpressionItem
+
+ @Override
+ public void visit(Function function)
+ {
+ if (function.getParameters() != null) {
+ for (Expression item : function.getParameters().getExpressions()) {
+ /*
+ * Only add a return type if the function was used in the select
+ */
+ if (currentPhase == Phase.SELECT) {
+ addResultType("c" + (returnTypes.size() + 1), Object.class);
+ }//if
+
+ item.accept(this);
+ }//for
+ }//if
+ }
+
+ @Override
+ public void visit(JdbcParameter jdbcParameter)
+ {
+ addQueryParameter(jdbcParameter, Object.class);
+
+ jdbcParameter.setUseFixedIndex(false);
+ jdbcParameter.setIndex(queryParameters.size());
+
+ /*
+ * Only add a return type if the parameter was used in the select
+ */
+ if (currentPhase == Phase.SELECT) {
+ addResultType("c" + (returnTypes.size() + 1), Object.class);
+ }//if
+ }//visitJdbcParameter
+
+ @Override
+ public void visit(JdbcNamedParameter jdbcNamedParameter)
+ {
+ addQueryParameter(jdbcNamedParameter, Object.class);
+ jdbcNamedParameter.setName("?");
+
+ /*
+ * Only add a return type if the parameter was used in the select
+ */
+ if (currentPhase == Phase.SELECT) {
+ addResultType("c" + (returnTypes.size() + 1), Object.class);
+ }//if
+ }//visitJdbcNamedParameter
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/parsers/JsqlVistorBase.java b/jpalite-core/src/main/java/io/jpalite/impl/parsers/JsqlVistorBase.java
new file mode 100644
index 0000000..6520294
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/parsers/JsqlVistorBase.java
@@ -0,0 +1,1023 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl.parsers;
+
+import net.sf.jsqlparser.expression.*;
+import net.sf.jsqlparser.expression.operators.arithmetic.*;
+import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
+import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
+import net.sf.jsqlparser.expression.operators.conditional.XorExpression;
+import net.sf.jsqlparser.expression.operators.relational.*;
+import net.sf.jsqlparser.schema.Column;
+import net.sf.jsqlparser.schema.Table;
+import net.sf.jsqlparser.statement.*;
+import net.sf.jsqlparser.statement.alter.Alter;
+import net.sf.jsqlparser.statement.alter.AlterSession;
+import net.sf.jsqlparser.statement.alter.AlterSystemStatement;
+import net.sf.jsqlparser.statement.alter.RenameTableStatement;
+import net.sf.jsqlparser.statement.alter.sequence.AlterSequence;
+import net.sf.jsqlparser.statement.analyze.Analyze;
+import net.sf.jsqlparser.statement.comment.Comment;
+import net.sf.jsqlparser.statement.create.index.CreateIndex;
+import net.sf.jsqlparser.statement.create.schema.CreateSchema;
+import net.sf.jsqlparser.statement.create.sequence.CreateSequence;
+import net.sf.jsqlparser.statement.create.synonym.CreateSynonym;
+import net.sf.jsqlparser.statement.create.table.CreateTable;
+import net.sf.jsqlparser.statement.create.view.AlterView;
+import net.sf.jsqlparser.statement.create.view.CreateView;
+import net.sf.jsqlparser.statement.delete.Delete;
+import net.sf.jsqlparser.statement.drop.Drop;
+import net.sf.jsqlparser.statement.execute.Execute;
+import net.sf.jsqlparser.statement.grant.Grant;
+import net.sf.jsqlparser.statement.insert.Insert;
+import net.sf.jsqlparser.statement.merge.Merge;
+import net.sf.jsqlparser.statement.replace.Replace;
+import net.sf.jsqlparser.statement.select.*;
+import net.sf.jsqlparser.statement.show.ShowTablesStatement;
+import net.sf.jsqlparser.statement.truncate.Truncate;
+import net.sf.jsqlparser.statement.update.Update;
+import net.sf.jsqlparser.statement.upsert.Upsert;
+import net.sf.jsqlparser.statement.values.ValuesStatement;
+
+public class JsqlVistorBase implements StatementVisitor, SelectVisitor, FromItemVisitor, SelectItemVisitor,
+ ExtraExpressionVisitor, GroupByVisitor, ItemsListVisitor, OrderByVisitor
+{
+ @Override
+ public void visit(Select select)
+ {
+ if (select.getWithItemsList() != null) {
+ for (WithItem withItem : select.getWithItemsList()) {
+ withItem.accept(this);
+ }
+ }
+ select.getSelectBody().accept(this);
+ }
+
+ @Override
+ public void visit(PlainSelect plainSelect)
+ {
+ if (plainSelect.getFromItem() != null) {
+ plainSelect.getFromItem().accept(this);
+ }
+
+ if (plainSelect.getJoins() != null) {
+ for (Join join : plainSelect.getJoins()) {
+ join.getRightItem().accept(this);
+ for (Expression vExpressions : join.getOnExpressions()) {
+ vExpressions.accept(this);
+ }//for
+ }//for
+ }//if
+
+ if (plainSelect.getSelectItems() != null) {
+ for (SelectItem item : plainSelect.getSelectItems()) {
+ item.accept(this);
+ }//for
+ }
+
+ if (plainSelect.getWhere() != null) {
+ plainSelect.getWhere().accept(this);
+ }
+
+ if (plainSelect.getHaving() != null) {
+ plainSelect.getHaving().accept(this);
+ }
+
+ if (plainSelect.getGroupBy() != null) {
+ plainSelect.getGroupBy().accept(this);
+ }//if
+
+ if (plainSelect.getOrderByElements() != null) {
+ for (OrderByElement element : plainSelect.getOrderByElements()) {
+ element.accept(this);
+ }
+ }//if
+ }//visit
+
+ @Override
+ public void visit(SetOperationList setOpList)
+ {
+ for (SelectBody select : setOpList.getSelects()) {
+ select.accept(this);
+ }//for
+ }
+
+ @Override
+ public void visit(BitwiseRightShift aThis)
+ {
+ aThis.accept(this);
+ }
+
+ @Override
+ public void visit(BitwiseLeftShift aThis)
+ {
+ aThis.accept(this);
+ }
+
+ @Override
+ public void visit(NullValue nullValue)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(Function function)
+ {
+ for (Expression item : function.getParameters().getExpressions()) {
+ item.accept(this);
+ }//for
+ }
+
+ @Override
+ public void visit(SignedExpression signedExpression)
+ {
+ if (signedExpression.getExpression() != null) {
+ signedExpression.getExpression().accept(this);
+ }//if
+ }
+
+ @Override
+ public void visit(JdbcParameter jdbcParameter)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(JdbcNamedParameter jdbcNamedParameter)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(BooleanValue aThis)
+ {
+ //Not Implemented
+ }
+
+ @Override
+ public void visit(DoubleValue doubleValue)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(LongValue longValue)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(HexValue hexValue)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(DateValue dateValue)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(TimeValue timeValue)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(TimestampValue timestampValue)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(Parenthesis parenthesis)
+ {
+ parenthesis.getExpression().accept(this);
+ }
+
+ @Override
+ public void visit(StringValue stringValue)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(Addition addition)
+ {
+ addition.getLeftExpression().accept(this);
+ addition.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(Division division)
+ {
+ division.getLeftExpression().accept(this);
+ division.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(IntegerDivision division)
+ {
+ division.getLeftExpression().accept(this);
+ division.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(Multiplication multiplication)
+ {
+ multiplication.getLeftExpression().accept(this);
+ multiplication.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(Subtraction subtraction)
+ {
+ subtraction.getLeftExpression().accept(this);
+ subtraction.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(AndExpression andExpression)
+ {
+ andExpression.getLeftExpression().accept(this);
+ andExpression.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(OrExpression orExpression)
+ {
+ orExpression.getLeftExpression().accept(this);
+ orExpression.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(XorExpression orExpression)
+ {
+ orExpression.getLeftExpression().accept(this);
+ orExpression.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(Between between)
+ {
+ between.getLeftExpression().accept(this);
+ between.getBetweenExpressionStart().accept(this);
+ between.getBetweenExpressionEnd().accept(this);
+ }
+
+ @Override
+ public void visit(EqualsTo equalsTo)
+ {
+ equalsTo.getLeftExpression().accept(this);
+ equalsTo.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(GreaterThan greaterThan)
+ {
+ greaterThan.getLeftExpression().accept(this);
+ greaterThan.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(GreaterThanEquals greaterThanEquals)
+ {
+ greaterThanEquals.getLeftExpression().accept(this);
+ greaterThanEquals.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(InExpression inExpression)
+ {
+ inExpression.getLeftExpression().accept(this);
+ if (inExpression.getRightExpression() == null) {
+ inExpression.getRightItemsList().accept(this);
+ }//if
+ else {
+ inExpression.getRightExpression().accept(this);
+ }
+ }
+
+ @Override
+ public void visit(FullTextSearch fullTextSearch)
+ {
+ throw new IllegalArgumentException("MATCH ACCEPT not supported in JQPL");
+ }
+
+ @Override
+ public void visit(IsNullExpression isNullExpression)
+ {
+ isNullExpression.getLeftExpression().accept(this);
+ }
+
+ @Override
+ public void visit(IsBooleanExpression isBooleanExpression)
+ {
+ isBooleanExpression.getLeftExpression().accept(this);
+ }
+
+ @Override
+ public void visit(LikeExpression likeExpression)
+ {
+ likeExpression.getLeftExpression().accept(this);
+ likeExpression.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(MinorThan minorThan)
+ {
+ minorThan.getLeftExpression().accept(this);
+ minorThan.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(MinorThanEquals minorThanEquals)
+ {
+ minorThanEquals.getLeftExpression().accept(this);
+ minorThanEquals.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(NotEqualsTo notEqualsTo)
+ {
+ notEqualsTo.getLeftExpression().accept(this);
+ notEqualsTo.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(Column tableColumn)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(AllColumns allColumns)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(AllTableColumns allTableColumns)
+ {
+ allTableColumns.accept((SelectItemVisitor) this);
+ }
+
+ @Override
+ public void visit(AllValue pAllValue)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(IsDistinctExpression pIsDistinctExpression)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(GeometryDistance pGeometryDistance)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(SelectExpressionItem selectExpressionItem)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(Table tableName)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(SubSelect subSelect)
+ {
+ //ignore
+ }
+
+ @Override
+ public void visit(ExpressionList expressionList)
+ {
+ for (Expression expression : expressionList.getExpressions()) {
+ expression.accept(this);
+ }//for
+ }
+
+ @Override
+ public void visit(NamedExpressionList namedExpressionList)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(MultiExpressionList multiExprList)
+ {
+ for (ExpressionList exprList : multiExprList.getExpressionLists()) {
+ for (Expression expr : exprList.getExpressions()) {
+ expr.accept(this);
+ }//for
+ }//for
+ }
+
+ @Override
+ public void visit(CaseExpression caseExpression)
+ {
+ for (WhenClause clause : caseExpression.getWhenClauses()) {
+ clause.accept(this);
+ }//if
+
+ if (caseExpression.getElseExpression() != null) {
+ caseExpression.getElseExpression().accept(this);
+ }//if
+ }
+
+ @Override
+ public void visit(WhenClause whenClause)
+ {
+ whenClause.getWhenExpression().accept(this);
+ }
+
+ @Override
+ public void visit(ExistsExpression existsExpression)
+ {
+ existsExpression.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(AnyComparisonExpression anyComparisonExpression)
+ {
+ anyComparisonExpression.accept(this);
+ }
+
+ @Override
+ public void visit(Concat concat)
+ {
+ concat.getLeftExpression().accept(this);
+ concat.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(Matches matches)
+ {
+ matches.getLeftExpression().accept(this);
+ matches.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(BitwiseAnd bitwiseAnd)
+ {
+ bitwiseAnd.getLeftExpression().accept(this);
+ bitwiseAnd.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(BitwiseOr bitwiseOr)
+ {
+ bitwiseOr.getLeftExpression().accept(this);
+ bitwiseOr.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(BitwiseXor bitwiseXor)
+ {
+ bitwiseXor.getLeftExpression().accept(this);
+ bitwiseXor.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(CastExpression cast)
+ {
+ cast.getLeftExpression().accept(this);
+ }
+
+ @Override
+ public void visit(TryCastExpression pTryCastExpression)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(Modulo modulo)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(AnalyticExpression aexpr)
+ {
+ if (aexpr.getExpression() != null) {
+ aexpr.getExpression().accept(this);
+ }//if
+ }
+
+ @Override
+ public void visit(ExtractExpression eexpr)
+ {
+ if (eexpr.getExpression() != null) {
+ eexpr.getExpression().accept(this);
+ }//if
+ }
+
+ @Override
+ public void visit(IntervalExpression iexpr)
+ {
+ if (iexpr.getExpression() != null) {
+ iexpr.getExpression().accept(this);
+ }//if
+ }
+
+ @Override
+ public void visit(OracleHierarchicalExpression oexpr)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(RegExpMatchOperator rexpr)
+ {
+ rexpr.getLeftExpression().accept(this);
+ rexpr.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(JsonExpression jsonExpr)
+ {
+ jsonExpr.getExpression().accept(this);
+ }
+
+ @Override
+ public void visit(JsonOperator jsonExpr)
+ {
+ jsonExpr.getLeftExpression().accept(this);
+ jsonExpr.getRightExpression().accept(this);
+ }
+
+ @Override
+ public void visit(RegExpMySQLOperator regExpMySQLOperator)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(UserVariable userVar)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(NumericBind bind)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(KeepExpression aexpr)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(MySQLGroupConcat groupConcat)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(ValueListExpression valueList)
+ {
+ valueList.getExpressionList().accept(this);
+ }
+
+ @Override
+ public void visit(RowConstructor rowConstructor)
+ {
+ rowConstructor.getExprList().accept(this);
+ }
+
+ @Override
+ public void visit(RowGetExpression rowGetExpression)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(OracleHint hint)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(TimeKeyExpression timeKeyExpression)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(DateTimeLiteralExpression literal)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(NotExpression aThis)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(NextValExpression aThis)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(CollateExpression aThis)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(SimilarToExpression aThis)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(ArrayExpression aThis)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(ArrayConstructor aThis)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(VariableAssignment aThis)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(XMLSerializeExpr aThis)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(TimezoneExpression aThis)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(JsonAggregateFunction aThis)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(JsonFunction aThis)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(ConnectByRootOperator aThis)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(OracleNamedFunctionParameter aThis)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(SubJoin subjoin)
+ {
+ subjoin.accept(this);
+ }
+
+ @Override
+ public void visit(LateralSubSelect lateralSubSelect)
+ {
+ lateralSubSelect.accept(this);
+ }
+
+ @Override
+ public void visit(ValuesList valuesList)
+ {
+ valuesList.accept(this);
+ }
+
+ @Override
+ public void visit(TableFunction tableFunction)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(ParenthesisFromItem aThis)
+ {
+ aThis.accept(this);
+ }
+
+ @Override
+ public void visit(Analyze pAnalyze)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(SavepointStatement savepointStatement)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(RollbackStatement rollbackStatement)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(Comment comment)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(Commit commit)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(Delete delete)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(Update update)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(Insert insert)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(Replace replace)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(Drop drop)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(Truncate truncate)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(CreateIndex createIndex)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(CreateSchema aThis)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(CreateTable createTable)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(CreateView createView)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(AlterView alterView)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(Alter alter)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(Statements stmts)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(Execute execute)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(SetStatement set)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(ResetStatement reset)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(ShowColumnsStatement set)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(ShowTablesStatement showTables)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(Merge merge)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(Upsert upsert)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(UseStatement use)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(Block block)
+ {
+ block.accept(this);
+ }
+
+ @Override
+ public void visit(WithItem withItem)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(ValuesStatement values)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(DescribeStatement describe)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(ExplainStatement aThis)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(ShowStatement aThis)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(DeclareStatement aThis)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(Grant grant)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(CreateSequence createSequence)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(AlterSequence alterSequence)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(CreateFunctionalStatement createFunctionalStatement)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(CreateSynonym createSynonym)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(AlterSession alterSession)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(IfElseStatement aThis)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(RenameTableStatement renameTableStatement)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(PurgeStatement purgeStatement)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(AlterSystemStatement alterSystemStatement)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(UnsupportedStatement pUnsupportedStatement)
+ {
+ //Not implemented
+ }
+
+ @Override
+ public void visit(GroupByElement groupBy)
+ {
+ groupBy.getGroupByExpressionList().accept(this);
+ }
+
+ @Override
+ public void visit(OrderByElement orderBy)
+ {
+ orderBy.getExpression().accept(this);
+ }
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/parsers/QueryParserFactory.java b/jpalite-core/src/main/java/io/jpalite/impl/parsers/QueryParserFactory.java
new file mode 100644
index 0000000..72e46aa
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/parsers/QueryParserFactory.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl.parsers;
+
+import io.jpalite.parsers.QueryParser;
+import io.jpalite.queries.QueryLanguage;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.PersistenceException;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static io.jpalite.JPALiteEntityManager.TRADESWITCH_OVERRIDE_BASIC_FETCHTYPE;
+import static io.jpalite.JPALiteEntityManager.TRADESWITCH_OVERRIDE_FETCHTYPE;
+
+public class QueryParserFactory
+{
+ private static final Map PARSED_QUERIES = new ConcurrentHashMap<>();
+
+ private QueryParserFactory()
+ {
+ }
+
+ /**
+ * Factory for query parsers used in TradeSwitch JPA
+ *
+ * @param rawQuery The JQPL query
+ * @param queryHints The query hints
+ */
+ public static QueryParser getParser(QueryLanguage language, String rawQuery, Map queryHints)
+ {
+ /*
+ * If we override the fetching definition on the entity, we need to reparse the query.
+ */
+ FetchType overrideFetch = (FetchType) queryHints.get(TRADESWITCH_OVERRIDE_FETCHTYPE);
+ FetchType overrideBasicFetch = (FetchType) queryHints.get(TRADESWITCH_OVERRIDE_BASIC_FETCHTYPE);
+ String cacheKey = rawQuery +
+ language +
+ ((overrideFetch == null) ? "NONE" : overrideFetch) +
+ ((overrideBasicFetch == null) ? "NONE" : overrideBasicFetch);
+
+ QueryParser parser = PARSED_QUERIES.get(cacheKey);
+ if (parser == null) {
+ parser = switch (language) {
+ case NATIVE -> new SQLParser(rawQuery, queryHints);
+ case JPQL -> new JPQLParser(rawQuery, queryHints);
+ default -> throw new PersistenceException("Not supported");
+ };
+
+ PARSED_QUERIES.put(cacheKey, parser);
+ }//if
+
+ return parser;
+ }//getParser
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/parsers/SQLParser.java b/jpalite-core/src/main/java/io/jpalite/impl/parsers/SQLParser.java
new file mode 100644
index 0000000..83cf132
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/parsers/SQLParser.java
@@ -0,0 +1,222 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl.parsers;
+
+import io.jpalite.impl.queries.QueryParameterImpl;
+import io.jpalite.parsers.QueryParser;
+import io.jpalite.parsers.QueryStatement;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static io.jpalite.JPALiteEntityManager.TRADESWITCH_ONLY_PRIMARYKEY_USED;
+
+@SuppressWarnings("java:S1452") //generic wildcard is required
+public class SQLParser implements QueryParser
+{
+ /**
+ * We may use either positional or named parameters, but we cannot mix them within the same query.
+ */
+ private boolean usingNamedParameters;
+ /**
+ * Map of parameters used in the query
+ */
+ private final List> queryParameters;
+ /**
+ * The parsed SQL statement
+ */
+ private final String query;
+ /**
+ * Thw query statement
+ */
+ private final QueryStatement queryStatement;
+ /**
+ * The query hints
+ */
+ @SuppressWarnings({"java:S1068", "unused", "MismatchedQueryAndUpdateOfCollection", "FieldCanBeLocal"})
+//Will be used later
+ private final Map queryHints;
+ /**
+ * Indicator that only primary keys are used
+ */
+ private boolean selectUsingPrimaryKey = false;
+
+ /**
+ * Constructor for the class. The method takes as input a JQPL Statement and converts it to a Native Statement. Note
+ * that the original pStatement is modified
+ *
+ * @param rawQuery The sql query
+ * @param queryHints The query hints
+ */
+ public SQLParser(String rawQuery, Map queryHints)
+ {
+ this.queryHints = new HashMap<>(queryHints);
+ usingNamedParameters = false;
+ queryParameters = new ArrayList<>();
+
+ if (queryHints.containsKey(TRADESWITCH_ONLY_PRIMARYKEY_USED)) {
+ selectUsingPrimaryKey = Boolean.parseBoolean(queryHints.get(TRADESWITCH_ONLY_PRIMARYKEY_USED).toString());
+ }//if
+
+ query = processSQLParameterLabels(rawQuery);
+ String statement = query.substring(0, query.indexOf(' ')).toUpperCase();
+ queryStatement = switch (statement) {
+ case "INSERT" -> QueryStatement.INSERT;
+ case "UPDATE" -> QueryStatement.UPDATE;
+ case "SELECT" -> QueryStatement.SELECT;
+ case "DELETE" -> QueryStatement.DELETE;
+ default -> QueryStatement.OTHER;
+ };
+ }//SQLParser
+
+ @Override
+ public QueryStatement getStatement()
+ {
+ return queryStatement;
+ }
+
+ @SuppressWarnings("java:S127") // We need to update the counter to skip the next character
+ private String processSQLParameterLabels(String inputQuery)
+ {
+ String sql = inputQuery;
+
+ boolean inSingleQuote = false;
+ boolean inDblQuote = false;
+ int nrParams = 0;
+ for (int i = 0; i < sql.length(); i++) {
+ switch (sql.charAt(i)) {
+ case ':':
+ if (!inSingleQuote && !inDblQuote) {
+ //Check if we have a double colon. If so, skip it
+ if (sql.length() > i + 1 && sql.charAt(i + 1) == ':') {
+ i++;
+ break;
+ }//if
+
+ //Find first space after the name eg :Param1 , :Param2
+ int end = sql.indexOf(',', i) - i;
+ int closeBracket = sql.indexOf(')', i) - i;
+ int space = sql.indexOf(' ', i) - i;
+ if (end < -1 || (closeBracket > -1 && end > closeBracket)) {
+ end = closeBracket;
+ }//if
+ if (end < -1 || space > -1 && end > space) {
+ end = space;
+ }//if
+ if (end < -1) {
+ end = sql.length();
+ }//if
+ else {
+ end += i;
+ }//else
+
+ nrParams++;
+
+ /*
+ * Catch case where Oracle syntax is used (col=:1, col=:2) and flag
+ * that as also using number based parameters
+ */
+ String parameterName = sql.substring(i + 1, end);
+ if (StringUtils.isNumeric(parameterName)) {
+ parameterName = null;
+ }//if
+ addQueryParameter(nrParams, parameterName);
+ sql = sql.substring(0, i) + "?" + sql.substring(end);
+ }//if
+ break;
+
+ case '?':
+ if (!inSingleQuote && !inDblQuote) {
+ nrParams++;
+ addQueryParameter(nrParams, null);
+ }//if
+ break;
+
+ case '\'':
+ inSingleQuote = !inSingleQuote;
+ break;
+
+ case '"':
+ inDblQuote = !inDblQuote;
+ break;
+
+ default:
+ break;
+ }//switch
+ }//for
+
+ return sql;
+ }//checkQuery
+
+ @Override
+ public String getQuery()
+ {
+ return query;
+ }//getQuery
+
+ @Override
+ public boolean isUsingNamedParameters()
+ {
+ return usingNamedParameters;
+ }
+
+ @Override
+ public int getNumberOfParameters()
+ {
+ return queryParameters.size();
+ }//getNumberOfParameters
+
+ @Override
+ public boolean isSelectUsingPrimaryKey()
+ {
+ return selectUsingPrimaryKey;
+ }
+
+ @Override
+ public List> getQueryParameters()
+ {
+ return queryParameters;
+ }
+
+ private void addQueryParameter(int index, String name)
+ {
+ if (name == null) {
+ if (queryParameters.isEmpty()) {
+ usingNamedParameters = false;
+ }//if
+ else if (usingNamedParameters) {
+ throw new IllegalArgumentException("Mixing positional and named parameters are not allowed");
+ }//else if
+
+ queryParameters.add(new QueryParameterImpl<>(index, Object.class));
+ }//if
+ else {
+ if (queryParameters.isEmpty()) {
+ usingNamedParameters = true;
+ }//if
+ else if (!usingNamedParameters) {
+ throw new IllegalArgumentException("Mixing positional and named parameters are not allowed");
+ }//else if
+
+ queryParameters.add(new QueryParameterImpl<>(name, queryParameters.size() + 1, Object.class));
+ }//else
+ }//addQueryParameter
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/providers/JPALiteEntityManagerFactoryImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/providers/JPALiteEntityManagerFactoryImpl.java
new file mode 100644
index 0000000..8b72455
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/providers/JPALiteEntityManagerFactoryImpl.java
@@ -0,0 +1,229 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl.providers;
+
+import io.jpalite.PersistenceContext;
+import io.jpalite.*;
+import io.jpalite.impl.EntityL2CacheImpl;
+import io.jpalite.impl.JPAConfig;
+import io.jpalite.impl.JPALiteEntityManagerImpl;
+import io.jpalite.impl.db.DatabasePoolFactory;
+import jakarta.persistence.*;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.metamodel.Metamodel;
+import lombok.extern.slf4j.Slf4j;
+
+import java.sql.SQLException;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Properties;
+import java.util.ServiceLoader;
+
+import static io.jpalite.JPALiteEntityManager.JPALITE_SHOW_SQL;
+import static io.jpalite.JPALiteEntityManager.PERSISTENCE_QUERY_LOG_SLOWTIME;
+import static io.jpalite.PersistenceContext.PERSISTENCE_JTA_MANAGED;
+
+@SuppressWarnings("unchecked")
+@Slf4j
+public class JPALiteEntityManagerFactoryImpl implements EntityManagerFactory
+{
+ private static final String NOT_SUPPORTED = "Not supported by current implementation";
+ private final long defaultSlowQueryTime = JPAConfig.getValue("jpalite.slowQueryTime", 500L);
+ private final boolean defaultShowQueries = JPAConfig.getValue("jpalite.showQueries", false);
+ private final String persistenceUnitName;
+ private boolean openFactory;
+
+ public JPALiteEntityManagerFactoryImpl(String persistenceUnitName)
+ {
+ this.persistenceUnitName = persistenceUnitName;
+ openFactory = true;
+
+ LOG.info("Building the Entity Manager Factory for EntityManager named {}", persistenceUnitName);
+ }
+
+ @Override
+ public EntityManager createEntityManager()
+ {
+ return entityManagerBuilder(SynchronizationType.UNSYNCHRONIZED, Collections.emptyMap());
+ }
+
+ @Override
+ public EntityManager createEntityManager(Map map)
+ {
+ return entityManagerBuilder(SynchronizationType.UNSYNCHRONIZED, map);
+ }
+
+ @Override
+ public EntityManager createEntityManager(SynchronizationType synchronizationType)
+ {
+ return entityManagerBuilder(synchronizationType, Collections.emptyMap());
+ }
+
+ @Override
+ public EntityManager createEntityManager(SynchronizationType pSynchronizationType, Map map)
+ {
+ return entityManagerBuilder(pSynchronizationType, map);
+ }
+
+ private JPALitePersistenceUnit getPersistenceUnit()
+ {
+ ServiceLoader loader = ServiceLoader.load(PersistenceUnitProvider.class);
+ for (PersistenceUnitProvider persistenceUnitProvider : loader) {
+ JPALitePersistenceUnit persistenceUnit = persistenceUnitProvider.getPersistenceUnit(persistenceUnitName);
+ if (persistenceUnit != null) {
+ if (persistenceUnit.getMultiTenantMode().equals(Boolean.TRUE)) {
+ ServiceLoader multiTenantLoader = ServiceLoader.load(MultiTenant.class);
+ for (MultiTenant multiTenant : multiTenantLoader) {
+ JPALitePersistenceUnit legacyPersistenceUnit = multiTenant.getPersistenceUnit(persistenceUnit);
+ if (legacyPersistenceUnit != null) {
+ return legacyPersistenceUnit;
+ }//if
+ }//for
+ }//if
+
+ return persistenceUnit;
+ }//if
+ }//for
+
+ LOG.warn(String.format("No PersistenceUnit was found for '%s'. %d SPI services found implementing PersistenceUnitProvider.class.",
+ persistenceUnitName, loader.stream().count()));
+ return null;
+ }//getPersistenceUnit
+
+ private PersistenceContext getPersistenceContext(SynchronizationType synchronizationType, Map properties) throws SQLException
+ {
+ JPALitePersistenceUnit persistenceUnit = getPersistenceUnit();
+ if (persistenceUnit == null) {
+ throw new PersistenceException("Unknown persistence unit " + persistenceUnitName);
+ }//if
+
+ DatabasePool databasePool = DatabasePoolFactory.getDatabasePool(persistenceUnit.getDataSourceName());
+
+ Properties localProperties = persistenceUnit.getProperties();
+ localProperties.putAll(properties);
+ localProperties.put(PERSISTENCE_JTA_MANAGED, synchronizationType == SynchronizationType.SYNCHRONIZED);
+ localProperties.putIfAbsent(PERSISTENCE_QUERY_LOG_SLOWTIME, defaultSlowQueryTime);
+ localProperties.putIfAbsent(JPALITE_SHOW_SQL, defaultShowQueries);
+
+ return databasePool.getPersistenceContext(persistenceUnit);
+ }//getPersistenceContext
+
+ private EntityManager entityManagerBuilder(SynchronizationType synchronizationType, Map entityProperties)
+ {
+ try {
+ PersistenceContext persistenceContext = getPersistenceContext(synchronizationType, entityProperties);
+ return new JPALiteEntityManagerImpl(persistenceContext, this);
+ }//try
+ catch (SQLException ex) {
+ throw new PersistenceException("Error connecting to the database", ex);
+ }//catch
+ }//entityBuilder
+
+ @Override
+ public CriteriaBuilder getCriteriaBuilder()
+ {
+ throw new UnsupportedOperationException(NOT_SUPPORTED);
+ }
+
+ @Override
+ public Metamodel getMetamodel()
+ {
+ throw new UnsupportedOperationException(NOT_SUPPORTED);
+ }
+
+ @Override
+ public boolean isOpen()
+ {
+ return openFactory;
+ }
+
+ @Override
+ public void close()
+ {
+ openFactory = false;
+ }
+
+ @Override
+ public Map getProperties()
+ {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public Cache getCache()
+ {
+ return new EntityL2CacheImpl(getPersistenceUnit());
+ }//getCache
+
+ @Override
+ public PersistenceUnitUtil getPersistenceUnitUtil()
+ {
+ return new PersistenceUnitUtil()
+ {
+ private JPAEntity checkEntity(Object entity)
+ {
+ if (entity instanceof JPAEntity jpaEntity) {
+ return jpaEntity;
+ }//if
+
+ throw new IllegalStateException(entity.getClass().getName() + " is not a TradeSwitch Entity");
+ }//checkEntity
+
+ @Override
+ public boolean isLoaded(Object entity, String field)
+ {
+ return !checkEntity(entity)._isLazyLoaded(field);
+ }
+
+ @Override
+ public boolean isLoaded(Object entity)
+ {
+ return !checkEntity(entity)._isLazyLoaded();
+ }
+
+ @Override
+ public Object getIdentifier(Object entity)
+ {
+ JPAEntity jpaEntity = checkEntity(entity);
+ return jpaEntity._getEntityState() == EntityState.TRANSIENT ? null : jpaEntity._getPrimaryKey();
+ }
+ };
+ }
+
+ @Override
+ public void addNamedQuery(String name, Query query)
+ {
+ throw new UnsupportedOperationException("Global Named Queries are not supported");
+ }
+
+ @Override
+ public T unwrap(Class cls)
+ {
+ if (cls.isAssignableFrom(this.getClass())) {
+ return (T) this;
+ }
+
+ throw new IllegalArgumentException("Could not unwrap this [" + this + "] as requested Java type [" + cls.getName() + "]");
+ }
+
+ @Override
+ public void addNamedEntityGraph(String graphName, EntityGraph entityGraph)
+ {
+ throw new UnsupportedOperationException(NOT_SUPPORTED);
+ }
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/providers/JPALitePersistenceProviderImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/providers/JPALitePersistenceProviderImpl.java
new file mode 100644
index 0000000..1b15a92
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/providers/JPALitePersistenceProviderImpl.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl.providers;
+
+import io.jpalite.JPAEntity;
+import jakarta.persistence.EntityManagerFactory;
+import jakarta.persistence.spi.LoadState;
+import jakarta.persistence.spi.PersistenceProvider;
+import jakarta.persistence.spi.PersistenceUnitInfo;
+import jakarta.persistence.spi.ProviderUtil;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class JPALitePersistenceProviderImpl implements PersistenceProvider
+{
+ private static final String NOT_SUPPORTED = "Not supported in the current implementation";
+
+ private final Map factory = new ConcurrentHashMap<>();
+
+ @Override
+ public EntityManagerFactory createEntityManagerFactory(String name, Map properties)
+ {
+ return factory.computeIfAbsent(name, JPALiteEntityManagerFactoryImpl::new);
+ }//createEntityManagerFactory
+
+ @Override
+ public EntityManagerFactory createContainerEntityManagerFactory(PersistenceUnitInfo info, Map properties)
+ {
+ throw new UnsupportedOperationException(NOT_SUPPORTED);
+ }//createEntityManagerFactory
+
+ @Override
+ public void generateSchema(PersistenceUnitInfo info, Map properties)
+ {
+ throw new UnsupportedOperationException(NOT_SUPPORTED);
+ }//generateSchema
+
+ @Override
+ public boolean generateSchema(String persistenceUnitName, Map properties)
+ {
+ throw new UnsupportedOperationException(NOT_SUPPORTED);
+ }
+
+ @Override
+ public ProviderUtil getProviderUtil()
+ {
+ return new ProviderUtil()
+ {
+ @Override
+ public LoadState isLoadedWithoutReference(Object entity, String attributeName)
+ {
+ if (entity instanceof JPAEntity) {
+ return LoadState.LOADED;
+ }//if
+
+ return LoadState.UNKNOWN;
+ }
+
+ @Override
+ public LoadState isLoadedWithReference(Object entity, String attributeName)
+ {
+ if (entity instanceof JPAEntity jpaEntity) {
+ return jpaEntity._loadState();
+ }//if
+
+ return LoadState.UNKNOWN;
+ }
+
+ @Override
+ public LoadState isLoaded(Object entity)
+ {
+ if (entity instanceof JPAEntity jpaEntity) {
+ return jpaEntity._loadState();
+ }//if
+
+ return LoadState.UNKNOWN;
+ }
+ };
+ }
+}
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/queries/EntityDeleteQueryImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/queries/EntityDeleteQueryImpl.java
new file mode 100644
index 0000000..784016a
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/queries/EntityDeleteQueryImpl.java
@@ -0,0 +1,101 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl.queries;
+
+import io.jpalite.EntityField;
+import io.jpalite.EntityMetaData;
+import io.jpalite.JPAEntity;
+import io.jpalite.queries.EntityQuery;
+import io.jpalite.queries.QueryLanguage;
+import jakarta.persistence.PersistenceException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class EntityDeleteQueryImpl implements EntityQuery
+{
+ private final EntityMetaData> metaData;
+ private final List parameters;
+ private final String query;
+
+ public EntityDeleteQueryImpl(JPAEntity entity, EntityMetaData> metaData)
+ {
+ this.metaData = metaData;
+ parameters = new ArrayList<>();
+ query = buildQuery(entity);
+ }//EntityDeleteQueryImpl
+
+ @Override
+ public QueryLanguage getLanguage()
+ {
+ return QueryLanguage.NATIVE;
+ }
+
+ private String buildQuery(JPAEntity entity)
+ {
+ EntityField[] idFields = metaData.getIdFields().toArray(new EntityField[0]);
+ if (idFields.length == 0) {
+ throw new PersistenceException("The entity have no @Id columns and cannot be deleted");
+ }//if
+
+ StringBuilder query = new StringBuilder();
+ query.append("delete from ")
+ .append(metaData.getTable())
+ .append(" where ");
+
+ int paramNr = 0;
+ for (EntityField field : idFields) {
+ if (paramNr > 0) {
+ query.append(" and ");
+ }//if
+ query.append(field.getColumn()).append("=?");
+ parameters.add(entity._getField(field.getName()));
+ paramNr++;
+ }//for
+
+ /*
+ The JPA Specification states that for versioned objects, it is permissible for an implementation to use
+ LockMode- Type.OPTIMISTIC_FORCE_INCREMENT where LockModeType.OPTIMISTIC was requested, but not vice versa.
+ We choose to handle Type.OPTIMISTIC as Type.OPTIMISTIC_FORCE_INCREMENT
+ */
+ if (entity._getMetaData().hasVersionField()) {
+ EntityField field = entity._getMetaData().getVersionField();
+ if (entity._isFieldModified(field.getName())) {
+ throw new PersistenceException("Version field was modified!");
+ }//if
+
+ query.append(" and ")
+ .append(field.getColumn()).append("=?");
+ parameters.add(entity._getField(field.getName()));
+ }//if
+
+ return query.toString();
+ }
+
+ @Override
+ public String getQuery()
+ {
+ return query;
+ }
+
+ @Override
+ public Object[] getParameters()
+ {
+ return parameters.toArray();
+ }
+}//EntityInsertInsertQuery
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/queries/EntityInsertQueryImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/queries/EntityInsertQueryImpl.java
new file mode 100644
index 0000000..f05fa17
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/queries/EntityInsertQueryImpl.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl.queries;
+
+import io.jpalite.EntityField;
+import io.jpalite.EntityMetaData;
+import io.jpalite.JPAEntity;
+import io.jpalite.MappingType;
+import io.jpalite.queries.EntityQuery;
+import io.jpalite.queries.QueryLanguage;
+
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.List;
+
+public class EntityInsertQueryImpl implements EntityQuery
+{
+ private final EntityMetaData> metaData;
+ private final List parameters;
+ private final String query;
+
+ public EntityInsertQueryImpl(JPAEntity entity, EntityMetaData> metaData)
+ {
+ this.metaData = metaData;
+ parameters = new ArrayList<>();
+ query = buildQuery(entity);
+ }//EntityInsertQueryImpl
+
+ @Override
+ public QueryLanguage getLanguage()
+ {
+ return QueryLanguage.NATIVE;
+ }
+
+ private Object generateVersionValue(EntityField versionField, Object currentVal)
+ {
+ return switch (versionField.getType().getSimpleName()) {
+ case "Long", "long" -> (currentVal == null) ? 1L : ((Long) currentVal) + 1;
+
+ case "Integer", "int" -> (currentVal == null) ? 1 : ((Integer) currentVal) + 1;
+
+ case "Timestamp" -> new Timestamp(System.currentTimeMillis());
+ default -> throw new IllegalStateException("Version field has unsupported type");
+ };
+ }//generateVersionValue
+
+ private String buildQuery(JPAEntity entity)
+ {
+ String sqlQuery = "insert into " + metaData.getTable() + "(";
+ StringBuilder columns = new StringBuilder("");
+ StringBuilder values = new StringBuilder("");
+
+ for (EntityField field : metaData.getEntityFields()) {
+ if (!field.isInsertable()
+ || field.getMappingType() == MappingType.ONE_TO_MANY
+ || (field.isNullable() && entity._isLazyLoaded(field.getName()))) {
+ continue;
+ }//if
+
+ Object val = entity._getField(field.getName());
+ if (field.isVersionField()) {
+ val = generateVersionValue(metaData.getVersionField(), val);
+ }//if
+
+ //If the column is nullable always update it. If not nullable
+ //and the value is null skip the column
+ if (field.isNullable() || val != null) {
+ if (columns.length() > 0) {
+ columns.append(",");
+ values.append(",");
+ }//if
+
+ columns.append(field.getColumn());
+ values.append("?");
+ if (val instanceof JPAEntity entityField) {
+ val = entityField._getPrimaryKey();
+ }//if
+ parameters.add(val);
+ }//if
+ }//for
+
+ String returnCols = "";
+ if (metaData.getIdField() != null) {
+ String versionCol = metaData.hasVersionField() ? "," + metaData.getVersionField().getColumn() : "";
+ returnCols = "returning " + metaData.getIdField().getColumn() + versionCol;
+ }//if
+
+ sqlQuery += columns + ") values(" + values + ")" + returnCols;
+
+ entity._clearModified();
+ return sqlQuery;
+ }
+
+ @Override
+ public String getQuery()
+ {
+ return query;
+ }
+
+ @Override
+ public Object[] getParameters()
+ {
+ return parameters.toArray();
+ }
+}//EntityInsertQueryImpl
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/queries/EntitySelectQueryImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/queries/EntitySelectQueryImpl.java
new file mode 100644
index 0000000..111e100
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/queries/EntitySelectQueryImpl.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl.queries;
+
+import io.jpalite.EntityMetaData;
+import io.jpalite.queries.EntityQuery;
+import io.jpalite.queries.QueryLanguage;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class EntitySelectQueryImpl implements EntityQuery
+{
+ private final EntityMetaData> metadata;
+ private final List parameters;
+ private final String query;
+ private QueryLanguage language;
+
+ public EntitySelectQueryImpl(Object primaryKey, EntityMetaData> metadata)
+ {
+ this.metadata = metadata;
+ parameters = new ArrayList<>();
+ parameters.add(primaryKey);
+ language = QueryLanguage.NATIVE;
+
+ query = buildQuery();
+ }
+
+ @Override
+ public QueryLanguage getLanguage()
+ {
+ return language;
+ }
+
+ private String buildQuery()
+ {
+ StringBuilder queryString = new StringBuilder("select ");
+
+ language = QueryLanguage.JPQL;
+
+ queryString.append(" e from ")
+ .append(metadata.getName())
+ .append(" e where e.")
+ .append(metadata.getIdField().getName())
+ .append("=?");
+
+ return queryString.toString();
+ }
+
+ @Override
+ public String getQuery()
+ {
+ return query;
+ }
+
+ @Override
+ public Object[] getParameters()
+ {
+ return parameters.toArray();
+ }
+}//EntitySelectQueryImpl
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/queries/EntityUpdateQueryImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/queries/EntityUpdateQueryImpl.java
new file mode 100644
index 0000000..a873d8f
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/queries/EntityUpdateQueryImpl.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl.queries;
+
+import io.jpalite.EntityField;
+import io.jpalite.EntityMetaData;
+import io.jpalite.JPAEntity;
+import io.jpalite.MappingType;
+import io.jpalite.queries.EntityQuery;
+import io.jpalite.queries.QueryLanguage;
+import jakarta.persistence.PersistenceException;
+
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.List;
+
+public class EntityUpdateQueryImpl implements EntityQuery
+{
+ private final EntityMetaData> metaData;
+ private final List parameters;
+ private final String query;
+
+ public EntityUpdateQueryImpl(JPAEntity entity, EntityMetaData> metaData)
+ {
+ this.metaData = metaData;
+ parameters = new ArrayList<>();
+ query = buildQuery(entity);
+ }//EntityInsertQuery
+
+ private Object generateVersionValue(EntityField versionField, Object currentVal)
+ {
+ return switch (versionField.getType().getSimpleName()) {
+ case "Long", "long" -> (currentVal == null) ? 1L : ((Long) currentVal) + 1;
+
+ case "Integer", "int" -> (currentVal == null) ? 1 : ((Integer) currentVal) + 1;
+
+ case "Timestamp" -> new Timestamp(System.currentTimeMillis());
+ default -> throw new IllegalStateException("Version field has unsupported type");
+ };
+ }//generateVersionValue
+
+ @Override
+ public QueryLanguage getLanguage()
+ {
+ return QueryLanguage.NATIVE;
+ }
+
+ @SuppressWarnings("java:S3776")//Complexity is reduced as far as possible
+ private void addFields(JPAEntity entity, StringBuilder columns, StringBuilder where, List whereParams)
+ {
+ for (EntityField field : metaData.getEntityFields()) {
+ if (!entity._isLazyLoaded(field.getName())) {
+ Object val = entity._getField(field.getName());
+
+ if (field.getMappingType() == MappingType.EMBEDDED && val instanceof JPAEntity vLinkEntity && vLinkEntity._isEntityModified()) {
+ addFields(vLinkEntity, columns, where, whereParams);
+ }//if
+ else {
+ if (field.isIdField() || field.isVersionField()) {
+ if (!where.isEmpty()) {
+ where.append(" and ");
+ }//if
+ where.append(field.getColumn()).append("=?");
+ whereParams.add(val);
+ }//if
+
+ if ((!field.isIdField() && entity._isFieldModified(field.getName()) && field.isUpdatable() && field.getMappingType() != MappingType.ONE_TO_MANY) || field.isVersionField()) {
+ /*
+ The JPA Specification states that for versioned objects, it is permissible for an implementation to use
+ LockMode- Type.OPTIMISTIC_FORCE_INCREMENT where LockModeType.OPTIMISTIC was requested, but not vice versa.
+ We choose to handle Type.OPTIMISTIC as Type.OPTIMISTIC_FORCE_INCREMENT
+ */
+ if (field.isVersionField()) {
+ if (entity._isFieldModified(field.getName())) {
+ throw new PersistenceException("Version field was modified!");
+ }//if
+
+ final Object newVersion = generateVersionValue(field, val);
+ val = newVersion;
+ entity._updateRestrictedField(e -> field.invokeSetter(e, newVersion));
+ }//if
+
+ if (val instanceof JPAEntity linkEntity) {
+ val = linkEntity._getPrimaryKey();
+ }//if
+
+ if (val != null || field.isNullable()) {
+ if (!columns.isEmpty()) {
+ columns.append(",");
+ }//if
+ columns.append(field.getColumn()).append("=?");
+ parameters.add(val);
+ }//if
+ }//if
+ }//else
+ }//if
+ }//for
+ }//addFields
+
+ private String buildQuery(JPAEntity entity)
+ {
+ if (!entity._isEntityModified()) {
+ return null;
+ }//if
+
+ String sqlQuery = "update " + metaData.getTable() + " set ";
+ StringBuilder columns = new StringBuilder();
+ StringBuilder where = new StringBuilder();
+ List params = new ArrayList<>();
+
+ addFields(entity, columns, where, params);
+
+ if (columns.isEmpty()) {
+ return null;
+ }
+
+ parameters.addAll(params);
+
+ return sqlQuery + columns + " where " + where;
+ }
+
+ @Override
+ public String getQuery()
+ {
+ return query;
+ }
+
+ @Override
+ public Object[] getParameters()
+ {
+ return parameters.toArray();
+ }
+}//EntityInsertInsertQuery
diff --git a/jpalite-core/src/main/java/io/jpalite/impl/queries/JPALiteQueryImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/queries/JPALiteQueryImpl.java
new file mode 100644
index 0000000..4c80fd0
--- /dev/null
+++ b/jpalite-core/src/main/java/io/jpalite/impl/queries/JPALiteQueryImpl.java
@@ -0,0 +1,1055 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.jpalite.impl.queries;
+
+import io.jpalite.PersistenceContext;
+import io.jpalite.*;
+import io.jpalite.impl.db.ConnectionWrapper;
+import io.jpalite.impl.parsers.QueryParserFactory;
+import io.jpalite.parsers.QueryParser;
+import io.jpalite.parsers.QueryStatement;
+import io.jpalite.queries.QueryLanguage;
+import io.opentelemetry.api.GlobalOpenTelemetry;
+import io.opentelemetry.api.trace.Span;
+import io.opentelemetry.api.trace.SpanKind;
+import io.opentelemetry.api.trace.Tracer;
+import io.opentelemetry.context.Scope;
+import jakarta.annotation.Nonnull;
+import jakarta.persistence.*;
+import lombok.extern.slf4j.Slf4j;
+
+import java.lang.reflect.InvocationTargetException;
+import java.sql.*;
+import java.util.Date;
+import java.util.*;
+
+import static io.jpalite.JPALiteEntityManager.*;
+import static jakarta.persistence.LockModeType.*;
+
+@Slf4j
+public class JPALiteQueryImpl implements Query
+{
+ private static final Tracer TRACER = GlobalOpenTelemetry.get().getTracer(JPALiteQueryImpl.class.getName());
+ public static final String SQL_QUERY = "query";
+ public static final String MIXING_POSITIONAL_AND_NAMED_PARAMETERS_ARE_NOT_ALLOWED = "Mixing positional and named parameters are not allowed";
+ /**
+ * The Persistence context link to the query
+ */
+ private final PersistenceContext persistenceContext;
+ /**
+ * The query (native) that will be executed
+ */
+ private String query;
+ /**
+ * The raw query that will be executed
+ */
+ private final String rawQuery;
+ /**
+ * The query language
+ */
+ private final QueryLanguage queryLanguage;
+ /**
+ * We may use either positional or named parameters but we cannot mix them within the same query.
+ */
+ private boolean usingNamedParameters = false;
+ /**
+ * True if selecting using primary key
+ */
+ private boolean selectUsingPrimaryKey = false;
+ private QueryStatement queryStatement = QueryStatement.OTHER;
+ /**
+ * The parameters that have been set
+ */
+ private List> params;
+ /**
+ * The query hints defined
+ */
+ private final Map hints;
+ /**
+ * The maximum number of rows to return for {@link #getResultList()}
+ */
+ private int maxResults = Integer.MAX_VALUE;
+ /**
+ * The number of rows in the cursor that should be skipped before returning a row.
+ */
+ private int firstResult = 0;
+ /**
+ * The lock mode of the returned item
+ */
+ private LockModeType lockMode = LockModeType.NONE;
+ /**
+ * The expected return type
+ */
+ private final Class> resultClass;
+
+ private String connectionName;
+ private int queryTimeout;
+ private int lockTimeout;
+ private boolean bypassL2Cache;
+ private boolean cacheResultList;
+ private boolean showSql;
+ private Class>[] queryResultTypes = null;
+ private FieldType fieldType;
+
+ /**
+ * This method supports both Native and JPQL based queries.
+ *
+ * resultClass defined the class the result will be mapped into and can be either and Entity Class or a base class
+ * or an array of base class types.
+ *
+ * The query language parameter defined the type of query. The following types are supported:
+ *
+ * JPQL queries
+ * JPQL queries can either be a single or a multi select query.
+ *
+ * A Single Select query is a query that only have one entity (eg select e from Employee e) or a specific field in
+ * an entity (eg select e.name from Employee E). In the case of a single select query resultClass MUST match the
+ * type of select expression.
+ *
+ * A Multi select query is a query that have more than one entity or entity fields eg (select e, d from Employee e
+ * JOIN e.department d) or (select e.name, e.department from Employee e). In the case of a multi select query
+ * resultClass must be an Object array (Object[].class).
+ *
+ * An exception to the above is if the selection return different unique types of only entities ( eg select e,
+ * e.department from Employee e) in which case resultClass could be the specific Entity in the multi select result
+ * set. This only applies to Entities and not entity fields!
+ *
+ *
+ *
+ * Native Queries
+ * Native Queries are normal SQL queries and can also have a single or multi select query. resultClass can either
+ * be a specific Entity class, a specific base class or a base type array. If an entity class is specified as the
+ * result class, the result set mapping process will try and use the column names found in the result set to map the
+ * result to the entity class.
+ *
+ * NOTE: Only the @Basic fields in the entity will (or can) be mapped.
+ *
+ * @param queryText The query to execute
+ * @param queryLanguage The query language
+ * @param persistenceContext The persistence context to use for the query
+ * @param resultClass The expected result class
+ */
+ public JPALiteQueryImpl(String queryText, QueryLanguage queryLanguage, PersistenceContext persistenceContext, Class resultClass, @Nonnull Map hints, LockModeType lockMode)
+ {
+ Span span = TRACER.spanBuilder("TradeSwitchQueryImpl::Init").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ if (queryText == null || queryText.isEmpty()) {
+ throw new IllegalArgumentException("No query was specified");
+ }//if
+
+ Boolean globalShowSQL = (Boolean) persistenceContext.getProperties().get(JPALITE_SHOW_SQL);
+ showSql = globalShowSQL != null && globalShowSQL;
+ this.lockMode = lockMode;
+ rawQuery = queryText;
+ this.queryLanguage = queryLanguage;
+ this.persistenceContext = persistenceContext;
+ this.resultClass = resultClass;
+ connectionName = persistenceContext.getConnectionName();
+ bypassL2Cache = false;
+ queryTimeout = 0;
+ lockTimeout = 0;
+ params = new ArrayList<>();
+ queryResultTypes = null;
+ query = null;
+ cacheResultList = false;
+
+ //Check that a valid return class was specified
+ checkResultClass(resultClass);
+
+ this.hints = new HashMap<>();
+ hints.forEach(this::setHint);
+
+ span.setAttribute("queryLang", this.queryLanguage.name());
+ span.setAttribute(SQL_QUERY, queryText);
+ }//try
+ finally {
+ span.end();
+ }
+ }//TradeSwitchQueryImpl
+
+ public JPALiteQueryImpl(String queryText, QueryLanguage queryLanguage, PersistenceContext persistenceContext, Class resultClass, @Nonnull Map hints)
+ {
+ this(queryText, queryLanguage, persistenceContext, resultClass, hints, NONE);
+ }//TradeSwitchQueryImpl
+
+ private void checkResultClass(Class> returnClass)
+ {
+ Class> checkedClass = returnClass;
+ if (checkedClass.isArray()) {
+ checkedClass = checkedClass.arrayType();
+ }//if
+
+ fieldType = FieldType.fieldType(checkedClass);
+ }//checkResultClass
+
+ private void checkUsingPositionalParameters()
+ {
+ if (params.isEmpty()) {
+ usingNamedParameters = false;
+ }
+ else if (usingNamedParameters) {
+ throw new IllegalArgumentException(MIXING_POSITIONAL_AND_NAMED_PARAMETERS_ARE_NOT_ALLOWED);
+ }//if
+ }
+
+ private void checkUsingNamedParameters()
+ {
+ if (params.isEmpty()) {
+ usingNamedParameters = true;
+ }
+ else if (!usingNamedParameters) {
+ throw new IllegalArgumentException(MIXING_POSITIONAL_AND_NAMED_PARAMETERS_ARE_NOT_ALLOWED);
+ }//if
+ }
+
+ private Object getColumnValue(Object entity, ResultSet resultSet, int columnNr)
+ {
+ try {
+ return switch (fieldType) {
+ case TYPE_BOOLEAN -> resultSet.getBoolean(columnNr);
+ case TYPE_INTEGER -> resultSet.getInt(columnNr);
+ case TYPE_LONGLONG -> resultSet.getLong(columnNr);
+ case TYPE_DOUBLEDOUBLE -> resultSet.getDouble(columnNr);
+ case TYPE_STRING -> resultSet.getString(columnNr);
+ case TYPE_TIMESTAMP -> resultSet.getTimestamp(columnNr);
+ case TYPE_OBJECT -> resultSet.getObject(columnNr);
+ case TYPE_ENTITY -> persistenceContext.mapResultSet(entity, "c" + columnNr + "_", resultSet);
+ default -> resultSet.getObject(columnNr);
+ };
+ }//try
+ catch (SQLException ex) {
+ throw new PersistenceException("SQL Error reading column from result set", ex);
+ }//catch
+ }//getColumnValue
+
+ @Nonnull
+ private Object[] buildArray(@Nonnull ResultSet resultSet)
+ {
+ List resultList = new ArrayList<>();
+ try {
+ if (queryResultTypes.length == 0) {
+ if (resultClass.isArray()) {
+ ResultSetMetaData metaData = resultSet.getMetaData();
+ for (int i = 1; i <= metaData.getColumnCount(); i++) {
+ resultList.add(getColumnValue(null, resultSet, i));
+ }//for
+ }//if
+ }//if
+ else {
+ for (int i = 1; i <= queryResultTypes.length; i++) {
+ resultList.add(getColumnValue(getNewObject(queryResultTypes[i - 1]), resultSet, i));
+ }//for
+ }//else
+ }//try
+ catch (SQLException ex) {
+ throw new PersistenceException("SQL Error mapping result to entity", ex);
+ }//catch
+
+ return resultList.toArray();
+ }//buildArray
+
+ private Object getNewObject(Class> returnClass)
+ {
+ if (fieldType == FieldType.TYPE_ENTITY) {
+ try {
+ return returnClass.getConstructor().newInstance();
+ }//try
+ catch (InstantiationException | IllegalAccessException | InvocationTargetException |
+ NoSuchMethodException pE) {
+ throw new PersistenceException("Error creating a new entity from class type " + returnClass.getName());
+ }//catch
+ }//if
+ return new Object();
+ }//getNewObject
+
+ protected Object mapResultSet(ResultSet resultSet)
+ {
+ if (resultClass.isArray() && !resultClass.isAssignableFrom(byte[].class)) {
+ return buildArray(resultSet);
+ }//if
+ else {
+ if (fieldType == FieldType.TYPE_ENTITY) {
+ if (queryResultTypes.length == 0) {
+ return persistenceContext.mapResultSet(getNewObject(resultClass), resultSet);
+ }//if
+ else {
+ return persistenceContext.mapResultSet(getNewObject(resultClass), "c1", resultSet);
+ }//else
+ }//if
+ else {
+ return getColumnValue(null, resultSet, 1);
+ }
+ }//else
+ }//mapResultSet
+
+ private PreparedStatement bindParameters(PreparedStatement statement) throws SQLException
+ {
+ for (QueryParameterImpl> parameter : params) {
+ if (parameter.getValue() != null) {
+ if (parameter.getValue().getClass().isAssignableFrom(Boolean.class)) {
+ statement.setObject(parameter.getPosition(), Boolean.TRUE.equals(parameter.getValue()) ? 1 : 0, Types.OTHER);
+ }
+ else {
+ if (parameter.getParameterType().equals(Object.class)) {
+ statement.setObject(parameter.getPosition(), parameter.getValue(), Types.OTHER);
+ }//if
+ else {
+ EntityMetaData> metaData = EntityMetaDataManager.getMetaData(parameter.getParameterType());
+ for (EntityField entityField : metaData.getEntityFields()) {
+ Object value = entityField.invokeGetter(parameter.getValue());
+ statement.setObject(parameter.getPosition(), value, Types.OTHER);
+ }//for
+ }//else
+ }//else
+ }//if
+ else {
+ statement.setNull(parameter.getPosition(), Types.OTHER);
+ }//else
+ }//for
+
+ return statement;
+ }//bindParameters
+
+ public String getConnectionName()
+ {
+ return connectionName;
+ }
+
+ private String getQuery()
+ {
+ if (query == null) {
+ processQuery();
+ }//if
+
+ return query;
+ }//getQuery
+
+ private String getQueryWithLimits(int firstResult, int maxResults)
+ {
+ String queryStr = getQuery();
+ if (queryStatement == QueryStatement.SELECT && (firstResult > 0 || maxResults < Integer.MAX_VALUE)) {
+ queryStr = "select * from (" + queryStr + ") __Q";
+ if (firstResult > 0) {
+ queryStr += " offset " + firstResult;
+ }//if
+
+ if (maxResults < Integer.MAX_VALUE) {
+ queryStr += " limit " + maxResults;
+ }//else
+ }//if
+
+ return queryStr;
+ }//applyLimits
+
+ private boolean isPessimisticLocking(LockModeType lockMode)
+ {
+ return (lockMode == PESSIMISTIC_READ || lockMode == PESSIMISTIC_FORCE_INCREMENT || lockMode == PESSIMISTIC_WRITE);
+ }//isPessimisticLocking
+
+ private String applyLocking(String sqlQuery)
+ {
+ if (queryStatement == QueryStatement.SELECT && isPessimisticLocking(lockMode)) {
+ return sqlQuery + switch (lockMode) {
+ case PESSIMISTIC_READ -> " FOR SHARE ";
+ case PESSIMISTIC_FORCE_INCREMENT, PESSIMISTIC_WRITE -> " FOR UPDATE ";
+ default -> "";
+ };
+ }//if
+ return sqlQuery;
+ }//applyLocking
+
+ @SuppressWarnings("java:S2077") // Dynamic formatted SQL is verified to be safe
+ private void applyLockTimeout(Statement statement)
+ {
+ if (queryStatement == QueryStatement.SELECT && lockTimeout > 0 && isPessimisticLocking(lockMode)) {
+ try {
+ statement.execute("SET LOCAL lock_timeout = '" + lockTimeout + "s'");
+ }//try
+ catch (SQLException ex) {
+ LOG.warn("Error setting lock timeout.", ex);
+ }//catch
+ }//if
+ }//applyLockTimeout
+
+ private Object executeQuery(String sqlQuery, SQLFunction function)
+ {
+ Span span = TRACER.spanBuilder("TradeSwitchQueryImpl::executeQuery").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent();
+ Connection connection = persistenceContext.getConnection(getConnectionName());
+ PreparedStatement vStatement = bindParameters(connection.prepareStatement(sqlQuery))) {
+
+ span.setAttribute(SQL_QUERY, sqlQuery);
+
+ if (JPAEntity.class.isAssignableFrom(resultClass)) {
+ persistenceContext.flushOnType(resultClass);
+ }//if
+
+ applyLockTimeout(vStatement);
+ vStatement.setQueryTimeout(queryTimeout);
+
+ boolean currentState = connection.unwrap(ConnectionWrapper.class).setEnableLogging(showSql);
+ try (ResultSet vResultSet = vStatement.executeQuery()) {
+ return function.apply(vResultSet);
+ }//try
+ finally {
+ connection.unwrap(ConnectionWrapper.class).setEnableLogging(currentState);
+ }//finally
+
+ }//try
+ catch (SQLTimeoutException ex) {
+ throw new QueryTimeoutException("Query timeout after " + queryTimeout + " seconds");
+ }//catch
+ catch (SQLException ex) {
+ if ("57014".equals(ex.getSQLState())) { //Postgresql state for query that timed out
+ throw new QueryTimeoutException("Query timeout after " + queryTimeout + " seconds");
+ }//if
+ else {
+ throw new PersistenceException("SQL Error executing the query: " + query, ex);
+ }//else
+ }//catch
+ finally {
+ span.end();
+ }//finally
+ }//executeQuery
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public List getResultList()
+ {
+ Span span = TRACER.spanBuilder("TradeSwitchQueryImpl::getResultList").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ span.setAttribute("resultType", resultClass.getSimpleName());
+
+ if (lockMode != LockModeType.NONE && !persistenceContext.getTransaction().isActive()) {
+ throw new TransactionRequiredException("No transaction is in progress");
+ }//if
+
+ if (maxResults < 0) {
+ return Collections.emptyList();
+ }//if
+
+ String queryStr = applyLocking(getQueryWithLimits(firstResult, maxResults));
+ return (List) executeQuery(queryStr, r ->
+ {
+ List resultList = new ArrayList<>();
+ while (r.next()) {
+ T entity = (T) mapResultSet(r);
+
+ if (isPessimisticLocking(lockMode)) {
+ ((JPAEntity) entity)._setLockMode(lockMode);
+ }//if
+
+ if (cacheResultList && entity instanceof JPAEntity jpaEntity && jpaEntity._getMetaData().isCacheable()) {
+ persistenceContext.l2Cache().add(jpaEntity);
+ }//if
+ resultList.add(entity);
+ }//while
+ return resultList;
+ });
+ }//try
+ finally {
+ span.end();
+ }
+ }//getResultList
+
+ @SuppressWarnings("unchecked")
+ private T checkCache()
+ {
+ T result = null;
+
+ if (selectUsingPrimaryKey) {
+ QueryParameterImpl> firstParam = params.stream().findFirst().orElse(null);
+
+ //Only check L1 cache if the primaryKey is set
+ if (firstParam != null) {
+ Object primaryKey = firstParam.getValue();
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Checking L1 cache for Entity [{}] using key [{}]", resultClass.getSimpleName(), primaryKey);
+ }//if
+
+ result = (T) persistenceContext.l1Cache().find(resultClass, primaryKey);
+ if (result == null) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Not found in L1 cache");
+ }//if
+ result = checkL2Cache(primaryKey);
+ }//if
+ }//if
+ }//if
+
+ return result;
+ }//checkCache
+
+ @SuppressWarnings("unchecked")
+ private T checkL2Cache(Object primaryKey)
+ {
+ T result = null;
+
+ if (selectUsingPrimaryKey && !bypassL2Cache) {
+ EntityMetaData metaData = EntityMetaDataManager.getMetaData(resultClass);
+ if (metaData.isCacheable()) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Checking L2 cache for Entity [{}] using key [{}]", resultClass.getSimpleName(), primaryKey);
+ }//if
+
+ result = (T) persistenceContext.l2Cache().find(resultClass, primaryKey);
+ if (result instanceof JPAEntity entity) {
+ persistenceContext.l1Cache().manage(entity);
+
+ FetchType hintValue = (FetchType) hints.get(TRADESWITCH_OVERRIDE_FETCHTYPE);
+ if (hintValue == null || hintValue.equals(FetchType.EAGER)) {
+ entity._lazyFetchAll(hintValue != null);
+ }//if
+ }//if
+ else {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Not found in L2 cache");
+ }//if
+ }
+ }//if
+ }//if
+
+ return result;
+ }//checkCache
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public T getSingleResult()
+ {
+ Span span = TRACER.spanBuilder("TradeSwitchQueryImpl::getSingleResult").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ span.setAttribute("resultType", resultClass.getSimpleName());
+
+ //Must parse the query before check the cache
+ String queryStr = applyLocking(getQueryWithLimits(firstResult, maxResults));
+
+ if (fieldType == FieldType.TYPE_ENTITY) {
+ T result = checkCache();
+ if (result != null) {
+ return result;
+ }//if
+ }//if
+
+
+ return (T) executeQuery(queryStr, r ->
+ {
+ if (r.next()) {
+ T result = (T) mapResultSet(r);
+
+ if (r.next()) {
+ throw new NonUniqueResultException("Query did not return a unique result");
+ }//if
+
+ if (result instanceof JPAEntity jpaEntity) {
+ if (jpaEntity._getMetaData().isCacheable() && !bypassL2Cache) {
+ persistenceContext.l2Cache().add(jpaEntity);
+ }//if
+
+ if (isPessimisticLocking(lockMode)) {
+ jpaEntity._setLockMode(lockMode);
+ }//if
+ }
+
+ span.setAttribute("result", "Result found");
+ return result;
+ }//if
+ else {
+ span.setAttribute("result", "No Result found");
+ throw new NoResultException("No Result found");
+ }//else
+ });
+ }//try
+ finally {
+ span.end();
+ }
+ }//getSingleResult
+
+ @Override
+ public int executeUpdate()
+ {
+ Span span = TRACER.spanBuilder("TradeSwitchQueryImpl::executeUpdate").setSpanKind(SpanKind.SERVER).startSpan();
+ try (Scope ignored = span.makeCurrent()) {
+ span.setAttribute(SQL_QUERY, getQuery());
+
+ if (queryStatement == QueryStatement.SELECT || queryStatement == QueryStatement.INSERT) {
+ throw new IllegalStateException("SELECT and INSERT is not allowed in executeUpdate");
+ }//if
+
+ try (Connection connection = persistenceContext.getConnection(getConnectionName());
+ PreparedStatement statement = bindParameters(connection.prepareStatement(getQuery()))) {
+ statement.setEscapeProcessing(false);
+
+ boolean currentState = connection.unwrap(ConnectionWrapper.class).setEnableLogging(showSql);
+ try {
+ return statement.executeUpdate();
+ }//try
+ finally {
+ connection.unwrap(ConnectionWrapper.class).setEnableLogging(currentState);
+ }//finally
+ }//try
+ catch (SQLException ex) {
+ throw new PersistenceException("SQL Error executing the update: " + query, ex);
+ }//catch
+ }//try
+ finally {
+ span.end();
+ }
+ }//executeUpdate
+
+ @Override
+ public Query setMaxResults(int maxResults)
+ {
+ if (maxResults < 0) {
+ throw new IllegalArgumentException("The max results value cannot be negative");
+ }//if
+
+ this.maxResults = maxResults;
+ return this;
+ }//setMaxResults
+
+ @Override
+ public int getMaxResults()
+ {
+ return maxResults;
+ }//getMaxResults
+
+ @Override
+ public Query setFirstResult(int startPosition)
+ {
+ if (startPosition < 0) {
+ throw new IllegalArgumentException("The first results value cannot be negative");
+ }//if
+
+ firstResult = startPosition;
+ return this;
+ }//setFirstResult
+
+ @Override
+ public int getFirstResult()
+ {
+ return firstResult;
+ }
+
+ @Override
+ @SuppressWarnings({"java:S6205", "unchecked"}) // This improves the readability of the assignment
+ public Query setHint(String hintName, Object value)
+ {
+ hints.put(hintName, value);
+ switch (hintName) {
+ case PERSISTENCE_QUERY_TIMEOUT -> {
+ if (value instanceof Long aLong) {
+ queryTimeout = aLong.intValue();
+ }
+ else if (value instanceof Integer anInteger) {
+ queryTimeout = anInteger;
+ }
+ else if (value instanceof String aString) {
+ queryTimeout = Integer.parseInt(aString);
+ }
+ }
+ case PERSISTENCE_LOCK_TIMEOUT -> {
+ if (value instanceof Long aLong) {
+ lockTimeout = aLong.intValue();
+ }
+ else if (value instanceof Integer anInteger) {
+ lockTimeout = anInteger;
+ }
+ else if (value instanceof String aString) {
+ lockTimeout = Integer.parseInt(aString);
+ }
+ }
+ case TRADESWITCH_CONNECTION_NAME -> connectionName = value.toString();
+ case PERSISTENCE_CACHE_RETRIEVEMODE -> {
+ if (value instanceof CacheRetrieveMode mode) {
+ bypassL2Cache = CacheRetrieveMode.BYPASS.equals(mode);
+ }
+ else {
+ bypassL2Cache = CacheRetrieveMode.BYPASS.equals(CacheRetrieveMode.valueOf(value.toString()));
+ }
+ }
+ case JPALITE_SHOW_SQL -> {
+ if (value instanceof Boolean showSqlHint) {
+ this.showSql = showSqlHint;
+ }//if
+ else {
+ showSql = Boolean.parseBoolean(value.toString());
+ }
+ }
+ case TRADESWITCH_CACHE_RESULTLIST -> {
+ EntityMetaData vMetaData = EntityMetaDataManager.getMetaData(resultClass);
+ if (vMetaData.isCacheable()) {
+ cacheResultList = Boolean.parseBoolean(value.toString());
+ }//if
+ else {
+ cacheResultList = false;
+ }//else
+ }
+ case TRADESWITCH_ONLY_PRIMARYKEY_USED -> selectUsingPrimaryKey = true;
+ case TRADESWITCH_OVERRIDE_BASIC_FETCHTYPE, TRADESWITCH_OVERRIDE_FETCHTYPE -> {
+ if (value instanceof FetchType fetchType) {
+ hints.put(hintName, fetchType);
+ }//if
+ else {
+ hints.put(hintName, FetchType.valueOf(value.toString()));
+ }
+ }
+ default -> LOG.trace("Unknown Query Hint[{}] - Ignored", hintName);
+ }//switch
+
+ return this;
+ }
+
+ @Override
+ public Map getHints()
+ {
+ return hints;
+ }
+
+ @SuppressWarnings("java:S6126") // IDE adds tabs and spaces in a text block
+ private void processQuery()
+ {
+ if (isPessimisticLocking(lockMode)) {
+ /**
+ * It is illegal to do a "SELECT FOR UPDATE" query that contains joins.
+ * We are forcing the parser to generate a query that do not have any joins.
+ */
+ hints.put(TRADESWITCH_OVERRIDE_FETCHTYPE, FetchType.LAZY);
+ }//if
+
+ try {
+ QueryParser parser = QueryParserFactory.getParser(queryLanguage, rawQuery, hints);
+ parser.checkType(resultClass);
+ queryResultTypes = parser.getReturnTypes().toArray(new Class>[0]);
+ query = parser.getQuery();
+
+ if (usingNamedParameters != parser.isUsingNamedParameters()) {
+ throw new IllegalArgumentException(MIXING_POSITIONAL_AND_NAMED_PARAMETERS_ARE_NOT_ALLOWED);
+ }//if
+
+ /**
+ * Check that the correct parameters are have value.
+ * Create a new list of parameters such that for every parameter used in the query
+ * an entry exists. The problem here is that for named parameters the same name could
+ * be used more than once in the query (which is okay)
+ */
+ List> parameters = new ArrayList<>();
+ parser.getQueryParameters().forEach(templateParam -> {
+ QueryParameterImpl> providedParameter = params.stream()
+ .filter(p -> p.getName().equals(templateParam.getName()))
+ .findFirst()
+ .orElse(null);
+
+ if (providedParameter == null) {
+ throw new IllegalArgumentException(String.format("Parameter '%s' is not set", templateParam.getName()));
+ }//if
+
+ parameters.add(templateParam.copyAndSet(providedParameter.getValue()));
+ });
+ params = parameters;
+
+ selectUsingPrimaryKey = parser.isSelectUsingPrimaryKey();
+ queryStatement = parser.getStatement();
+
+ if (showSql) {
+ LOG.info("\n------------ Query Parser -------------\n" +
+ "Query language: {}\n" +
+ "----------- Raw ----------\n" +
+ "{}\n" +
+ "---------- Parsed --------\n" +
+ "{}\n" +
+ "--------------------------------------",
+ queryLanguage, rawQuery, query);
+ }//if
+ }//try
+ catch (PersistenceException ex) {
+ LOG.error("Error parsing query. Language: {}, query: {}", queryLanguage, rawQuery);
+ throw new QueryParsingException("Error parsing query", ex);
+ }//catch
+ }//processQuery
+
+ @Override
+ public Query setParameter(Parameter param, X value)
+ {
+ if (param.getName() != null) {
+ return setParameter(param.getName(), value);
+ }//if
+
+ return setParameter(param.getPosition(), value);
+ }//setParameter
+
+ @Override
+ public Query setParameter(Parameter param, Calendar value, TemporalType temporalType)
+ {
+ if (param.getName() != null) {
+ return setParameter(param.getName(), value, temporalType);
+ }//if
+
+ return setParameter(param.getPosition(), value, temporalType);
+ }//setParameter
+
+ @Override
+ public Query setParameter(Parameter param, Date value, TemporalType temporalType)
+ {
+ if (param.getName() != null) {
+ return setParameter(param.getName(), value, temporalType);
+ }//if
+
+ return setParameter(param.getPosition(), value, temporalType);
+ }//setParameter
+
+ @SuppressWarnings("unchecked")
+ private QueryParameterImpl findOrCreateParameter(String name)
+ {
+ checkUsingNamedParameters();
+
+ QueryParameterImpl> param = params.stream()
+ .filter(p -> p.getName().equals(name))
+ .findFirst()
+ .orElse(null);
+ if (param == null) {
+ param = new QueryParameterImpl<>(name, params.size() + 1, Object.class);
+ params.add(param);
+ }
+
+ return (QueryParameterImpl) param;
+ }
+
+ @SuppressWarnings("unchecked")
+ private QueryParameterImpl findOrCreateParameter(int position)
+ {
+ checkUsingPositionalParameters();
+ QueryParameterImpl> param = params.stream()
+ .filter(p -> p.getPosition() == position)
+ .findFirst()
+ .orElse(null);
+ if (param == null) {
+ param = new QueryParameterImpl<>(position, Object.class);
+ params.add(param);
+ }
+
+ return (QueryParameterImpl) param;
+ }
+
+ @Override
+ public Query setParameter(String pName, Object value)
+ {
+ QueryParameterImpl parameter = findOrCreateParameter(pName);
+ parameter.setValue(value);
+
+ return this;
+ }//setParameter
+
+ @Override
+ public Query setParameter(String name, Calendar value, TemporalType temporalType)
+ {
+ QueryParameterImpl parameter = findOrCreateParameter(name);
+
+ switch (temporalType) {
+ case DATE -> parameter.setValue(new java.sql.Date(value.getTimeInMillis()));
+ case TIME -> parameter.setValue(new java.sql.Time(value.getTimeInMillis()));
+ case TIMESTAMP -> parameter.setValue(new Timestamp(value.getTimeInMillis()));
+ }//switch
+
+ return this;
+ }//setParameter
+
+ @Override
+ public Query setParameter(String name, Date value, TemporalType temporalType)
+ {
+ QueryParameterImpl parameter = findOrCreateParameter(name);
+
+ switch (temporalType) {
+ case DATE -> parameter.setValue(new java.sql.Date(value.getTime()));
+ case TIME -> parameter.setValue(new java.sql.Time(value.getTime()));
+ case TIMESTAMP -> parameter.setValue(new Timestamp(value.getTime()));
+ }//switch
+
+ return this;
+ }//setParameter
+
+ @Override
+ public Query setParameter(int position, Object value)
+ {
+ QueryParameterImpl parameter = findOrCreateParameter(position);
+ parameter.setValue(value);
+
+ return this;
+ }//setParameter
+
+ @Override
+ public Query setParameter(int position, Calendar value, TemporalType temporalType)
+ {
+ QueryParameterImpl parameter = findOrCreateParameter(position);
+
+ switch (temporalType) {
+ case DATE -> parameter.setValue(new java.sql.Date(value.getTimeInMillis()));
+ case TIME -> parameter.setValue(new java.sql.Time(value.getTimeInMillis()));
+ case TIMESTAMP -> parameter.setValue(new Timestamp(value.getTimeInMillis()));
+ }//switch
+
+ return this;
+ }//setParameter
+
+ @Override
+ public Query setParameter(int position, Date value, TemporalType temporalType)
+ {
+ QueryParameterImpl parameter = findOrCreateParameter(position);
+
+ switch (temporalType) {
+ case DATE -> parameter.setValue(new java.sql.Date(value.getTime()));
+ case TIME -> parameter.setValue(new java.sql.Time(value.getTime()));
+ case TIMESTAMP -> parameter.setValue(new Timestamp(value.getTime()));
+ }//switch
+
+ return this;
+ }//setParameter
+
+ @Override
+ public Set> getParameters()
+ {
+ return new HashSet<>(params);
+ }//getParameters
+
+ @Override
+ @Nonnull
+ public Parameter> getParameter(String name)
+ {
+ checkUsingNamedParameters();
+
+ Parameter> param = params.stream()
+ .filter(p -> p.getName().equals(name))
+ .findFirst()
+ .orElse(null);
+ if (param == null) {
+ throw new IllegalArgumentException("Named parameter [" + name + "] does not exist");
+ }//if
+
+ return param;
+ }//getParameters
+
+ @Override
+ @Nonnull
+ @SuppressWarnings("unchecked")
+ public Parameter getParameter(String name, Class type)
+ {
+ Parameter> parameter = getParameter(name);
+
+ if (!type.isAssignableFrom(parameter.getParameterType())) {
+ throw new IllegalArgumentException("Parameter [" + parameter.getParameterType().getName() + "] is not assignable to type " + type.getName());
+ }//if
+
+ return (Parameter) parameter;
+ }//getParameters
+
+ @Override
+ @Nonnull
+ public Parameter> getParameter(int position)
+ {
+ checkUsingPositionalParameters();
+ Parameter> param = params.stream()
+ .filter(p -> p.getPosition() == position)
+ .findFirst().orElse(null);
+ if (param == null) {
+ throw new IllegalArgumentException("Positional parameter [" + position + "] does not exist");
+ }//if
+
+ return param;
+ }//getParameters
+
+ @Override
+ @Nonnull
+ @SuppressWarnings("unchecked")
+ public