diff --git a/.github/workflows/qodana_code_quality.yml b/.github/workflows/qodana_code_quality.yml new file mode 100644 index 0000000..02d5a85 --- /dev/null +++ b/.github/workflows/qodana_code_quality.yml @@ -0,0 +1,20 @@ +name: Qodana +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + - initial + +jobs: + qodana: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: 'Qodana Scan' + uses: JetBrains/qodana-action@v2024.1 + env: + QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d3e03c9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +.settings +.project +.classpath +.metadata +*.stale + +# ItelliJ +.idea/ +*.iml + +# Silly Mac OSX stuff +.DS_Store +**/target/ diff --git a/LICENSE b/LICENSE index f288702..261eeb9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,674 +1,201 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. + 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 Parameter getParameter(int position, Class type) + { + Parameter parameter = (Parameter) getParameter(position); + + if (!type.isAssignableFrom(parameter.getParameterType())) { + throw new IllegalArgumentException("Parameter [" + parameter.getParameterType().getName() + "] is not assignable to type " + type.getName()); + }//if + + return parameter; + }//getParameters + + @Override + public boolean isBound(Parameter param) + { + if (param.getName() != null) { + return ((QueryParameterImpl) getParameter(param.getName())).isBounded(); + }//if + + return ((QueryParameterImpl) getParameter(param.getPosition())).isBounded(); + }//isBound + + @Override + @SuppressWarnings("unchecked") + public X getParameterValue(Parameter param) + { + if (param.getName() != null) { + return (X) getParameterValue(param.getName()); + }//if + + return (X) getParameterValue(param.getPosition()); + }//getParameterValue + + @Override + public Object getParameterValue(String name) + { + QueryParameterImpl vParameter = (QueryParameterImpl) getParameter(name); + + return vParameter.getValue(); + }//getParameterValue + + @Override + public Object getParameterValue(int position) + { + QueryParameterImpl vParameter = (QueryParameterImpl) getParameter(position); + + return vParameter.getValue(); + }//getParameterValue + + @Override + public Query setFlushMode(FlushModeType flushMode) + { + throw new UnsupportedOperationException("FlushMode is not supported"); + } + + @Override + public FlushModeType getFlushMode() + { + return FlushModeType.AUTO; + } + + @Override + public Query setLockMode(LockModeType lockMode) + { + this.lockMode = lockMode; + return this; + } + + @Override + public LockModeType getLockMode() + { + return lockMode; + } + + @Override + public X unwrap(Class cls) + { + 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/queries/NamedNativeQueryImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/queries/NamedNativeQueryImpl.java new file mode 100644 index 0000000..702d31f --- /dev/null +++ b/jpalite-core/src/main/java/io/jpalite/impl/queries/NamedNativeQueryImpl.java @@ -0,0 +1,33 @@ +/* + * 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.queries.QueryLanguage; +import jakarta.annotation.Nonnull; +import jakarta.persistence.NamedNativeQuery; + +import java.util.Map; + +public class NamedNativeQueryImpl extends TypedQueryImpl +{ + public NamedNativeQueryImpl(@Nonnull NamedNativeQuery namedQuery, PersistenceContext persistenceContext, Class entityClass, @Nonnull Map hints) + { + super(namedQuery.query(), QueryLanguage.NATIVE, persistenceContext, entityClass, NamedQueryImpl.buildHints(hints, namedQuery.hints())); + }//NamedNativeQueryImpl +}//NamedNativeQueryImpl diff --git a/jpalite-core/src/main/java/io/jpalite/impl/queries/NamedQueryImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/queries/NamedQueryImpl.java new file mode 100644 index 0000000..56ac734 --- /dev/null +++ b/jpalite-core/src/main/java/io/jpalite/impl/queries/NamedQueryImpl.java @@ -0,0 +1,46 @@ +/* + * 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.queries.QueryLanguage; +import jakarta.annotation.Nonnull; +import jakarta.persistence.NamedQuery; +import jakarta.persistence.QueryHint; + +import java.util.HashMap; +import java.util.Map; + +public class NamedQueryImpl extends TypedQueryImpl +{ + public static final @Nonnull Map buildHints(Map hints, QueryHint[] namedHints) + { + Map newHints = new HashMap<>(); + newHints.putAll(hints); + for (QueryHint hint : namedHints) { + newHints.put(hint.name(), hint.value()); + }//for + return newHints; + }//buildHints + + public NamedQueryImpl(@Nonnull NamedQuery namedQuery, PersistenceContext persistenceContext, Class entityClass, @Nonnull Map hints) + { + super(namedQuery.query(), QueryLanguage.JPQL, persistenceContext, entityClass, buildHints(hints, namedQuery.hints())); + setLockMode(namedQuery.lockMode()); + }//NamedQueryImpl +}//NamedQueryImpl diff --git a/jpalite-core/src/main/java/io/jpalite/impl/queries/NativeQueryImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/queries/NativeQueryImpl.java new file mode 100644 index 0000000..db4c8db --- /dev/null +++ b/jpalite-core/src/main/java/io/jpalite/impl/queries/NativeQueryImpl.java @@ -0,0 +1,32 @@ +/* + * 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.queries.QueryLanguage; +import jakarta.annotation.Nonnull; + +import java.util.Map; + +public class NativeQueryImpl extends JPALiteQueryImpl +{ + public NativeQueryImpl(String sqlQuery, PersistenceContext persistenceContext, Class resultClass, @Nonnull Map hints) + { + super(sqlQuery, QueryLanguage.NATIVE, persistenceContext, resultClass, hints); + } +}//NativeQueryImpl diff --git a/jpalite-core/src/main/java/io/jpalite/impl/queries/QueryImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/queries/QueryImpl.java new file mode 100644 index 0000000..72407f3 --- /dev/null +++ b/jpalite-core/src/main/java/io/jpalite/impl/queries/QueryImpl.java @@ -0,0 +1,33 @@ +/* + * 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.queries.QueryLanguage; +import jakarta.annotation.Nonnull; + +import java.util.Map; + +@SuppressWarnings({"java:S3740", "rawtypes", "unchecked"}) //Not use generics here +public class QueryImpl extends JPALiteQueryImpl +{ + public QueryImpl(String queryText, PersistenceContext persistenceContext, Class resultClass, @Nonnull Map hints) + { + super(queryText, QueryLanguage.JPQL, persistenceContext, resultClass, hints); + } +}//QueryImpl diff --git a/jpalite-core/src/main/java/io/jpalite/impl/queries/QueryParameterImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/queries/QueryParameterImpl.java new file mode 100644 index 0000000..2635ac1 --- /dev/null +++ b/jpalite-core/src/main/java/io/jpalite/impl/queries/QueryParameterImpl.java @@ -0,0 +1,93 @@ +/* + * 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 jakarta.persistence.Parameter; + +public class QueryParameterImpl implements Parameter +{ + private Integer position; + private final String name; + private final Class parameterType; + private T value; + private boolean bounded = false; + + public QueryParameterImpl(Integer position, Class parameterType) + { + name = String.valueOf(position); + this.position = position; + this.parameterType = parameterType; + } + + public QueryParameterImpl(String name, Integer position, Class parameterType) + { + this.name = name; + this.position = position; + this.parameterType = parameterType; + } + + public boolean isBounded() + { + return bounded; + } + + public T getValue() + { + if (!bounded) { + throw new IllegalStateException("Parameter [" + (name != null ? name : position) + "] is not set"); + }//if + + return value; + } + + public void setValue(T pValue) + { + value = pValue; + bounded = true; + } + + @Override + public String getName() + { + return name; + } + + @Override + public Integer getPosition() + { + return position; + } + + public void setPosition(Integer position) + { + this.position = position; + } + + @Override + public Class getParameterType() + { + return parameterType; + } + + public QueryParameterImpl copyAndSet(Object value) + { + QueryParameterImpl copy = new QueryParameterImpl<>(name, position, parameterType); + copy.setValue(copy.parameterType.cast(value)); + return copy; + }//copyAndSet +} diff --git a/jpalite-core/src/main/java/io/jpalite/impl/queries/SQLFunction.java b/jpalite-core/src/main/java/io/jpalite/impl/queries/SQLFunction.java new file mode 100644 index 0000000..5ab768e --- /dev/null +++ b/jpalite-core/src/main/java/io/jpalite/impl/queries/SQLFunction.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.impl.queries; + +import java.sql.SQLException; + +@FunctionalInterface +public interface SQLFunction +{ + R apply(T t) throws SQLException; +} diff --git a/jpalite-core/src/main/java/io/jpalite/impl/queries/TypedQueryImpl.java b/jpalite-core/src/main/java/io/jpalite/impl/queries/TypedQueryImpl.java new file mode 100644 index 0000000..337b688 --- /dev/null +++ b/jpalite-core/src/main/java/io/jpalite/impl/queries/TypedQueryImpl.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.PersistenceContext; +import io.jpalite.queries.QueryLanguage; +import jakarta.annotation.Nonnull; +import jakarta.persistence.*; + +import java.util.Calendar; +import java.util.Date; +import java.util.Map; + +@SuppressWarnings("unchecked") +public class TypedQueryImpl extends JPALiteQueryImpl implements TypedQuery +{ + public TypedQueryImpl(String queryText, QueryLanguage queryLanguage, PersistenceContext persistenceContext, Class entityClass, @Nonnull Map hints) + { + super(queryText, queryLanguage, persistenceContext, entityClass, hints); + } + + @Override + public TypedQuery setMaxResults(int maxResults) + { + return (TypedQuery) super.setMaxResults(maxResults); + }//setMaxResults + + @Override + public TypedQuery setFirstResult(int startPosition) + { + return (TypedQuery) super.setFirstResult(startPosition); + }//setFirstResult + + @Override + public TypedQuery setHint(String hintName, Object value) + { + return (TypedQuery) super.setHint(hintName, value); + }//setHint + + @Override + public TypedQuery setParameter(String name, Calendar value, TemporalType temporalType) + { + return (TypedQuery) super.setParameter(name, value, temporalType); + }//setParameter + + @Override + public TypedQuery setFlushMode(FlushModeType flushMode) + { + return (TypedQuery) super.setFlushMode(flushMode); + }//setFlushMode + + @Override + public TypedQuery setLockMode(LockModeType lockMode) + { + return (TypedQuery) super.setLockMode(lockMode); + }//setLockMode + + @Override + public

TypedQuery setParameter(Parameter

param, P value) + { + return (TypedQuery) super.setParameter(param, value); + }//setParameter + + @Override + public TypedQuery setParameter(Parameter param, Calendar value, TemporalType temporalType) + { + return (TypedQuery) super.setParameter(param, value, temporalType); + }//setParameter + + @Override + public TypedQuery setParameter(Parameter param, Date value, TemporalType temporalType) + { + return (TypedQuery) super.setParameter(param, value, temporalType); + }//setParameter + + @Override + public TypedQuery setParameter(String name, Object value) + { + return (TypedQuery) super.setParameter(name, value); + }//setParameter + + @Override + public TypedQuery setParameter(String name, Date value, TemporalType temporalType) + { + return (TypedQuery) super.setParameter(name, value, temporalType); + }//setParameter + + @Override + public TypedQuery setParameter(int position, Object value) + { + return (TypedQuery) super.setParameter(position, value); + }//setParameter + + @Override + public TypedQuery setParameter(int position, Calendar value, TemporalType temporalType) + { + return (TypedQuery) super.setParameter(position, value, temporalType); + }//setParameter + + @Override + public TypedQuery setParameter(int position, Date value, TemporalType temporalType) + { + return (TypedQuery) super.setParameter(position, value, temporalType); + }//setParameter +}//TypedQueryImpl diff --git a/jpalite-core/src/main/java/io/jpalite/impl/serializers/JPAEntityMarshaller.java b/jpalite-core/src/main/java/io/jpalite/impl/serializers/JPAEntityMarshaller.java new file mode 100644 index 0000000..d813777 --- /dev/null +++ b/jpalite-core/src/main/java/io/jpalite/impl/serializers/JPAEntityMarshaller.java @@ -0,0 +1,252 @@ +/* + * 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.serializers; + +import io.jpalite.*; +import org.infinispan.protostream.ProtobufTagMarshaller; +import org.infinispan.protostream.TagReader; +import org.infinispan.protostream.TagWriter; +import org.infinispan.protostream.annotations.impl.GeneratedMarshallerBase; +import org.infinispan.protostream.impl.BaseMarshallerDelegate; +import org.infinispan.protostream.impl.ByteArrayOutputStreamEx; +import org.infinispan.protostream.impl.SerializationContextImpl; +import org.infinispan.protostream.impl.TagWriterImpl; + +import java.io.*; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +public class JPAEntityMarshaller extends GeneratedMarshallerBase implements ProtobufTagMarshaller +{ + private final Class entityClass; + + public JPAEntityMarshaller(Class entityClass) + { + this.entityClass = entityClass; + } + + @Override + public Class getJavaClass() + { + return entityClass; + } + + @Override + public String getTypeName() + { + return "org.tradeswitch." + entityClass.getSimpleName(); + } + + @Override + public T read(ReadContext context) throws IOException + { + TagReader reader = context.getReader(); + + try { + EntityMetaData metaData = EntityMetaDataManager.getMetaData(entityClass); + + T entity = (T) metaData.getNewEntity(); + + int tag = reader.readTag(); + for (EntityField field : metaData.getEntityFields()) { + if (tag == 0) { + //if tag is zero we have reached the end + break; + }//if + + int fieldTag; + if (field.getFieldType() == FieldType.TYPE_ENTITY) { + EntityMetaData entityMetaData = EntityMetaDataManager.getMetaData(field.getType()); + if (entityMetaData.getEntityType() == EntityType.ENTITY_EMBEDDABLE) { + fieldTag = field.getFieldType().getWireTypeTag(field.getFieldNr()); + }//if + else { + fieldTag = entityMetaData.getIdField().getFieldType().getWireTypeTag(field.getFieldNr()); + }//else + }//if + else { + fieldTag = field.getFieldType().getWireTypeTag(field.getFieldNr()); + }//else + + if (fieldTag == tag) { + Object value = readField(context, reader, metaData, entity, field); + field.invokeSetter(entity, value); + tag = reader.readTag(); + }//if + }//for + ((JPAEntity) entity)._clearModified(); + + return entity; + }//try + catch (Throwable ex) { + throw new IOException("Error reading Entity", ex); + }//catch + }//read + + @SuppressWarnings("java:S6205")//Block is not redundant + private Object readField(ReadContext context, TagReader reader, EntityMetaData metaData, T entity, EntityField field) throws Throwable + { + return switch (field.getFieldType()) { + case TYPE_BOOLEAN, TYPE_BOOL -> reader.readBool(); + case TYPE_INTEGER, TYPE_INT -> reader.readInt32(); + case TYPE_LONGLONG, TYPE_LONG -> reader.readInt64(); + case TYPE_DOUBLEDOUBLE, TYPE_DOUBLE -> reader.readDouble(); + case TYPE_STRING -> reader.readString(); + case TYPE_ENUM -> { + String enumName = reader.readString(); + for (Object enumVal : field.getType().getEnumConstants()) { + if (((Enum) enumVal).name().equals(enumName)) { + yield enumVal; + }//if + }//for + + yield null; + } + case TYPE_ORDINAL_ENUM -> field.getType().getEnumConstants()[reader.readInt32()]; + case TYPE_TIMESTAMP -> new Timestamp(reader.readFixed64()); + case TYPE_LOCALTIME -> new Timestamp(reader.readFixed64()).toLocalDateTime(); + case TYPE_BYTES -> reader.readByteArray(); + case TYPE_OBJECT -> { + ByteArrayInputStream in = new ByteArrayInputStream(reader.readByteArray()); + ObjectInputStream stream = new ObjectInputStream(in); + yield stream.readObject(); + } + case TYPE_CUSTOMTYPE -> readNestedEntity(context, reader, field.getType()); + case TYPE_ENTITY -> { + if (metaData.getEntityType() == EntityType.ENTITY_EMBEDDABLE) { + yield readNestedEntity(context, reader, field.getType()); + }//if + else { + EntityMetaData subMetaData = EntityMetaDataManager.getMetaData(field.getType()); + if (subMetaData == null) { + throw new IOException(field.getType() + " is not an entity"); + }//if + + Object primaryKey; + //If we have multiple keys then that primary key will be stored in an embedded object + if (metaData.hasMultipleIdFields()) { + primaryKey = readNestedEntity(context, reader, field.getType()); + }//if + else { + primaryKey = readField(context, reader, metaData, entity, subMetaData.getIdField()); + }//else + + JPAEntity sub = (JPAEntity) subMetaData.getNewEntity(); + sub._makeReference(primaryKey); + yield sub; + }//else + } + }; + }//readField + + private Object readNestedEntity(ReadContext context, TagReader reader, Class objectClass) throws IOException + { + BaseMarshallerDelegate delegate = ((SerializationContextImpl) context.getSerializationContext()).getMarshallerDelegate(objectClass); + + int length = reader.readUInt32(); + int oldLimit = reader.pushLimit(length); + + Object nestedObj = readMessage(delegate, context); + + reader.checkLastTagWas(0); + reader.popLimit(oldLimit); + + return nestedObj; + } + + @Override + public void write(WriteContext context, T entity) throws IOException + { + try { + writeEntity(context, context.getWriter(), (JPAEntity) entity); + }//try + catch (Throwable ex) { + throw new IOException("Error writing Entity", ex); + }//catch + }//write + + private void writeNestedEntity(WriteContext context, TagWriter writer, int fieldNr, JPAEntity entity) throws Throwable + { + ByteArrayOutputStreamEx out = new ByteArrayOutputStreamEx(); + TagWriterImpl nestedWriter = TagWriterImpl.newNestedInstance(context, out); + writeEntity(context, nestedWriter, entity); + writer.writeBytes(fieldNr, out.getByteBuffer()); + }//writeNestedEntity + + private void writeField(WriteContext context, TagWriter writer, Class typeClass, FieldType fieldType, int fieldNr, Object value) throws Throwable + { + switch (fieldType) { + case TYPE_BOOLEAN -> writer.writeBool(fieldNr, (Boolean) value); + case TYPE_INTEGER -> writer.writeInt32(fieldNr, (Integer) value); + case TYPE_LONGLONG -> writer.writeInt64(fieldNr, (Long) value); + case TYPE_DOUBLEDOUBLE -> writer.writeDouble(fieldNr, (Double) value); + case TYPE_BOOL -> writer.writeBool(fieldNr, (boolean) value); + case TYPE_INT -> writer.writeInt32(fieldNr, (int) value); + case TYPE_LONG -> writer.writeInt64(fieldNr, (long) value); + case TYPE_DOUBLE -> writer.writeDouble(fieldNr, (double) value); + case TYPE_STRING -> writer.writeString(fieldNr, (String) value); + case TYPE_ENUM -> writer.writeString(fieldNr, ((Enum) value).name()); + case TYPE_ORDINAL_ENUM -> writer.writeInt32(fieldNr, ((Enum) value).ordinal()); + case TYPE_TIMESTAMP -> writer.writeFixed64(fieldNr, ((Timestamp) value).getTime()); + case TYPE_BYTES -> writer.writeBytes(fieldNr, ((byte[]) value)); + case TYPE_LOCALTIME -> + writer.writeFixed64(fieldNr, Timestamp.from(((LocalDateTime) value).toInstant(ZoneOffset.UTC)).getTime()); + case TYPE_OBJECT -> { + ByteArrayOutputStream recvOut = new ByteArrayOutputStream(); + ObjectOutputStream stream = new ObjectOutputStream(recvOut); + stream.writeObject(value); + stream.flush(); + writer.writeBytes(fieldNr, recvOut.toByteArray()); + } + case TYPE_CUSTOMTYPE -> { + @SuppressWarnings("java:S3740")//Can't use generics here + BaseMarshallerDelegate delegate = ((SerializationContextImpl) context.getSerializationContext()).getMarshallerDelegate(typeClass); + writeNestedMessage(delegate, context, fieldNr, value); + } + case TYPE_ENTITY -> { + EntityMetaData metaData = ((JPAEntity) value)._getMetaData(); + + //If it is not an embeddable entity then we only store the primary key + if (metaData.getEntityType() == EntityType.ENTITY_EMBEDDABLE) { + writeNestedEntity(context, writer, fieldNr, (JPAEntity) value); + }//if + else { + //If we have multiple keys then that primary key will be stored in an embedded object + if (metaData.hasMultipleIdFields()) { + writeNestedEntity(context, writer, fieldNr, (JPAEntity) ((JPAEntity) value)._getPrimaryKey()); + }//if + else { + EntityField vKeyField = metaData.getIdField(); + writeField(context, writer, vKeyField.getType(), vKeyField.getFieldType(), fieldNr, ((JPAEntity) value)._getPrimaryKey()); + }//else + }//else + } + }//switch + }//writeField + + private void writeEntity(WriteContext context, TagWriter writer, JPAEntity entity) throws Throwable + { + for (EntityField field : entity._getMetaData().getEntityFields()) { + Object value = field.invokeGetter(entity); + if (value != null) { + writeField(context, writer, field.getType(), field.getFieldType(), field.getFieldNr(), value); + }//if + }//for + }//writeEntity +} diff --git a/jpalite-core/src/main/java/io/jpalite/parsers/QueryParser.java b/jpalite-core/src/main/java/io/jpalite/parsers/QueryParser.java new file mode 100644 index 0000000..0daed7b --- /dev/null +++ b/jpalite-core/src/main/java/io/jpalite/parsers/QueryParser.java @@ -0,0 +1,107 @@ +/* + * 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.parsers; + +import io.jpalite.impl.queries.JPALiteQueryImpl; +import io.jpalite.impl.queries.QueryParameterImpl; + +import java.util.Collections; +import java.util.List; + +/** + * The QueryParser interface is to be implemented by all classes that implements Query Parsers used by + * {@link JPALiteQueryImpl} to parser query of various languages into a format that can be executed by the + * Persistent context linked to query + */ +@SuppressWarnings("java:S1452") //generic wildcard is required +public interface QueryParser +{ + /** + * Get the query statement + * + * @return The query statement + */ + QueryStatement getStatement(); + + /** + * Return the parsed query + * + * @return The parsed and converted query + */ + String getQuery(); + + /** + * Provides a check to determine the type of query parameters was used in the raw query + * + * @return True for named and false for positional base parameters + */ + boolean isUsingNamedParameters(); + + /** + * Indicates if the query has parameters + */ + int getNumberOfParameters(); + + /** + * The query parameters found in the raw query. The map is in the order that the parameter appeared in the query. + * The key of the map is either the named parameter or the positional number. + * + * @return A parameter map + */ + List> getQueryParameters(); + + /** + * The return types return for each select item. The list specifies the java class type for each of the select items + * in the raw query. + *

+ * Note that the parsed query could have more select items that what is contained in the list. + *

+ * If the parser do not support this an empty list is returned + *

+ * + * @return The list + */ + default List> getReturnTypes() + { + return Collections.emptyList(); + } + + /** + * Check to see if only PK was used in the where clause. + * + * @return True if PK was used + */ + default boolean isSelectUsingPrimaryKey() + { + return false; + } + + /** + * Check if the given return type is provided by the JQPL guery. If not an IllegalArgumentException exception is + * generated + *

+ * If not used the method always return true + *

+ * + * @param typeToCheck The class to check + * @throws IllegalArgumentException if the type is not provided + */ + default void checkType(Class typeToCheck) + { + } +}//QueryParser diff --git a/jpalite-core/src/main/java/io/jpalite/parsers/QueryStatement.java b/jpalite-core/src/main/java/io/jpalite/parsers/QueryStatement.java new file mode 100644 index 0000000..d496be0 --- /dev/null +++ b/jpalite-core/src/main/java/io/jpalite/parsers/QueryStatement.java @@ -0,0 +1,27 @@ +/* + * 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.parsers; + +public enum QueryStatement +{ + SELECT, + UPDATE, + DELETE, + INSERT, + OTHER +} diff --git a/jpalite-core/src/main/java/io/jpalite/queries/EntityQuery.java b/jpalite-core/src/main/java/io/jpalite/queries/EntityQuery.java new file mode 100644 index 0000000..f7d2934 --- /dev/null +++ b/jpalite-core/src/main/java/io/jpalite/queries/EntityQuery.java @@ -0,0 +1,27 @@ +/* + * 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.queries; + +public interface EntityQuery +{ + String getQuery(); + + Object[] getParameters(); + + QueryLanguage getLanguage(); +} diff --git a/jpalite-core/src/main/java/io/jpalite/queries/QueryLanguage.java b/jpalite-core/src/main/java/io/jpalite/queries/QueryLanguage.java new file mode 100644 index 0000000..a97656e --- /dev/null +++ b/jpalite-core/src/main/java/io/jpalite/queries/QueryLanguage.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.queries; + +public enum QueryLanguage +{ + NATIVE, + SQL, + JPQL, + GRID +} diff --git a/jpalite-core/src/main/resources/META-INF/native-image/reflect-config.json b/jpalite-core/src/main/resources/META-INF/native-image/reflect-config.json new file mode 100644 index 0000000..95e4cc2 --- /dev/null +++ b/jpalite-core/src/main/resources/META-INF/native-image/reflect-config.json @@ -0,0 +1,109 @@ +[ + { + "name": "io.jpalite.impl.JPALiteEntityManagerImpl", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "allPublicFields": true, + "unsafeAllocated": true + }, + { + "name": "io.jpalite.impl.db.DatabasePoolImpl", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "allPublicFields": true, + "unsafeAllocated": true + }, + { + "name": "io.jpalite.impl.providers.JPALiteEntityManagerFactoryImpl", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "allPublicFields": true, + "unsafeAllocated": true + }, + { + "name": "io.jpalite.impl.providers.JPALitePersistenceProviderImpl", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "allPublicFields": true, + "unsafeAllocated": true + }, + { + "name": "io.jpalite.impl.providers.JPALiteEntityManagerFactoryImpl", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "allPublicFields": true, + "unsafeAllocated": true + }, + { + "name": "io.jpalite.impl.providers.JPALiteEntityManagerFactoryImpl", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "allPublicFields": true, + "unsafeAllocated": true + }, + { + "name": "io.agroal.pool.DataSource", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "allPublicFields": true, + "unsafeAllocated": true + }, + { + "name": "io.agroal.pool.ConnectionHandler[]", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "allPublicFields": true, + "unsafeAllocated": true + }, + { + "name": "org.infinispan.client.hotrod.RemoteCacheManager", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "allPublicFields": true + }, + { + "name": "sun.util.resources.CurrencyNames", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "allPublicFields": true + }, + { + "name": "com.sun.rowset.providers.RIOptimisticProvider", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "allPublicFields": true + } +] diff --git a/jpalite-core/src/main/resources/META-INF/native-image/resource-config.json b/jpalite-core/src/main/resources/META-INF/native-image/resource-config.json new file mode 100644 index 0000000..96fe03e --- /dev/null +++ b/jpalite-core/src/main/resources/META-INF/native-image/resource-config.json @@ -0,0 +1,21 @@ +{ + "bundles": [ + { + "name": "com.sun.rowset.RowSetResourceBundle" + } + ], + "resources": { + "includes": [ + { + "pattern": ".*/rowset.properties$" + }, + { + "pattern": "META-INF/services/*$" + }, + { + "pattern": ".*\\.PersistenceProvider" + } + ], + "excludes": [] + } +} diff --git a/jpalite-core/src/main/resources/META-INF/services/jakarta.persistence.spi.PersistenceProvider b/jpalite-core/src/main/resources/META-INF/services/jakarta.persistence.spi.PersistenceProvider new file mode 100644 index 0000000..cf10318 --- /dev/null +++ b/jpalite-core/src/main/resources/META-INF/services/jakarta.persistence.spi.PersistenceProvider @@ -0,0 +1,18 @@ +# +# 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. +# + +io.jpalite.impl.providers.JPALitePersistenceProviderImpl diff --git a/jpalite-core/src/test/java/io/jpalite/jqpl/Company.java b/jpalite-core/src/test/java/io/jpalite/jqpl/Company.java new file mode 100644 index 0000000..ae08656 --- /dev/null +++ b/jpalite-core/src/test/java/io/jpalite/jqpl/Company.java @@ -0,0 +1,41 @@ +/* + * 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.jqpl; + +import io.jpalite.impl.JPAEntityImpl; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Data; + +@Entity +@Table(name = "COMPANY") +@Data +public class Company extends JPAEntityImpl +{ + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "IRN", updatable = false) + private long id; + + @Column(name = "NAME") + private String name; +}//Company diff --git a/jpalite-core/src/test/java/io/jpalite/jqpl/Department.java b/jpalite-core/src/test/java/io/jpalite/jqpl/Department.java new file mode 100644 index 0000000..cc3ef35 --- /dev/null +++ b/jpalite-core/src/test/java/io/jpalite/jqpl/Department.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.jqpl; + +import jakarta.persistence.*; +import lombok.Data; +import io.jpalite.impl.JPAEntityImpl; + +import java.util.List; + +@Entity +@Table(name = "DEPT") +@Data +public class Department extends JPAEntityImpl +{ + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "IRN", updatable = false) + private Integer id; + + @Column(name = "NAME") + private String name; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "COMP", nullable = false) + private Company company; + + @OneToMany(mappedBy = "department") + private List employees; +}//Department diff --git a/jpalite-core/src/test/java/io/jpalite/jqpl/Department1.java b/jpalite-core/src/test/java/io/jpalite/jqpl/Department1.java new file mode 100644 index 0000000..59cb896 --- /dev/null +++ b/jpalite-core/src/test/java/io/jpalite/jqpl/Department1.java @@ -0,0 +1,48 @@ +/* + * 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.jqpl; + +import io.jpalite.impl.JPAEntityImpl; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Data; + +@Entity +@Table(name = "DEPT") +@Data +public class Department1 extends JPAEntityImpl +{ + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "IRN", updatable = false) + private int id; + + @Column(name = "NAME") + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "COMP") + private Company company; +}//Department diff --git a/jpalite-core/src/test/java/io/jpalite/jqpl/Employee.java b/jpalite-core/src/test/java/io/jpalite/jqpl/Employee.java new file mode 100644 index 0000000..a0f112a --- /dev/null +++ b/jpalite-core/src/test/java/io/jpalite/jqpl/Employee.java @@ -0,0 +1,58 @@ +/* + * 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.jqpl; + +import io.jpalite.impl.JPAEntityImpl; +import jakarta.persistence.*; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +/** + * + */ +@Entity +@Table(name = "EMPLOYEE") +@Data +public class Employee extends JPAEntityImpl +{ + + @Id + @GeneratedValue + @Column(name = "IRN") + private int id; + + @Embedded + private FullName fullName; + + @Column(name = "AGE") + private int age; + + @Column(name = "SALARY") + @Basic(fetch = FetchType.LAZY) + private BigDecimal salary; + + @ManyToOne + @JoinColumn(name = "DEPT", nullable = false) + private Department department; + + @OneToMany(mappedBy = "employee") + private List phones; +}//Employee + +//--------------------------------------------------------------------[ End ]--- diff --git a/jpalite-core/src/test/java/io/jpalite/jqpl/Employee1.java b/jpalite-core/src/test/java/io/jpalite/jqpl/Employee1.java new file mode 100644 index 0000000..33983dc --- /dev/null +++ b/jpalite-core/src/test/java/io/jpalite/jqpl/Employee1.java @@ -0,0 +1,62 @@ +/* + * 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.jqpl; + +import jakarta.persistence.Basic; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Data; +import io.jpalite.impl.JPAEntityImpl; + +import java.math.BigDecimal; + +/** + * + */ +@Entity +@Table(name = "EMPLOYEE") +@Data +public class Employee1 extends JPAEntityImpl +{ + + @Id + @GeneratedValue + @Column(name = "IRN") + private int id; + + @Column(name = "NAME") + private String name; + + @Column(name = "AGE") + private int age; + + @Column(name = "SALARY") + @Basic(fetch = FetchType.LAZY) + private BigDecimal salary; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "DEPT") + private Department1 department; +}//Employee + +//--------------------------------------------------------------------[ End ]--- diff --git a/jpalite-core/src/test/java/io/jpalite/jqpl/FullName.java b/jpalite-core/src/test/java/io/jpalite/jqpl/FullName.java new file mode 100644 index 0000000..cdbe168 --- /dev/null +++ b/jpalite-core/src/test/java/io/jpalite/jqpl/FullName.java @@ -0,0 +1,49 @@ +/* + * 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.jqpl; + +import io.jpalite.impl.JPAEntityImpl; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import lombok.Data; + +/** + * + */ +@Embeddable +@Data +public class FullName extends JPAEntityImpl +{ + + public FullName() + { + } + + public FullName(String pName, String pSurname) + { + name = pName; + surname = pSurname; + } + + @Column(name = "NAME") + private String name; + + @Column(name = "SURNAME") + private String surname; +}//FullName + +//--------------------------------------------------------------------[ End ]--- diff --git a/jpalite-core/src/test/java/io/jpalite/jqpl/JPQLParserTest.java b/jpalite-core/src/test/java/io/jpalite/jqpl/JPQLParserTest.java new file mode 100644 index 0000000..63b3154 --- /dev/null +++ b/jpalite-core/src/test/java/io/jpalite/jqpl/JPQLParserTest.java @@ -0,0 +1,609 @@ +/* + * 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.jqpl; + +import io.jpalite.JPALiteEntityManager; +import jakarta.persistence.FetchType; +import net.sf.jsqlparser.JSQLParserException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import io.jpalite.EntityMetaData; +import io.jpalite.EntityMetaDataManager; +import io.jpalite.impl.EntityMetaDataImpl; +import io.jpalite.impl.parsers.JPQLParser; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class JPQLParserTest +{ + static { + // This is a hack to get the JPA to work + EntityMetaDataManager.register(new EntityMetaDataImpl<>(RatePlan.class)); + EntityMetaDataManager.register(new EntityMetaDataImpl<>(FullName.class)); + EntityMetaDataManager.register(new EntityMetaDataImpl<>(Employee.class)); + EntityMetaDataManager.register(new EntityMetaDataImpl<>(Department.class)); + EntityMetaDataManager.register(new EntityMetaDataImpl<>(Company.class)); + EntityMetaDataManager.register(new EntityMetaDataImpl<>(Phone.class)); + + EntityMetaDataManager.register(new EntityMetaDataImpl<>(Employee1.class)); + EntityMetaDataManager.register(new EntityMetaDataImpl<>(Department1.class)); + } + + @Test + void testUsingSelectIn() + { + JPQLParser vParser = new JPQLParser("select RatePlan from RatePlan where (uid, resourceVersion) in (select e.uid, max(e.resourceVersion) from RatePlan e group by e.uid)", new HashMap<>()); + Assertions.assertEquals("SELECT t1.ID \"c1-1\", t1.UID \"c1-2\", t1.RESOURCE_VERSION \"c1-3\", t1.OPERATOR_ID \"c1-4\", t1.PLAN_NAME \"c1-5\", t1.CREATED_BY \"c1-6\", t1.APPROVED_BY \"c1-7\", t1.EFFECTIVE_DATE \"c1-8\", " + + "t1.RATE_PLAN_CONFIG \"c1-9\", t1.MODIFIED_ON \"c1-10\", t1.CREATED_DATE \"c1-11\" " + + "FROM RATE_PLAN t1 " + + "WHERE (t1.UID, t1.RESOURCE_VERSION) IN (SELECT t2.UID \"c1\", max(t2.RESOURCE_VERSION) \"c2\" " + + "FROM RATE_PLAN t2 GROUP BY t2.UID)", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(RatePlan.class, vParser.getReturnTypes().get(0)); + } + + @Test + void testUsingBitAndOperators() throws JSQLParserException + { + JPQLParser vParser = new JPQLParser("select E from Employee E where bitand(E.department.id, :flag) = :flag", new HashMap<>()); + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2-1\", t1.SURNAME \"c1-2-2\", t1.AGE \"c1-3\", " + + "t2.IRN \"c1-5-1\", t2.NAME \"c1-5-2\", " + + "t3.IRN \"c1-5-3-1\", t3.NAME \"c1-5-3-2\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN " + + "WHERE bitand(t2.IRN, ?) = ?", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee.class, vParser.getReturnTypes().get(0)); + + vParser = new JPQLParser("select Employee from Employee where bitand(department.id, :flag) = :flag", new HashMap<>()); + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2-1\", t1.SURNAME \"c1-2-2\", t1.AGE \"c1-3\", " + + "t2.IRN \"c1-5-1\", t2.NAME \"c1-5-2\", " + + "t3.IRN \"c1-5-3-1\", t3.NAME \"c1-5-3-2\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN " + + "WHERE bitand(t2.IRN, ?) = ?", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee.class, vParser.getReturnTypes().get(0)); + + vParser = new JPQLParser("select Employee1 from Employee1 where bitand(department.id, :flag) = :flag", new HashMap<>()); + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2\", t1.AGE \"c1-3\", t1.DEPT \"c1-5\" " + + "FROM EMPLOYEE t1 " + + "LEFT JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "WHERE bitand(t2.IRN, ?) = ?", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee1.class, vParser.getReturnTypes().get(0)); + } + + @Test + void whenUsingBracketsInWhereClauses() throws JSQLParserException + { + JPQLParser vParser = new JPQLParser("select E from Employee E where (E.department.id = :val)", new HashMap<>()); + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2-1\", t1.SURNAME \"c1-2-2\", t1.AGE \"c1-3\", " + + "t2.IRN \"c1-5-1\", t2.NAME \"c1-5-2\", " + + "t3.IRN \"c1-5-3-1\", t3.NAME \"c1-5-3-2\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN " + + "WHERE (t2.IRN = ?)", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee.class, vParser.getReturnTypes().get(0)); + + vParser = new JPQLParser("select Employee from Employee where (department.id = :val)", new HashMap<>()); + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2-1\", t1.SURNAME \"c1-2-2\", t1.AGE \"c1-3\", " + + "t2.IRN \"c1-5-1\", t2.NAME \"c1-5-2\", " + + "t3.IRN \"c1-5-3-1\", t3.NAME \"c1-5-3-2\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN " + + "WHERE (t2.IRN = ?)", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee.class, vParser.getReturnTypes().get(0)); + } + + @Test + void whenPathExpressionIsUsedForSingleValuedAssociation_thenCreatesImplicitInnerWithLazyJoin() throws JSQLParserException + { + JPQLParser vParser = new JPQLParser("SELECT e FROM Employee1 e", new HashMap<>()); + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2\", t1.AGE \"c1-3\", t1.DEPT \"c1-5\" " + + "FROM EMPLOYEE t1", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee1.class, vParser.getReturnTypes().get(0)); + + vParser = new JPQLParser("SELECT e FROM Employee1 e JOIN e.department", new HashMap<>()); + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2\", t1.AGE \"c1-3\", " + + "t2.IRN \"c1-5-1\", t2.NAME \"c1-5-2\", t2.COMP \"c1-5-3\" " + + "FROM EMPLOYEE t1 " + + "LEFT JOIN DEPT t2 ON t1.DEPT = t2.IRN", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee1.class, vParser.getReturnTypes().get(0)); + } + + @Test + void whenOverridingFetchTypes_thenGenerateQueryCorrect() throws JSQLParserException + { + Map vHints = new HashMap<>(); + vHints.put(JPALiteEntityManager.TRADESWITCH_OVERRIDE_BASIC_FETCHTYPE, FetchType.EAGER); + JPQLParser vParser = new JPQLParser("SELECT e FROM Employee e", vHints); + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2-1\", t1.SURNAME \"c1-2-2\", t1.AGE \"c1-3\", t1.SALARY \"c1-4\", " + + "t2.IRN \"c1-5-1\", t2.NAME \"c1-5-2\", " + + "t3.IRN \"c1-5-3-1\", t3.NAME \"c1-5-3-2\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee.class, vParser.getReturnTypes().get(0)); + + vHints = new HashMap<>(); + vHints.put(JPALiteEntityManager.TRADESWITCH_OVERRIDE_FETCHTYPE, FetchType.LAZY); + vParser = new JPQLParser("SELECT e FROM Employee e", vHints); + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2-1\", t1.SURNAME \"c1-2-2\", t1.AGE \"c1-3\", t1.DEPT \"c1-5\" " + + "FROM EMPLOYEE t1", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee.class, vParser.getReturnTypes().get(0)); + + vHints = new HashMap<>(); + vHints.put(JPALiteEntityManager.TRADESWITCH_OVERRIDE_FETCHTYPE, FetchType.LAZY); + vHints.put(JPALiteEntityManager.TRADESWITCH_OVERRIDE_BASIC_FETCHTYPE, FetchType.EAGER); + vParser = new JPQLParser("SELECT e FROM Employee e", vHints); + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2-1\", t1.SURNAME \"c1-2-2\", t1.AGE \"c1-3\", " + + "t1.SALARY \"c1-4\", t1.DEPT \"c1-5\" " + + "FROM EMPLOYEE t1", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee.class, vParser.getReturnTypes().get(0)); + } + + @Test + void whenPathExpressionIsUsedForSingleValuedAssociation_thenCreatesImplicitInnerJoin() throws JSQLParserException + { + JPQLParser vParser = new JPQLParser("SELECT e FROM Employee e", new HashMap<>()); + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2-1\", t1.SURNAME \"c1-2-2\", t1.AGE \"c1-3\", " + + "t2.IRN \"c1-5-1\", t2.NAME \"c1-5-2\", " + + "t3.IRN \"c1-5-3-1\", t3.NAME \"c1-5-3-2\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee.class, vParser.getReturnTypes().get(0)); + + vParser = new JPQLParser("SELECT Employee FROM Employee", new HashMap<>()); + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2-1\", t1.SURNAME \"c1-2-2\", t1.AGE \"c1-3\", " + + "t2.IRN \"c1-5-1\", t2.NAME \"c1-5-2\", " + + "t3.IRN \"c1-5-3-1\", t3.NAME \"c1-5-3-2\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee.class, vParser.getReturnTypes().get(0)); + + vParser = new JPQLParser("SELECT e FROM Employee e JOIN e.department", new HashMap<>()); + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2-1\", t1.SURNAME \"c1-2-2\", t1.AGE \"c1-3\", " + + "t2.IRN \"c1-5-1\", t2.NAME \"c1-5-2\", " + + "t3.IRN \"c1-5-3-1\", t3.NAME \"c1-5-3-2\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee.class, vParser.getReturnTypes().get(0)); + + vParser = new JPQLParser("select e.department.id, e.department.company from Employee e", new HashMap<>()); + Assertions.assertEquals("SELECT t2.IRN \"c1\", " + + "t3.IRN \"c2-1\", t3.NAME \"c2-2\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN", + vParser.getQuery()); + Assertions.assertEquals(2, vParser.getReturnTypes().size()); + Assertions.assertEquals(Integer.class, vParser.getReturnTypes().get(0)); + Assertions.assertEquals(Company.class, vParser.getReturnTypes().get(1)); + + vParser = new JPQLParser("select e.fullName.name, e.department.name, e.department.id from Employee e", new HashMap<>()); + Assertions.assertEquals("SELECT t1.NAME \"c1\", t2.NAME \"c2\", t2.IRN \"c3\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN", + vParser.getQuery()); + Assertions.assertEquals(3, vParser.getReturnTypes().size()); + Assertions.assertEquals(String.class, vParser.getReturnTypes().get(0)); + Assertions.assertEquals(String.class, vParser.getReturnTypes().get(1)); + Assertions.assertEquals(Integer.class, vParser.getReturnTypes().get(2)); + + vParser = new JPQLParser("select e.fullName, e.department from Employee e", new HashMap<>()); + Assertions.assertEquals("SELECT t1.NAME \"c1-1\", t1.SURNAME \"c1-2\", t2.IRN \"c2-1\", t2.NAME \"c2-2\", " + + "t3.IRN \"c2-3-1\", t3.NAME \"c2-3-2\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN", + vParser.getQuery()); + Assertions.assertEquals(2, vParser.getReturnTypes().size()); + Assertions.assertEquals(FullName.class, vParser.getReturnTypes().get(0)); + Assertions.assertEquals(Department.class, vParser.getReturnTypes().get(1)); + } + + @Test + void whenJoinKeywordIsUsed_thenCreatesExplicitInnerJoin() throws JSQLParserException + { + JPQLParser vParser = new JPQLParser("select case when exists(select 1 from Employee where id=:id) then 1 else 0 end", new HashMap<>()); + + Assertions.assertEquals("SELECT CASE WHEN EXISTS (SELECT 1 \"c1\" FROM EMPLOYEE t1 WHERE t1.IRN = ?) THEN 1 ELSE 0 END \"c1\"", + vParser.getQuery()); + + vParser = new JPQLParser("select count(Employee) from Employee where department.company=:id", new HashMap<>()); + + Assertions.assertEquals("SELECT count(t1.IRN) \"c1\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN " + + "WHERE t3.IRN = ?", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Object.class, vParser.getReturnTypes().get(0)); + Assertions.assertEquals(1, vParser.getQueryParameters().size()); + Assertions.assertEquals(Object.class, vParser.getQueryParameters().get(0).getParameterType()); + Assertions.assertFalse(vParser.isSelectUsingPrimaryKey()); + } + + @Test + void whenWhereINIsUsed() throws JSQLParserException + { + JPQLParser vParser = new JPQLParser("SELECT e FROM Employee e where e.age in (11,22,33)", new HashMap<>()); + + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2-1\", t1.SURNAME \"c1-2-2\", t1.AGE \"c1-3\", " + + "t2.IRN \"c1-5-1\", t2.NAME \"c1-5-2\", " + + "t3.IRN \"c1-5-3-1\", t3.NAME \"c1-5-3-2\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN " + + "WHERE t1.AGE IN (11, 22, 33)", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee.class, vParser.getReturnTypes().get(0)); + + vParser = new JPQLParser("SELECT e FROM Employee e where e.age in (select e1.age from Employee1 e1)", new HashMap<>()); + + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2-1\", t1.SURNAME \"c1-2-2\", t1.AGE \"c1-3\", " + + "t2.IRN \"c1-5-1\", t2.NAME \"c1-5-2\", " + + "t3.IRN \"c1-5-3-1\", t3.NAME \"c1-5-3-2\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN " + + "WHERE t1.AGE IN (SELECT t4.AGE \"c1\" FROM EMPLOYEE t4)", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee.class, vParser.getReturnTypes().get(0)); + + } + + @Test + void whenIsNullIsUsed() throws JSQLParserException + { + JPQLParser vParser = new JPQLParser("SELECT e FROM Employee e where e.fullName.name is null", new HashMap<>()); + + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2-1\", t1.SURNAME \"c1-2-2\", t1.AGE \"c1-3\", " + + "t2.IRN \"c1-5-1\", t2.NAME \"c1-5-2\", " + + "t3.IRN \"c1-5-3-1\", t3.NAME \"c1-5-3-2\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN " + + "WHERE t1.NAME IS NULL", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee.class, vParser.getReturnTypes().get(0)); + + vParser = new JPQLParser("SELECT e FROM Employee e where e.fullName.name is not null", new HashMap<>()); + + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2-1\", t1.SURNAME \"c1-2-2\", t1.AGE \"c1-3\", " + + "t2.IRN \"c1-5-1\", t2.NAME \"c1-5-2\", " + + "t3.IRN \"c1-5-3-1\", t3.NAME \"c1-5-3-2\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN " + + "WHERE t1.NAME IS NOT NULL", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee.class, vParser.getReturnTypes().get(0)); + } + + @Test + void whenExistIsUsed_thenCreatesExplicitInnerJoin() throws JSQLParserException + { + JPQLParser vParser = new JPQLParser("SELECT d FROM Employee e JOIN e.department d", new HashMap<>()); + + Assertions.assertEquals("SELECT t2.IRN \"c1-1\", t2.NAME \"c1-2\", " + + "t3.IRN \"c1-3-1\", t3.NAME \"c1-3-2\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Department.class, vParser.getReturnTypes().get(0)); + } + + @Test + void whenLeftKeywordIsSpecified_thenCreatesOuterJoinAndIncludesNonMatched() throws JSQLParserException + { + JPQLParser vParser = new JPQLParser("SELECT DISTINCT d FROM Department d LEFT JOIN d.employees e", new HashMap<>()); + + Assertions.assertEquals("SELECT DISTINCT t1.IRN \"c1-1\", t1.NAME \"c1-2\", " + + "t3.IRN \"c1-3-1\", t3.NAME \"c1-3-2\" " + + "FROM DEPT t1 " + + "LEFT JOIN EMPLOYEE t2 ON t1.IRN = t2.DEPT " + + "INNER JOIN COMPANY t3 ON t1.COMP = t3.IRN", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Department.class, vParser.getReturnTypes().get(0)); + } + + @Test + void whenEntitiesAreListedInFrom_ThenCreatesCartesianProduct() throws JSQLParserException + { + //Mark the company field as lazy for this test + EntityMetaData vMetaData = EntityMetaDataManager.getMetaData(Department.class); + vMetaData.getEntityField("company").setFetchType(FetchType.LAZY); + + JPQLParser vParser = new JPQLParser("SELECT e, d FROM Employee e, Department d", new HashMap<>()); + vMetaData.getEntityField("company").setFetchType(FetchType.EAGER); + + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2-1\", t1.SURNAME \"c1-2-2\", t1.AGE \"c1-3\", " + + "t2.IRN \"c1-5-1\", t2.NAME \"c1-5-2\", " + + "t2.COMP \"c1-5-3\", " + + "t2.IRN \"c2-1\", t2.NAME \"c2-2\", t2.COMP \"c2-3\" " + + "FROM EMPLOYEE t1, DEPT t2", + vParser.getQuery()); + Assertions.assertEquals(2, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee.class, vParser.getReturnTypes().get(0)); + Assertions.assertEquals(Department.class, vParser.getReturnTypes().get(1)); + } + + @Test + void whenEntitiesAreListedInFromAndMatchedInWhere_ThenCreatesJoin() throws JSQLParserException + { + JPQLParser vParser = new JPQLParser("SELECT d FROM Employee e, Department d WHERE e.department = d", new HashMap<>()); + + Assertions.assertEquals("SELECT t2.IRN \"c1-1\", t2.NAME \"c1-2\", " + + "t3.IRN \"c1-3-1\", t3.NAME \"c1-3-2\" " + + "FROM EMPLOYEE t1, DEPT t2 " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN " + + "WHERE t1.DEPT = t2.IRN", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Department.class, vParser.getReturnTypes().get(0)); + } + + @Test + void whenEntitiesAreOrderedByFK() throws JSQLParserException + { + + JPQLParser vParser = new JPQLParser("SELECT Employee FROM Employee Order by department.name asc", new HashMap<>()); + + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2-1\", t1.SURNAME \"c1-2-2\", t1.AGE \"c1-3\", " + + "t2.IRN \"c1-5-1\", t2.NAME \"c1-5-2\", " + + "t3.IRN \"c1-5-3-1\", t3.NAME \"c1-5-3-2\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN " + + "ORDER BY t2.NAME ASC", vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee.class, vParser.getReturnTypes().get(0)); + } + + @Test + void whenCollectionValuedAssociationIsJoined_ThenCanSelect() throws JSQLParserException + { + JPQLParser vParser = new JPQLParser("SELECT e FROM Employee e JOIN e.phones ph WHERE ph LIKE '1%'", new HashMap<>()); + + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2-1\", t1.SURNAME \"c1-2-2\", t1.AGE \"c1-3\", " + + "t3.IRN \"c1-5-1\", t3.NAME \"c1-5-2\", " + + "t4.IRN \"c1-5-3-1\", t4.NAME \"c1-5-3-2\" " + + "FROM EMPLOYEE t1 " + + "JOIN PHONE t2 ON t1.IRN = t2.EMPL " + + "INNER JOIN DEPT t3 ON t1.DEPT = t3.IRN " + + "INNER JOIN COMPANY t4 ON t3.COMP = t4.IRN " + + "WHERE t2.IRN LIKE '1%'", + vParser.getQuery()); + + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee.class, vParser.getReturnTypes().get(0)); + } + + @Test + void whenMultipleEntitiesAreListedWithJoin_ThenCreatesMultipleJoins() throws JSQLParserException + { + JPQLParser vParser = new JPQLParser("SELECT ph FROM Employee e " + + "JOIN e.department d " + + "JOIN e.phones ph " + + "WHERE d.name IS NOT NULL", new HashMap<>()); + Assertions.assertEquals("SELECT t3.IRN \"c1-1\", t3.NUM \"c1-2\", " + + "t1.IRN \"c1-3-1\", t1.NAME \"c1-3-2-1\", t1.SURNAME \"c1-3-2-2\", t1.AGE \"c1-3-3\", " + + "t2.IRN \"c1-3-5-1\", t2.NAME \"c1-3-5-2\", " + + "t4.IRN \"c1-3-5-3-1\", t4.NAME \"c1-3-5-3-2\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "JOIN PHONE t3 ON t1.IRN = t3.EMPL " + + "INNER JOIN COMPANY t4 ON t2.COMP = t4.IRN " + + "WHERE t2.NAME IS NOT NULL", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Phone.class, vParser.getReturnTypes().get(0)); + } + + @Test + void whenGroupByOrderBy_ThenCreatesJoins() throws JSQLParserException + { + JPQLParser vParser = new JPQLParser("SELECT e.fullName.name, count(ph.id) FROM Employee e " + + "JOIN e.phones ph " + + "group by e.fullName.name " + + "order by e.fullName.name, ph", new HashMap<>()); + Assertions.assertEquals("SELECT t1.NAME \"c1\", count(t2.IRN) \"c2\" " + + "FROM EMPLOYEE t1 " + + "JOIN PHONE t2 ON t1.IRN = t2.EMPL " + + "GROUP BY t1.NAME " + + "ORDER BY t1.NAME, t2.IRN", + vParser.getQuery()); + Assertions.assertEquals(2, vParser.getReturnTypes().size()); + Assertions.assertEquals(String.class, vParser.getReturnTypes().get(0)); + Assertions.assertEquals(Object.class, vParser.getReturnTypes().get(1)); + } + + @Test + void whenGroupByHaving_ThenCreatesJoins() throws JSQLParserException + { + JPQLParser vParser = new JPQLParser("SELECT e.fullName.name, count(ph.id),:p1 FROM Employee e " + + "JOIN e.phones ph " + + "group by e.fullName.name " + + "having count(ph.id) > 1", new HashMap<>()); + Assertions.assertEquals("SELECT t1.NAME \"c1\", count(t2.IRN) \"c2\", ? \"c3\" " + + "FROM EMPLOYEE t1 " + + "JOIN PHONE t2 ON t1.IRN = t2.EMPL " + + "GROUP BY t1.NAME " + + "HAVING count(t2.IRN) > 1", + vParser.getQuery()); + assertDoesNotThrow(() -> vParser.checkType(Object[].class)); + assertThrows(IllegalArgumentException.class, () -> vParser.checkType(Employee.class)); + } + + @Test + void whenWhereByEntity_ThenCreateWhereOnPK() throws JSQLParserException + { + JPQLParser vParser = new JPQLParser("SELECT e FROM Employee e " + + "where e.fullName=:FullName", new HashMap<>()); + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2-1\", t1.SURNAME \"c1-2-2\", t1.AGE \"c1-3\", " + + "t2.IRN \"c1-5-1\", t2.NAME \"c1-5-2\", " + + "t3.IRN \"c1-5-3-1\", t3.NAME \"c1-5-3-2\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN " + + "WHERE (t1.NAME, t1.SURNAME) = (?, ?)", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee.class, vParser.getReturnTypes().get(0)); + Assertions.assertEquals(1, vParser.getQueryParameters().size()); + Assertions.assertEquals(FullName.class, vParser.getQueryParameters().get(0).getParameterType()); + + vParser = new JPQLParser("SELECT Employee FROM Employee " + + "where fullName=:FullName and age>:age", new HashMap<>()); + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2-1\", t1.SURNAME \"c1-2-2\", t1.AGE \"c1-3\", " + + "t2.IRN \"c1-5-1\", t2.NAME \"c1-5-2\", " + + "t3.IRN \"c1-5-3-1\", t3.NAME \"c1-5-3-2\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN " + + "WHERE (t1.NAME, t1.SURNAME) = (?, ?) " + + "AND t1.AGE > ?", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee.class, vParser.getReturnTypes().get(0)); + Assertions.assertEquals(2, vParser.getQueryParameters().size()); + Assertions.assertEquals(FullName.class, vParser.getQueryParameters().get(0).getParameterType()); + + vParser = new JPQLParser("SELECT e FROM Employee e where e=?1", new HashMap<>()); + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2-1\", t1.SURNAME \"c1-2-2\", t1.AGE \"c1-3\", " + + "t2.IRN \"c1-5-1\", t2.NAME \"c1-5-2\", " + + "t3.IRN \"c1-5-3-1\", t3.NAME \"c1-5-3-2\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN " + + "WHERE t1.IRN = ?", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee.class, vParser.getReturnTypes().get(0)); + Assertions.assertEquals(1, vParser.getQueryParameters().size()); + Assertions.assertEquals(Object.class, vParser.getQueryParameters().get(0).getParameterType()); + Assertions.assertTrue(vParser.isSelectUsingPrimaryKey()); + + vParser = new JPQLParser("SELECT e FROM Employee e where e.department.company=?1", new HashMap<>()); + Assertions.assertEquals("SELECT t1.IRN \"c1-1\", t1.NAME \"c1-2-1\", t1.SURNAME \"c1-2-2\", t1.AGE \"c1-3\", " + + "t2.IRN \"c1-5-1\", t2.NAME \"c1-5-2\", " + + "t3.IRN \"c1-5-3-1\", t3.NAME \"c1-5-3-2\" " + + "FROM EMPLOYEE t1 " + + "INNER JOIN DEPT t2 ON t1.DEPT = t2.IRN " + + "INNER JOIN COMPANY t3 ON t2.COMP = t3.IRN " + + "WHERE t3.IRN = ?", + vParser.getQuery()); + Assertions.assertEquals(1, vParser.getReturnTypes().size()); + Assertions.assertEquals(Employee.class, vParser.getReturnTypes().get(0)); + Assertions.assertEquals(1, vParser.getQueryParameters().size()); + Assertions.assertEquals(Object.class, vParser.getQueryParameters().get(0).getParameterType()); + Assertions.assertFalse(vParser.isSelectUsingPrimaryKey()); + } + + @Test + void deleteEntityUsingPK() throws JSQLParserException + { + JPQLParser vParser = new JPQLParser("DELETE Employee e where e.id=:id", new HashMap<>()); + Assertions.assertEquals("DELETE EMPLOYEE t1 WHERE t1.IRN = ?", + vParser.getQuery()); + + vParser = new JPQLParser("DELETE Employee where age>?", new HashMap<>()); + Assertions.assertEquals("DELETE EMPLOYEE t1 WHERE t1.AGE > ?", + vParser.getQuery()); + } + + @Test + void updateEntityUsingPK() throws JSQLParserException + { + JPQLParser vParser = new JPQLParser("UPDATE Employee e set e.salary=e.salary*1.05 " + + "where e.id=:id", new HashMap<>()); + Assertions.assertEquals("UPDATE EMPLOYEE t1 SET t1.SALARY = t1.SALARY * 1.05 WHERE t1.IRN = ?", + vParser.getQuery()); + + vParser = new JPQLParser("UPDATE Employee set salary=salary*1.05 " + + "where id=:id", new HashMap<>()); + Assertions.assertEquals("UPDATE EMPLOYEE t1 SET t1.SALARY = t1.SALARY * 1.05 WHERE t1.IRN = ?", + vParser.getQuery()); + + vParser = new JPQLParser("UPDATE Employee e set (e.salary,e.age)=(e.salary*1.05,e.age+1) " + + "where e.id=:id", new HashMap<>()); + Assertions.assertEquals("UPDATE EMPLOYEE t1 SET (t1.SALARY, t1.AGE) = (t1.SALARY * 1.05, t1.AGE + 1) WHERE t1.IRN = ?", + vParser.getQuery()); + + vParser = new JPQLParser("UPDATE Employee set (salary,age)=(salary*1.05,age+1) " + + "where id=:id", new HashMap<>()); + Assertions.assertEquals("UPDATE EMPLOYEE t1 SET (t1.SALARY, t1.AGE) = (t1.SALARY * 1.05, t1.AGE + 1) WHERE t1.IRN = ?", + vParser.getQuery()); + } + + @Test + void whenUsingNamedParameters_thenCheckIfNamesReused() + { + JPQLParser vParser = new JPQLParser("select Employee from Employee " + + "where id = :num " + + "and age= :num", + new HashMap<>()); + assertEquals(2, vParser.getNumberOfParameters()); + } +} diff --git a/jpalite-core/src/test/java/io/jpalite/jqpl/Phone.java b/jpalite-core/src/test/java/io/jpalite/jqpl/Phone.java new file mode 100644 index 0000000..09c5280 --- /dev/null +++ b/jpalite-core/src/test/java/io/jpalite/jqpl/Phone.java @@ -0,0 +1,46 @@ +/* + * 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.jqpl; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Data; +import io.jpalite.impl.JPAEntityImpl; + +@Entity +@Table(name = "PHONE") +@Data +public class Phone extends JPAEntityImpl +{ + @Id + @GeneratedValue + @Column(name = "IRN") + private int id; + + @Column(name = "NUM") + private String number; + + @ManyToOne + @JoinColumn(name = "EMPL") + private Employee employee; +}//Phone diff --git a/jpalite-core/src/test/java/io/jpalite/jqpl/RatePlan.java b/jpalite-core/src/test/java/io/jpalite/jqpl/RatePlan.java new file mode 100644 index 0000000..ca67845 --- /dev/null +++ b/jpalite-core/src/test/java/io/jpalite/jqpl/RatePlan.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.jqpl; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; +import lombok.Data; +import io.jpalite.impl.JPAEntityImpl; + +import java.sql.Timestamp; + +@Data +@Entity +@Table(name = "RATE_PLAN") +public class RatePlan extends JPAEntityImpl +{ + @Id + @GeneratedValue + @Column(name = "ID", updatable = false) + Long id; + + @Column(name = "UID", nullable = false) + String uid; + + @Column(name = "RESOURCE_VERSION", nullable = false) + long resourceVersion = 0; + + @Column(name = "OPERATOR_ID", nullable = false) + long operatorId; + + @Column(name = "PLAN_NAME") + String name; + + @Column(name = "CREATED_BY", nullable = false) + String createdBy; + + @Column(name = "APPROVED_BY") + String approvedBy; + + @Column(name = "EFFECTIVE_DATE", nullable = false) + Timestamp effectiveDate; + + @Column(name = "RATE_PLAN_CONFIG", nullable = false) + String ratePlanConfig; + + @Version + @Column(name = "MODIFIED_ON", nullable = false) + Timestamp modifiedOn; + + @Column(name = "CREATED_DATE", nullable = false) + Timestamp createdDate; +} diff --git a/jpalite-core/src/test/resources/META-INF/services/io.jpalite.MultiTenant b/jpalite-core/src/test/resources/META-INF/services/io.jpalite.MultiTenant new file mode 100644 index 0000000..2900c83 --- /dev/null +++ b/jpalite-core/src/test/resources/META-INF/services/io.jpalite.MultiTenant @@ -0,0 +1,16 @@ +# +# 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. +# diff --git a/jpalite-maven-plugin/jpalite-maven-plugin.iml b/jpalite-maven-plugin/jpalite-maven-plugin.iml new file mode 100644 index 0000000..ffa23e7 --- /dev/null +++ b/jpalite-maven-plugin/jpalite-maven-plugin.iml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/jpalite-maven-plugin/pom.xml b/jpalite-maven-plugin/pom.xml new file mode 100644 index 0000000..d73c0ee --- /dev/null +++ b/jpalite-maven-plugin/pom.xml @@ -0,0 +1,143 @@ + + + + + + io.jpalite + jpalite-parent + 3.0.0 + ../pom.xml + + + 4.0.0 + jpalite-maven-plugin + maven-plugin + JPALite Tooling + + + 21 + 21 + UTF-8 + + + + org.apache.maven + maven-plugin-api + 3.9.6 + provided + + + org.apache.maven.plugin-tools + maven-plugin-annotations + 3.11.0 + provided + + + org.apache.maven + maven-core + 3.9.6 + provided + + + jakarta.persistence + jakarta.persistence-api + + + org.javassist + javassist + 3.30.2-GA + + + org.infinispan.protostream + protostream-processor + + + junit + junit + 3.8.1 + test + + + io.jpalite + jpalite-core + ${project.version} + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-ext + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + 3.11.0 + + + + report + + + + + + + + + + + + org.apache.maven.plugins + maven-plugin-plugin + 3.11.0 + + tsjpa + + + + default-descriptor + + descriptor + + process-classes + + + help-goal + + helpmojo + + + + + + + org.apache.maven.plugins + maven-site-plugin + 3.12.1 + + + + + diff --git a/jpalite-maven-plugin/src/main/java/io/jpalite/JPALiteMojo.java b/jpalite-maven-plugin/src/main/java/io/jpalite/JPALiteMojo.java new file mode 100644 index 0000000..eda2938 --- /dev/null +++ b/jpalite-maven-plugin/src/main/java/io/jpalite/JPALiteMojo.java @@ -0,0 +1,164 @@ +/* + * 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.JPALiteToolingImpl; +import org.apache.maven.artifact.DependencyResolutionRequiredException; +import org.apache.maven.plugin.AbstractMojo; +import org.apache.maven.plugin.MojoExecutionException; +import org.apache.maven.plugins.annotations.LifecyclePhase; +import org.apache.maven.plugins.annotations.Mojo; +import org.apache.maven.plugins.annotations.Parameter; +import org.apache.maven.plugins.annotations.ResolutionScope; +import org.apache.maven.project.MavenProject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Goal which touches a timestamp file. + * + * @goal1 jpalite + * @phase1 process-sources + *

+ * {@code ... + * + * false + * + * ... + */ +@Mojo(name = "jpalite", defaultPhase = LifecyclePhase.PROCESS_TEST_CLASSES, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, threadSafe = true) +public class JPALiteMojo extends AbstractMojo +{ + private static final String LINE_SEPARATOR = "------------------------------------------------------------------------"; + private static final Logger LOG = LoggerFactory.getLogger(JPALiteMojo.class); + + @Parameter(defaultValue = "${project}", property = "tsjpa.project", required = true, readonly = true) + private MavenProject project; + + @Parameter(defaultValue = "false", property = "tsjpa.skip", required = false) + private boolean skip; + + private List getClassList(String pDir) throws IOException + { + List vClassList = new ArrayList<>(); + Path vInput = Paths.get(pDir); + if (vInput.toFile().exists()) { + Files.walkFileTree(vInput, new SimpleFileVisitor<>() + { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException + { + if (Files.isRegularFile(file)) { + String vFilename = file.getFileName().toString(); + if (vFilename.endsWith(".class")) { + String vPath = file.getParent().toString(); + if (vPath.equals(pDir)) { + vPath = ""; + }//if + else { + vPath = vPath.substring(pDir.length() + 1).replace("/", ".") + "."; + }//else + + String vNameSpace = vPath + vFilename.substring(0, vFilename.length() - 6); + vClassList.add(vNameSpace); + }//if + }//if + return FileVisitResult.CONTINUE; + } + }); + }//if + + return vClassList; + } + + @SuppressWarnings("java:S2112") //We need URLs here + public void execute() throws MojoExecutionException + { + if (skip) { + getLog().info("Skipping executing."); + return; + }//if + + URLClassLoader contextClassLoader = null; + ClassLoader currentLoader = Thread.currentThread().getContextClassLoader(); + try { + //Setup the classpath for the plugin + Set urls = new HashSet<>(); + for (String resource : project.getRuntimeClasspathElements()) { + urls.add(new File(resource).toURI().toURL()); + }//for + + urls.add(new File(project.getBuild().getTestOutputDirectory()).toURI().toURL()); + + contextClassLoader = URLClassLoader.newInstance( + urls.toArray(new URL[0]), + Thread.currentThread().getContextClassLoader()); + + Thread.currentThread().setContextClassLoader(contextClassLoader); + + JPALiteTooling tooling = new JPALiteToolingImpl(); + + //Process runtime classes + LOG.info(LINE_SEPARATOR); + List list = getClassList(project.getBuild().getOutputDirectory()); + LOG.info("JPA Tooling found {} runtime classes", list.size()); + LOG.info(LINE_SEPARATOR); + tooling.process(project.getBuild().getOutputDirectory(), list); + + //Process test classes + LOG.info(LINE_SEPARATOR); + list = getClassList(project.getBuild().getTestOutputDirectory()); + LOG.info("JPA Tooling found {} test classes", list.size()); + LOG.info(LINE_SEPARATOR); + tooling.process(project.getBuild().getTestOutputDirectory(), list); + }//try + catch (IOException ex) { + throw new MojoExecutionException("Error build class list ", ex); + }//catch + catch (JPALiteToolingException ex) { + LOG.error("Error executing JPALite Tooling class", ex); + throw new MojoExecutionException("Error executing JPALite Tooling class"); + } + catch (DependencyResolutionRequiredException ex) { + LOG.error("Error loading JPALite Tooling class", ex); + throw new MojoExecutionException("Error loading JPALite Tooling class"); + } + finally { + Thread.currentThread().setContextClassLoader(currentLoader); + if (contextClassLoader != null) { + try { + contextClassLoader.close(); + }//try + catch (IOException ex) { + //ignore + }//catch + }//if + }//finally + }//execute +}//TradeSwitchJPAMojo diff --git a/jpalite-maven-plugin/src/main/java/io/jpalite/JPALiteTooling.java b/jpalite-maven-plugin/src/main/java/io/jpalite/JPALiteTooling.java new file mode 100644 index 0000000..540797b --- /dev/null +++ b/jpalite-maven-plugin/src/main/java/io/jpalite/JPALiteTooling.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; + +import java.util.List; + +public interface JPALiteTooling +{ + /** + * The entry point for the JPA tooling process. The method is called with a list of classes to process and + */ + void process(String outputDir, List classList) throws JPALiteToolingException; +}//JPALiteTooling diff --git a/jpalite-maven-plugin/src/main/java/io/jpalite/JPALiteToolingException.java b/jpalite-maven-plugin/src/main/java/io/jpalite/JPALiteToolingException.java new file mode 100644 index 0000000..42e8a96 --- /dev/null +++ b/jpalite-maven-plugin/src/main/java/io/jpalite/JPALiteToolingException.java @@ -0,0 +1,40 @@ +/* + * 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 class JPALiteToolingException extends Exception +{ + public JPALiteToolingException() + { + } + + public JPALiteToolingException(String message) + { + super(message); + } + + public JPALiteToolingException(String message, Throwable cause) + { + super(message, cause); + } + + public JPALiteToolingException(Throwable cause) + { + super(cause); + } +}//JPAToolingException diff --git a/jpalite-maven-plugin/src/main/java/io/jpalite/impl/JPALiteToolingImpl.java b/jpalite-maven-plugin/src/main/java/io/jpalite/impl/JPALiteToolingImpl.java new file mode 100644 index 0000000..b7d6959 --- /dev/null +++ b/jpalite-maven-plugin/src/main/java/io/jpalite/impl/JPALiteToolingImpl.java @@ -0,0 +1,310 @@ +/* + * 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.JPALiteTooling; +import io.jpalite.JPALiteToolingException; +import jakarta.persistence.*; +import javassist.*; +import org.infinispan.protostream.GeneratedSchema; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import static java.lang.reflect.Modifier.*; + +public class JPALiteToolingImpl implements JPALiteTooling +{ + private static final Logger LOG = LoggerFactory.getLogger(JPALiteToolingImpl.class); + private static final String CHECK_FIELD_NAME = "JPALITE_TOOLING"; + public static final String ERROR_PROCESSING_FILE = "Error processing file"; + + private final Map entityClasses = new TreeMap<>(); + private final List converterClasses = new ArrayList<>(); + private final List protoStreamClasses = new ArrayList<>(); + private String outputDir; + private final ClassPool classPool; + + public JPALiteToolingImpl() + { + outputDir = "."; + classPool = ClassPool.getDefault(); + }//JPATooling + + private void applyLazyFetch(CtClass entityClass, CtField field) throws CannotCompileException, NotFoundException + { + //Id fields cannot be fetched lazily + if (!field.hasAnnotation(Id.class) && field.hasAnnotation(Column.class)) { + String fieldName = field.getName(); + String type = field.getType().getName(); + String vGetterMethod = ((type.equals(Boolean.class.getName()) || type.equals(boolean.class.getName()) ? "is" : "get") + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1)); + try { + CtMethod getter = entityClass.getDeclaredMethod(vGetterMethod); + getter.insertBefore("{_lazyFetch(\"" + field.getName() + "\");}"); + }//try + catch (NotFoundException ex) { + LOG.debug("Field {}::{} have no getting method - Lazy fetching tooling not applied.", entityClass.getName(), field.getName()); + }//catch + }//if + }//applyLazyFetch + + private void applyChangeTracker(CtClass entityClass, CtField field) throws CannotCompileException, NotFoundException + { + String fieldName = field.getName(); + String vSetterMethod = "set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1); + + try { + CtMethod setter = entityClass.getDeclaredMethod(vSetterMethod, new CtClass[]{field.getType()}); + setter.insertBefore("{_markField(\"" + field.getName() + "\");}"); + }//try + catch (NotFoundException ex) { + LOG.warn("Field {}::{} have no setter method - Change tracking not applied.", entityClass.getName(), field.getName()); + }//catch + }//applyChangeTracker + + private void applyToString(CtClass entityClass) throws CannotCompileException + { + try { + CtMethod setter = entityClass.getDeclaredMethod("toString", new CtClass[]{}); + setter.insertBefore("{return super.toString();}"); + }//try + catch (NotFoundException ex) { + CtMethod toString = CtMethod.make("public String toString(){return super.toString();}", entityClass); + entityClass.addMethod(toString); + }//catch + }//applyToString + + private void applyEquals(CtClass pEntityClass) throws CannotCompileException + { + try { + CtMethod method = pEntityClass.getDeclaredMethod("equals"); + pEntityClass.removeMethod(method); + }//try + catch (NotFoundException ex) { + //Ignore + }//catch + + CtMethod equals = CtMethod.make("public boolean equals(Object o){" + + "return super.equals(o);" + + "}", pEntityClass); + pEntityClass.addMethod(equals); + }//applyEquals + + private void applyTooling(CtClass entityClass) throws JPALiteToolingException + { + try { + LOG.debug("Applying JPA Tooling to {}", entityClass.getName()); + Class jpaEntityClass = Thread.currentThread().getContextClassLoader().loadClass("io.jpalite.impl.JPAEntityImpl"); + CtClass jpaEntityImpl = classPool.get(jpaEntityClass.getName()); + + entityClass.setSuperclass(jpaEntityImpl); + if (entityClass.getAnnotation(Cacheable.class) != null) { + CtClass serializationContext = classPool.get(GeneratedSchema.class.getName()); + entityClass.addInterface(serializationContext); + protoStreamClasses.add(entityClass.getName()); + }//if + for (CtField field : entityClass.getDeclaredFields()) { + if (!isStatic(field.getModifiers()) && !isFinal(field.getModifiers()) && !isTransient(field.getModifiers()) && field.getAnnotation(Transient.class) == null) { + applyChangeTracker(entityClass, field); + applyLazyFetch(entityClass, field); + applyToString(entityClass); + applyEquals(entityClass); + }//if + }//for + }//try + catch (CannotCompileException ex) { + throw new JPALiteToolingException("Compiler error " + entityClass.getName()); + } + catch (ClassNotFoundException | NotFoundException ex) { + LOG.error("Error transforming {}", entityClass.getName(), ex); + throw new JPALiteToolingException("Error check annotation " + entityClass.getName()); + } + }//applyTooling + + private void toolClass(CtClass ctClass) throws JPALiteToolingException, CannotCompileException, IOException + { + try { + ctClass.getDeclaredField(CHECK_FIELD_NAME); + LOG.debug("JPA Tooling already applied to {} - skipping class", ctClass.getSimpleName()); + }//try + catch (NotFoundException ex) { + applyTooling(ctClass); + + CtField checkField = CtField.make("private static final boolean " + CHECK_FIELD_NAME + " = true;", ctClass); + ctClass.addField(checkField); + + ctClass.writeFile(outputDir); + }//else + }//toolClass + + private void processConvertClass(CtClass ctClass) throws JPALiteToolingException + { + try { + Converter converter = (Converter) ctClass.getAnnotation(Converter.class); + if (converter != null) { + converterClasses.add(ctClass); + }//if + }//try + catch (ClassNotFoundException ex) { + throw new JPALiteToolingException(ERROR_PROCESSING_FILE, ex); + }//catch + }//processClass + + private void processClass(CtClass ctClass) throws JPALiteToolingException + { + try { + Entity entity = (Entity) ctClass.getAnnotation(Entity.class); + if (entity != null || ctClass.getAnnotation(Table.class) != null || ctClass.getAnnotation(Embeddable.class) != null) { + + String entityName = ctClass.getSimpleName(); + if (entity != null && !entity.name().isEmpty()) { + entityName = entity.name(); + }//if + + String entityClass = entityClasses.get(entityName); + if (entityClass != null) { + throw new JPALiteToolingException("Entity [" + entityName + "] name found in [" + ctClass.getName() + "] is already defined in [" + entityClass + "]"); + }//if + entityClasses.put(entityName, ctClass.getName()); + + toolClass(ctClass); + }//if + + }//try + catch (ClassNotFoundException | IOException | CannotCompileException ex) { + throw new JPALiteToolingException(ERROR_PROCESSING_FILE, ex); + }//catch + }//processClass + + @SuppressWarnings("java:S6126") //The IDE insert tabs and spaces into a text block resulting in a compile error + @Override + public void process(String outputDir, List classList) throws JPALiteToolingException + { + String vAttrs = "\",\n" + + "\"allDeclaredConstructors\": true,\n" + + "\"allPublicConstructors\": true,\n" + + "\"allDeclaredMethods\": true,\n" + + "\"allPublicMethods\": true,\n" + + "\"allDeclaredFields\": true,\n" + + "\"allPublicFields\": true }\n"; + + this.outputDir = outputDir; + try { + entityClasses.clear(); + converterClasses.clear(); + + classPool.insertClassPath(outputDir); + classPool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader())); + classPool.appendSystemPath(); + + Files.createDirectories(Path.of(outputDir + "/META-INF/services")); + Files.createDirectories(Path.of(outputDir + "/META-INF/native-image/org.tradeswitch.persistent")); + + //Build a list of all the converter classes first + for (String className : classList) { + CtClass ctClass = classPool.get(className); + processConvertClass(ctClass); + }//for + + //Process the entity classes + for (String className : classList) { + CtClass ctClass = classPool.get(className); + processClass(ctClass); + }//for + + try (FileOutputStream outputStream = new FileOutputStream(outputDir + "/META-INF/persistenceUnits.properties"); + FileOutputStream nativeImageStream = new FileOutputStream(outputDir + "/META-INF/native-image/org.tradeswitch.persistent/reflect-config.json")) { + nativeImageStream.write("[\n".getBytes()); + boolean first = true; + for (Map.Entry entry : entityClasses.entrySet()) { + outputStream.write(entry.getKey().getBytes()); + outputStream.write('='); + outputStream.write(entry.getValue().getBytes()); + outputStream.write('\n'); + + if (first) { + first = false; + } + else { + nativeImageStream.write(','); + }//else + + nativeImageStream.write("{\n\"name\": \"".getBytes()); + nativeImageStream.write(entry.getValue().getBytes()); + nativeImageStream.write(vAttrs.getBytes()); + + } + + try (FileOutputStream converterStream = new FileOutputStream(outputDir + "/META-INF/org.tradeswitch.converters")) { + for (CtClass convertClass : converterClasses) { + + try { + converterStream.write(convertClass.getName().getBytes()); + converterStream.write('\n'); + + if (first) { + first = false; + } + else { + nativeImageStream.write(','); + }//else + + nativeImageStream.write("{\n\"name\": \"".getBytes()); + nativeImageStream.write(convertClass.getName().getBytes()); + nativeImageStream.write(vAttrs.getBytes()); + + CtMethod convertToEntityAttribute = convertClass.getDeclaredMethod("convertToEntityAttribute"); + if (convertToEntityAttribute != null) { + nativeImageStream.write(",{\n\"name\": \"".getBytes()); + nativeImageStream.write(convertToEntityAttribute.getReturnType().getName().getBytes()); + nativeImageStream.write(vAttrs.getBytes()); + } + } + catch (Exception ex) { + LOG.error("Converter {} not correctly implemented", convertClass.getName()); + } + }//for + }//try + + nativeImageStream.write(']'); + }//try + + try (FileOutputStream outputStream = new FileOutputStream(outputDir + "/META-INF/services/org.infinispan.protostream.SerializationContextInitializer", true)) { + for (String className : protoStreamClasses) { + outputStream.write(className.getBytes()); + outputStream.write('\n'); + }//for + }//try + }//try + catch (NotFoundException ex) { + throw new JPALiteToolingException(ERROR_PROCESSING_FILE, ex); + } + catch (IOException ex) { + throw new JPALiteToolingException("IO Error creating persistenceUnits file", ex); + }//catch + }//process + +}//JPALiteToolingImpl diff --git a/jpalite-quarkus-extension/deployment/pom.xml b/jpalite-quarkus-extension/deployment/pom.xml new file mode 100644 index 0000000..4c26882 --- /dev/null +++ b/jpalite-quarkus-extension/deployment/pom.xml @@ -0,0 +1,79 @@ + + + + + 4.0.0 + + io.jpalite + jpalite-extension-parent + 3.0.0 + ../pom.xml + + jpalite-extension-deployment + JPALite Extension - Deployment + + + io.quarkus + quarkus-junit5-internal + test + + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-arc-deployment + + + io.quarkus + quarkus-narayana-jta-deployment + + + io.quarkus + quarkus-mutiny-deployment + + + io.quarkus + quarkus-smallrye-context-propagation-deployment + + + io.jpalite + jpalite-extension + ${project.version} + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.platform.version} + + + + + + + diff --git a/jpalite-quarkus-extension/deployment/src/main/java/io/jpalite/extension/deployment/JPALiteExtensionProcessor.java b/jpalite-quarkus-extension/deployment/src/main/java/io/jpalite/extension/deployment/JPALiteExtensionProcessor.java new file mode 100644 index 0000000..3c054e2 --- /dev/null +++ b/jpalite-quarkus-extension/deployment/src/main/java/io/jpalite/extension/deployment/JPALiteExtensionProcessor.java @@ -0,0 +1,52 @@ +/* + * 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.extension.deployment; + +import io.jpalite.agroal.AgroalDataSourceProvider; +import io.jpalite.extension.PropertyPersistenceUnitProvider; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; +import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; + +class JPALiteExtensionProcessor +{ + + private static final String FEATURE = "jpalite-extension"; + + @BuildStep + FeatureBuildItem feature() + { + return new FeatureBuildItem(FEATURE); + } + + @BuildStep + NativeImageResourceBuildItem nativeImageResourceBuildItem() + { + return new NativeImageResourceBuildItem("META-INF/services/io.jpalite.DataSourceProvider", + "META-INF/services/io.jpalite.PersistenceUnitProvider"); + } + + @BuildStep + ReflectiveClassBuildItem reflection() + { + return ReflectiveClassBuildItem.builder(AgroalDataSourceProvider.class, + PropertyPersistenceUnitProvider.class) + .build(); + } +} diff --git a/jpalite-quarkus-extension/pom.xml b/jpalite-quarkus-extension/pom.xml new file mode 100644 index 0000000..8f20a9d --- /dev/null +++ b/jpalite-quarkus-extension/pom.xml @@ -0,0 +1,81 @@ + + + + + 4.0.0 + + io.jpalite + jpalite-parent + 3.0.0 + ../pom.xml + + + jpalite-extension-parent + pom + JPALite Extension - Parent + + deployment + runtime + + + + + + + io.quarkus + quarkus-maven-plugin + ${quarkus.platform.version} + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + org.jboss.logmanager.LogManager + ${maven.home} + ${settings.localRepository} + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${maven-failsafe-plugin.version} + + + org.jboss.logmanager.LogManager + ${maven.home} + ${settings.localRepository} + + + + + maven-compiler-plugin + ${maven-compiler-plugin.version} + + + -parameters + + + + + + + diff --git a/jpalite-quarkus-extension/runtime/pom.xml b/jpalite-quarkus-extension/runtime/pom.xml new file mode 100644 index 0000000..6333ec2 --- /dev/null +++ b/jpalite-quarkus-extension/runtime/pom.xml @@ -0,0 +1,104 @@ + + + + + 4.0.0 + + io.jpalite + jpalite-extension-parent + 3.0.0 + ../pom.xml + + jpalite-extension + JPALite Extension - Runtime + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-narayana-jta + + + io.agroal + agroal-pool + + + org.projectlombok + lombok + + + io.jpalite + jpalite-core + ${project.version} + + + + + + io.quarkus + quarkus-extension-maven-plugin + ${quarkus.platform.version} + + + compile + + extension-descriptor + + + ${project.groupId}:${project.artifactId}-deployment:${project.version} + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.platform.version} + + + org.projectlombok + lombok + ${lombok.version} + + + + + + io.smallrye + jandex-maven-plugin + ${maven-jandex-plugin.version} + + + make-index + + jandex + + + + + + + diff --git a/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/DatabaseCleanup.java b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/DatabaseCleanup.java new file mode 100644 index 0000000..af3759a --- /dev/null +++ b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/DatabaseCleanup.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.interceptor.InterceptorBinding; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@InterceptorBinding +@Target({METHOD, TYPE}) +@Retention(RUNTIME) +public @interface DatabaseCleanup +{ +} diff --git a/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/DatabaseCleanupHandler.java b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/DatabaseCleanupHandler.java new file mode 100644 index 0000000..5cbffdf --- /dev/null +++ b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/DatabaseCleanupHandler.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 io.jpalite.impl.db.DatabasePoolFactory; +import jakarta.annotation.Priority; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InvocationContext; +import lombok.extern.slf4j.Slf4j; + +@DatabaseCleanup +@Interceptor +@Priority(Interceptor.Priority.LIBRARY_AFTER) +@Slf4j +public class DatabaseCleanupHandler +{ + @AroundInvoke + public Object cleanupDatabase(InvocationContext context) throws Exception + { + try { + return context.proceed(); + } + finally { + LOG.info("***************************************************Cleaning up database pool!"); + DatabasePoolFactory.cleanup(); + } + } +} diff --git a/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/PersistenceUnit.java b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/PersistenceUnit.java new file mode 100644 index 0000000..8497cf4 --- /dev/null +++ b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/PersistenceUnit.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.enterprise.util.AnnotationLiteral; +import jakarta.inject.Qualifier; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.Objects; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({TYPE, FIELD, METHOD, PARAMETER, PACKAGE}) +@Retention(RUNTIME) +@Documented +@Qualifier +public @interface PersistenceUnit +{ + String value() default ""; + + + class PersistenceUnitLiteral extends AnnotationLiteral implements PersistenceUnit + { + + private final String name; + + public PersistenceUnitLiteral(String name) + { + this.name = name; + } + + @Override + public String value() + { + return name; + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (!(o instanceof PersistenceUnitLiteral that)) return false; + if (!super.equals(o)) return false; + + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() + { + int result = super.hashCode(); + result = 31 * result + (name != null ? name.hashCode() : 0); + return result; + } + } +} diff --git a/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/agroal/AgroalDataSourceProvider.java b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/agroal/AgroalDataSourceProvider.java new file mode 100644 index 0000000..e81208f --- /dev/null +++ b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/agroal/AgroalDataSourceProvider.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.agroal; + + +import io.jpalite.DataSourceProvider; +import io.quarkus.arc.Arc; + +import javax.sql.DataSource; + +public class AgroalDataSourceProvider implements DataSourceProvider +{ + + @Override + public DataSource getDataSource(String dataSourceName) + { + return Arc.container().instance(AgroalDataSources.class).get() + .getDataSource(dataSourceName); + }//getDataSource +}//AgroalDataSourceProvider diff --git a/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/agroal/AgroalDataSources.java b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/agroal/AgroalDataSources.java new file mode 100644 index 0000000..40485f8 --- /dev/null +++ b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/agroal/AgroalDataSources.java @@ -0,0 +1,130 @@ +/* + * 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.agroal; + +import io.agroal.api.AgroalDataSource; +import io.agroal.api.configuration.AgroalConnectionPoolConfiguration; +import io.agroal.api.configuration.AgroalDataSourceConfiguration; +import io.agroal.api.configuration.supplier.AgroalConnectionFactoryConfigurationSupplier; +import io.agroal.api.configuration.supplier.AgroalConnectionPoolConfigurationSupplier; +import io.agroal.api.configuration.supplier.AgroalDataSourceConfigurationSupplier; +import io.agroal.api.security.NamePrincipal; +import io.agroal.api.security.SimplePassword; +import io.agroal.pool.DataSource; +import io.jpalite.agroal.configuration.DataSourceConfigMapping; +import io.quarkus.arc.Unremovable; +import io.smallrye.config.SmallRyeConfig; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.persistence.PersistenceException; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.config.Config; + +import java.sql.Statement; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Singleton +@Unremovable +@Slf4j +public class AgroalDataSources +{ + private final Map dataSources = new ConcurrentHashMap<>(); + + @Inject + Config configProvider; + + public AgroalDataSource getDataSource(String dataSourceName) + { + return dataSources.computeIfAbsent(dataSourceName, this::loadDataSource); + }//getDataSource + + private AgroalDataSource loadDataSource(String dataSourceName) + { + SmallRyeConfig config = configProvider.unwrap(SmallRyeConfig.class); + + DataSourceConfigMapping dataSourceConfigMapping = config.getConfigMapping(DataSourceConfigMapping.class); + + DataSourceConfigMapping.DataSourceConfig dataSourceConfig = dataSourceConfigMapping.getDataSourceConfig(dataSourceName); + if (dataSourceConfig == null) { + throw new PersistenceException("DataSource '" + dataSourceName + "' not found"); + }//if + + if (dataSourceConfig.jdbc().url().isEmpty()) { + throw new PersistenceException("The jdbc.url property is missing for DataSource '" + dataSourceName + "'"); + }//if + + LOG.info("Creating Agroal data source ({}) for url {}", dataSourceName, dataSourceConfig.jdbc().url().get()); + AgroalConnectionFactoryConfigurationSupplier connectionConfiguration = new AgroalConnectionFactoryConfigurationSupplier() + .jdbcUrl(dataSourceConfig.jdbc().url().get()) + .connectionProviderClassName(dataSourceConfig.jdbc().driver()) + .autoCommit(true) + .jdbcTransactionIsolation(dataSourceConfig.jdbc().transactionIsolationLevel()); + + + dataSourceConfig.jdbc().jdbcProperties().forEach((k, v) -> connectionConfiguration.jdbcProperty(k, v)); + if (dataSourceConfig.jdbc().currentSchema().isPresent()) { + connectionConfiguration.jdbcProperty("currentSchema", dataSourceConfig.jdbc().currentSchema().get()); + }//if + + if (dataSourceConfig.username().isPresent()) { + NamePrincipal username = new NamePrincipal(dataSourceConfig.username().get()); + connectionConfiguration.principal(username).recoveryPrincipal(username); + }//if + if (dataSourceConfig.password().isPresent()) { + SimplePassword password = new SimplePassword(dataSourceConfig.password().get()); + connectionConfiguration.credential(password).credential(password); + }//if + + AgroalConnectionPoolConfigurationSupplier poolConfiguration = new AgroalConnectionPoolConfigurationSupplier() + .minSize(dataSourceConfig.jdbc().minSize()) + .maxSize(dataSourceConfig.jdbc().maxSize()) + .initialSize(dataSourceConfig.jdbc().initialSize()) + .connectionValidator(selectValidator(dataSourceConfig.jdbc().validationQuery())) + .acquisitionTimeout(dataSourceConfig.jdbc().acquisitionTimeout()) + .leakTimeout(dataSourceConfig.jdbc().leakDetectionInterval()) + .validationTimeout(dataSourceConfig.jdbc().validationInterval()) + .reapTimeout(dataSourceConfig.jdbc().idleRemovalInterval()) + .maxLifetime(dataSourceConfig.jdbc().maxLifetime()) + .enhancedLeakReport(dataSourceConfig.jdbc().leakReport()) + .connectionFactoryConfiguration(connectionConfiguration); + + + AgroalDataSourceConfigurationSupplier agroalConfig = new AgroalDataSourceConfigurationSupplier() + .dataSourceImplementation(AgroalDataSourceConfiguration.DataSourceImplementation.AGROAL) + .metricsEnabled(dataSourceConfig.jdbc().enableMetrics()) + .connectionPoolConfiguration(poolConfiguration); + + LOG.debug("Creating agroal pool {} with URI {}", dataSourceName, dataSourceConfig.jdbc().url()); + return new DataSource(agroalConfig.get()); + }//loadDataSource + + private AgroalConnectionPoolConfiguration.ConnectionValidator selectValidator(final String validationQuery) + { + return connection -> + { + try (Statement check = connection.createStatement()) { + check.execute(validationQuery); + return connection.isValid(0); + } + catch (Exception t) { + return false; + } + }; + }//SelectValidator +}//AgroalDataSources diff --git a/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/agroal/configuration/DataSourceConfigMapping.java b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/agroal/configuration/DataSourceConfigMapping.java new file mode 100644 index 0000000..a08e989 --- /dev/null +++ b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/agroal/configuration/DataSourceConfigMapping.java @@ -0,0 +1,200 @@ +/* + * 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.agroal.configuration; + +import io.agroal.api.configuration.AgroalConnectionFactoryConfiguration; +import io.quarkus.runtime.annotations.ConfigDocMapKey; +import io.quarkus.runtime.annotations.ConfigDocSection; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.*; + +import java.time.Duration; +import java.util.Map; +import java.util.Optional; + +@ConfigMapping(prefix = "jpalite.datasource") +@ConfigRoot(phase = ConfigPhase.RUN_TIME) +public interface DataSourceConfigMapping +{ + /** + * The default data source + */ + @WithParentName + DataSourceConfig defaultDataSource(); + + /** + * The named data sources + */ + @ConfigDocSection + @ConfigDocMapKey("datasource-name") + @WithParentName + Map namedDataSource(); + + interface DataSourceConfig + { + /** + * SThe kind of database we will connect to (e.g. h2, postgresql) + */ + @WithDefault("postgresql") + @WithName("db-kind") + String dbKind(); + + /** + * The datasource username + */ + Optional username(); + + /** + * The datasource password + */ + Optional password(); + + /** + * The JDBC Settings for the data source + * + * @return + */ + JDBCConfig jdbc(); + }//DataSourceConfig + + interface JDBCConfig + { + /** + * The datasource URL + */ + Optional url(); + + /** + * The datasource driver class name + */ + @WithDefault("org.postgresql.Driver") + String driver(); + + /** + * The transaction isolation level to use + */ + @WithName("transaction-isolation-level") + @WithDefault("READ_COMMITTED") + @WithConverter(TransactionIsolationConverter.class) + AgroalConnectionFactoryConfiguration.TransactionIsolation transactionIsolationLevel(); + + /** + * The database schema to use (if supported by the database kind) + */ + @WithName("currentSchema") + Optional currentSchema(); + + /** + * Enable datasource metrics collection. If unspecified, collecting metrics will be enabled by default if a metrics extension is active. + */ + @WithName("enable-metrics") + @WithDefault("true") + boolean enableMetrics(); + + /** + * The datasource pool minimum size + */ + @WithDefault("0") + @WithName("min-size") + int minSize(); + + /** + * The datasource pool maximum size + */ + @WithDefault("10") + @WithName("max-size") + int maxSize(); + + /** + * The initial pool size + */ + @WithDefault("0") + @WithName("initial-size") + int initialSize(); + + /** + * The max lifetime of a connection. + */ + @WithDefault("5M") + @WithName("max-lifetime") + @WithConverter(DurationConverter.class) + Duration maxLifetime(); + + /** + * The interval at which we try to remove idle connections. + */ + @WithDefault("2M") + @WithName("idle-removal-interval") + @WithConverter(DurationConverter.class) + Duration idleRemovalInterval(); + + /** + * The interval at which we check for connection leaks. + */ + @WithDefault("1M") + @WithName("leak-detection-interval") + @WithConverter(DurationConverter.class) + Duration leakDetectionInterval(); + + /** + * The timeout before cancelling the acquisition of a new connection + */ + @WithDefault("1M") + @WithName("acquisition-timeout") + @WithConverter(DurationConverter.class) + Duration acquisitionTimeout(); + + /** + * + */ + @WithDefault("true") + @WithName("extended-leak-report") + boolean leakReport(); + + /** + * The interval at which we validate idle connections in the background. Set to 0 to disable background validation. + */ + @WithDefault("2M") + @WithName("validation-interval") + @WithConverter(DurationConverter.class) + Duration validationInterval(); + + /** + * Query executed to validate a connection. + */ + @WithDefault("select 1") + @WithName("validation-query") + String validationQuery(); + + /** + * Other unspecified properties to be passed to the JDBC driver when creating new connections. + */ + @WithName("additional-jdbc-properties") + Map jdbcProperties(); + }//JDBCConfig + + default DataSourceConfig getDataSourceConfig(String dataSourceName) + { + if ("".equals(dataSourceName)) { + return defaultDataSource(); + } + + return namedDataSource().get(dataSourceName); + }//getDataSource +}//DataSourceConfigMapping diff --git a/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/agroal/configuration/DurationConverter.java b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/agroal/configuration/DurationConverter.java new file mode 100644 index 0000000..9924029 --- /dev/null +++ b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/agroal/configuration/DurationConverter.java @@ -0,0 +1,32 @@ +/* + * 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.agroal.configuration; + +import org.eclipse.microprofile.config.spi.Converter; + +import java.time.Duration; + +public class DurationConverter implements Converter +{ + + @Override + public Duration convert(String value) throws IllegalArgumentException, NullPointerException + { + return Duration.parse("PT" + value); + } +}//DurationConverter diff --git a/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/agroal/configuration/TransactionIsolationConverter.java b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/agroal/configuration/TransactionIsolationConverter.java new file mode 100644 index 0000000..c12f72c --- /dev/null +++ b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/agroal/configuration/TransactionIsolationConverter.java @@ -0,0 +1,30 @@ +/* + * 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.agroal.configuration; + +import io.agroal.api.configuration.AgroalConnectionFactoryConfiguration; +import org.eclipse.microprofile.config.spi.Converter; + +public class TransactionIsolationConverter implements Converter +{ + @Override + public AgroalConnectionFactoryConfiguration.TransactionIsolation convert(String value) throws IllegalArgumentException, NullPointerException + { + return AgroalConnectionFactoryConfiguration.TransactionIsolation.valueOf(value); + } +}//TransactionIsolationConverter diff --git a/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/extension/JPALiteConfigMapping.java b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/extension/JPALiteConfigMapping.java new file mode 100644 index 0000000..af1154d --- /dev/null +++ b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/extension/JPALiteConfigMapping.java @@ -0,0 +1,162 @@ +/* + * 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.extension; + +import io.jpalite.JPALiteEntityManager; +import io.quarkus.runtime.annotations.ConfigDocMapKey; +import io.quarkus.runtime.annotations.ConfigDocSection; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.*; +import jakarta.persistence.SharedCacheMode; +import jakarta.persistence.ValidationMode; +import jakarta.persistence.spi.PersistenceUnitTransactionType; +import org.eclipse.microprofile.config.spi.Converter; + +import java.util.Map; + +@ConfigMapping(prefix = "jpalite.persistenceUnit") +@ConfigRoot(phase = ConfigPhase.RUN_TIME) +public interface JPALiteConfigMapping +{ + /** + * The default persistenceUnit + * + * @return The default value + */ + @WithParentName + PersistenceUnitConfig defaultPersistenceUnit(); + + /** + * The defined units + * + * @return Map of units + */ + @ConfigDocSection + @ConfigDocMapKey("name") + @WithParentName + Map namedPersistenceUnits(); + + interface PersistenceUnitConfig + { + /** + * Specifies a valid datasource that will be associated with the persistence unit. + * The default value points to the default datasource. + */ + @WithDefault("") + @WithName("datasource-name") + String datasourceName(); + + /** + * The name of the 2nd level cache that is to be used + */ + @WithDefault("L2CACHE") + @WithName("cache-name") + String cacheName(); + + /** + * The name of the 2nd level cache provider that is to be used + */ + @WithDefault("") + @WithName("cache-provider") + String cacheProvider(); + + /** + * The cache configuration to use when create a new cache + */ + @WithDefault("" + + " " + + " " + + "") + @WithName("cache-config") + String cacheConfig(); + + /** + * Set to TRUE if the persistence unit reference a multi-tenant data source + */ + @WithDefault("FALSE") + @WithName("multi-tenant") + Boolean multiTenant(); + + /** + * The cache share mode + */ + @WithDefault("ENABLE_SELECTIVE") + @WithConverter(SharedCacheModeConverter.class) + @WithName("shared-cache-mode") + SharedCacheMode sharedCacheMode(); + + /** + * Additional properties associated with the persistence unit. + * See {@link JPALiteEntityManager} for a list of valid properties. + */ + Map properties(); + + /** + * Set the transaction type value for the persistence unit. + */ + @WithName("transaction-type") + @WithDefault("RESOURCE_LOCAL") + @WithConverter(PersistenceUnitTransactionTypeConverter.class) + PersistenceUnitTransactionType transactionType(); + + /** + * The validation mode + */ + @WithName("validation-mode") + @WithDefault("NONE") + @WithConverter(ValidationModeConverter.class) + ValidationMode validationMode(); + }//PersistenceUnitConfig + + class PersistenceUnitTransactionTypeConverter implements Converter + { + @Override + public PersistenceUnitTransactionType convert(String value) throws IllegalArgumentException, NullPointerException + { + return PersistenceUnitTransactionType.valueOf(value); + } + }//PersistenceUnitTransactionTypeConverter + + class SharedCacheModeConverter implements Converter + { + @Override + public SharedCacheMode convert(String value) throws IllegalArgumentException, NullPointerException + { + return SharedCacheMode.valueOf(value); + } + } + + class ValidationModeConverter implements Converter + { + @Override + public ValidationMode convert(String value) throws IllegalArgumentException, NullPointerException + { + return ValidationMode.valueOf(value); + } + } + + default PersistenceUnitConfig getPersistenceUnit(String persistenceUnitName) + { + if ("".equals(persistenceUnitName)) { + return defaultPersistenceUnit(); + }//if + + return namedPersistenceUnits().get(persistenceUnitName); + }//getPersistenceUnit +}//JPAConfigMapping diff --git a/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/extension/PersistenceProducer.java b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/extension/PersistenceProducer.java new file mode 100644 index 0000000..a00783f --- /dev/null +++ b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/extension/PersistenceProducer.java @@ -0,0 +1,146 @@ +/* + * 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.extension; + +import io.jpalite.PersistenceUnit; +import io.quarkus.arc.Unremovable; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.spi.InjectionPoint; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.Persistence; +import jakarta.transaction.TransactionManager; +import jakarta.transaction.TransactionSynchronizationRegistry; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; + +import java.lang.annotation.Annotation; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +@Unremovable +@ApplicationScoped +@Slf4j +public class PersistenceProducer +{ + private final Map entityManagerFactoryList = new ConcurrentHashMap<>(); + + @Inject + TransactionManager transactionManager; + + @Inject + TransactionSynchronizationRegistry transactionSynchronizationRegistry; + + public EntityManagerFactory getEntityManagerFactory(String persistenceUnit) + { + + String persistenceUnitName = ""; + if (persistenceUnit != null && !persistenceUnit.isBlank()) { + if (persistenceUnit.startsWith("${")) { + String key = persistenceUnit.substring(2, persistenceUnit.lastIndexOf('}')); + Config configProvider = ConfigProvider.getConfig(); + Optional value = configProvider.getOptionalValue(key, String.class); + if (value.isPresent()) { + LOG.debug("Persistence unit name is defined using a variable {} = {} ", persistenceUnitName, value.get()); + persistenceUnitName = value.get(); + }//if + else { + LOG.debug("Persistence unit name defined using a variable {} but variable is not defined", persistenceUnitName); + }//else + }//if + else { + persistenceUnitName = persistenceUnit; + } + }//if + + LOG.debug("Producing new Entity Manager Factory using @PersistenceUnit(\"{}\")", persistenceUnitName); + return entityManagerFactoryList.computeIfAbsent(persistenceUnitName, Persistence::createEntityManagerFactory); + } + + public EntityManager getEntityManager(String persistenceUnit) + { + return new TransactionScopedEntityManagerImpl(getEntityManagerFactory(persistenceUnit), + transactionManager, + transactionSynchronizationRegistry); + }//getEntityManager + + @Unremovable + @Produces + @PersistenceUnit + public EntityManagerFactory injectEntityManagerFactory(InjectionPoint injectionPoint) + { + String persistenceUnitName = ""; + + Annotation qualifier = null; + if (injectionPoint.getAnnotated() != null) { + qualifier = injectionPoint.getAnnotated().getAnnotations() + .stream() + .filter(PersistenceUnit.class::isInstance) + .findFirst() + .orElse(null); + }//if + + if (qualifier == null) { + /** + * If PersistenceUnit annotation is not found, check if it was not provided + * as a qualifier + */ + qualifier = injectionPoint.getQualifiers() + .stream() + .filter(PersistenceUnit.class::isInstance) + .findFirst() + .orElse(null); + }//if + + if (qualifier instanceof PersistenceUnit persistenceUnit) { + persistenceUnitName = persistenceUnit.value(); + }//if + + return getEntityManagerFactory(persistenceUnitName); + }//getEntityManagerFactory + + @Unremovable + @Produces + @PersistenceUnit + public EntityManager injectEntityManager(InjectionPoint injectionPoint) + { + return new TransactionScopedEntityManagerImpl(injectEntityManagerFactory(injectionPoint), + transactionManager, + transactionSynchronizationRegistry); + }//getEntityManager + + @Unremovable + @Produces + public EntityManagerFactory injectDefaultEntityManagerFactory(InjectionPoint injectionPoint) + { + return injectEntityManagerFactory(injectionPoint); + } + + @Unremovable + @Produces + public EntityManager injectDefaultEntityManager(InjectionPoint injectionPoint) + { + return new TransactionScopedEntityManagerImpl(injectEntityManagerFactory(injectionPoint), + transactionManager, + transactionSynchronizationRegistry); + }//getEntityManager +} diff --git a/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/extension/PersistenceUnitProperties.java b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/extension/PersistenceUnitProperties.java new file mode 100644 index 0000000..6d8a342 --- /dev/null +++ b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/extension/PersistenceUnitProperties.java @@ -0,0 +1,53 @@ +/* + * 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.extension; + +import io.jpalite.impl.CustomPersistenceUnit; +import io.smallrye.config.SmallRyeConfig; +import jakarta.persistence.PersistenceException; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.infinispan.commons.configuration.StringConfiguration; + +public class PersistenceUnitProperties extends CustomPersistenceUnit +{ + + public PersistenceUnitProperties(String unitName) + { + super(unitName); + + Config configProvider = ConfigProvider.getConfig(); + SmallRyeConfig config = configProvider.unwrap(SmallRyeConfig.class); + JPALiteConfigMapping jpaConfigMapping = config.getConfigMapping(JPALiteConfigMapping.class); + JPALiteConfigMapping.PersistenceUnitConfig unitConfig = jpaConfigMapping.getPersistenceUnit(unitName); + if (unitConfig == null) { + throw new PersistenceException("Persistent Unit '" + unitName + "' not found"); + }//if + + getProperties().putAll(unitConfig.properties()); + setMultiTenantMode(unitConfig.multiTenant()); + setTransactionType(unitConfig.transactionType()); + setDataSourceName(unitConfig.datasourceName()); + setCacheName(unitConfig.cacheName()); + setSharedCacheMode(unitConfig.sharedCacheMode()); + setValidationMode(unitConfig.validationMode()); + setCacheProvider(unitConfig.cacheProvider()); + setCacheConfig(new StringConfiguration(unitConfig.cacheConfig())); + }//PersistenceUnitProperties + +}//PersistenceUnitProperties diff --git a/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/extension/PropertyPersistenceUnitProvider.java b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/extension/PropertyPersistenceUnitProvider.java new file mode 100644 index 0000000..b6b1cb0 --- /dev/null +++ b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/extension/PropertyPersistenceUnitProvider.java @@ -0,0 +1,29 @@ +/* + * 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.extension; + +import io.jpalite.JPALitePersistenceUnit; +import io.jpalite.PersistenceUnitProvider; + +public class PropertyPersistenceUnitProvider implements PersistenceUnitProvider +{ + public JPALitePersistenceUnit getPersistenceUnit(String persistenceUnitName) + { + return new PersistenceUnitProperties(persistenceUnitName); + } +} diff --git a/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/extension/TransactionScopedEntityManagerImpl.java b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/extension/TransactionScopedEntityManagerImpl.java new file mode 100644 index 0000000..84b2d36 --- /dev/null +++ b/jpalite-quarkus-extension/runtime/src/main/java/io/jpalite/extension/TransactionScopedEntityManagerImpl.java @@ -0,0 +1,595 @@ +/* + * 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.extension; + +import io.jpalite.JPALiteEntityManager; +import io.jpalite.PersistenceContext; +import io.quarkus.arc.Arc; +import io.quarkus.runtime.BlockingOperationControl; +import io.quarkus.runtime.BlockingOperationNotAllowedException; +import jakarta.annotation.Nonnull; +import jakarta.enterprise.context.ContextNotActiveException; +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 jakarta.transaction.Status; +import jakarta.transaction.Synchronization; +import jakarta.transaction.TransactionManager; +import jakarta.transaction.TransactionSynchronizationRegistry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Closeable; +import java.sql.ResultSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class TransactionScopedEntityManagerImpl implements JPALiteEntityManager +{ + private static final Logger LOG = LoggerFactory.getLogger(TransactionScopedEntityManagerImpl.class); + private final EntityManagerFactory entityManagerFactory; + private final TransactionManager transactionManager; + private final TransactionSynchronizationRegistry transactionSynchronizationRegistry; + private final String syncKey = getClass().getName() + "-entityManager"; + private final Map properties; + + private static class EntityManagerSession implements Closeable + { + private final boolean canClose; + private final EntityManager entityManager; + + public EntityManagerSession(EntityManager entityManager, boolean canClose) + { + this.canClose = canClose; + this.entityManager = entityManager; + } + + @Override + public void close() + { + if (canClose) { + entityManager.close(); + }//if + } + + public EntityManager getEntityManager() + { + return entityManager; + } + } + + public TransactionScopedEntityManagerImpl(EntityManagerFactory entityManagerFactory, + TransactionManager transactionManager, + TransactionSynchronizationRegistry transactionSynchronizationRegistry) + { + properties = new HashMap<>(); + this.entityManagerFactory = entityManagerFactory; + this.transactionManager = transactionManager; + this.transactionSynchronizationRegistry = transactionSynchronizationRegistry; + } + + private EntityManagerSession getEntityManager() + { + 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 (!Arc.container().requestContext().isActive()) { + throw new ContextNotActiveException("Cannot use the EntityManager because neither a transaction nor a CDI request context is active. " + + "Consider adding @Transactional to your method to automatically activate a transaction, " + + "or @ActivateRequestContext if you have valid reasons not to use transactions."); + }//if + + LOG.trace("Fetching EntityManager"); + if (isInTransaction()) { + LOG.trace("In JTA Transaction, getting EntityManager from JTA"); + EntityManager entityManager = (EntityManager) transactionSynchronizationRegistry.getResource(syncKey); + if (entityManager != null) { + properties.forEach(entityManager::setProperty); + return new EntityManagerSession(entityManager, false); + }//if + + final EntityManager newEntityManager = entityManagerFactory.createEntityManager(SynchronizationType.SYNCHRONIZED, properties); + transactionSynchronizationRegistry.putResource(syncKey, newEntityManager); + + newEntityManager.joinTransaction(); + transactionSynchronizationRegistry.registerInterposedSynchronization(new Synchronization() + { + @Override + public void beforeCompletion() + { + PersistenceContext persistenceContexter = newEntityManager.unwrap(PersistenceContext.class); + persistenceContexter.commit(); + } + + @Override + public void afterCompletion(int pStatus) + { + PersistenceContext persistenceContexter = newEntityManager.unwrap(PersistenceContext.class); + persistenceContexter.afterCompletion(pStatus); + persistenceContexter.release(); + newEntityManager.close(); + } + }); + return new EntityManagerSession(newEntityManager, false); + }//if + + return new EntityManagerSession(entityManagerFactory.createEntityManager(SynchronizationType.UNSYNCHRONIZED, properties), + true); + }//getEntityManager + + private boolean isInTransaction() + { + 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); + } + } + + @Override + public void flushEntity(T entity) + { + try (EntityManagerSession session = getEntityManager()) { + if (session.getEntityManager() instanceof JPALiteEntityManager vTSEntityManager) { + vTSEntityManager.flushEntity(entity); + }//if + } + } + + @Override + public void flushOnType(Class entityClass) + { + try (EntityManagerSession session = getEntityManager()) { + if (session.getEntityManager() instanceof JPALiteEntityManager vTSEntityManager) { + vTSEntityManager.flushOnType(entityClass); + }//if + } + } + + @Override + public X mapResultSet(@Nonnull X entity, ResultSet resultSet) + { + try (EntityManagerSession session = getEntityManager()) { + if (session.getEntityManager() instanceof JPALiteEntityManager jpaEntityManager) { + return jpaEntityManager.mapResultSet(entity, resultSet); + }//if + } + return entity; + } + + @Override + public void persist(Object entity) + { + try (EntityManagerSession session = getEntityManager()) { + session.getEntityManager().persist(entity); + } + } + + @Override + public T merge(T entity) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().merge(entity); + } + } + + @Override + public T clone(T entity) + { + try (EntityManagerSession session = getEntityManager()) { + if (session.getEntityManager() instanceof JPALiteEntityManager jpaEntityManager) { + return jpaEntityManager.clone(entity); + }//if + } + return entity; + } + + @Override + public void remove(Object entity) + { + try (EntityManagerSession session = getEntityManager()) { + session.getEntityManager().remove(entity); + } + } + + @Override + public T find(Class entityClass, Object primaryKey) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().find(entityClass, primaryKey); + } + } + + @Override + public T find(Class entityClass, Object primaryKey, Map properties) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().find(entityClass, primaryKey, properties); + } + } + + @Override + public T find(Class entityClass, Object primaryKey, LockModeType lockMode) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().find(entityClass, primaryKey, lockMode); + } + } + + @Override + public T find(Class entityClass, Object primaryKey, LockModeType lockMode, Map properties) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().find(entityClass, primaryKey, lockMode, properties); + } + } + + @Override + public T getReference(Class entityClass, Object primaryKey) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().getReference(entityClass, primaryKey); + } + } + + @Override + public void flush() + { + try (EntityManagerSession session = getEntityManager()) { + session.getEntityManager().flush(); + } + } + + @Override + public void setFlushMode(FlushModeType flushMode) + { + try (EntityManagerSession session = getEntityManager()) { + session.getEntityManager().setFlushMode(flushMode); + } + } + + @Override + public FlushModeType getFlushMode() + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().getFlushMode(); + } + } + + @Override + public void lock(Object entity, LockModeType lockMode) + { + try (EntityManagerSession session = getEntityManager()) { + session.getEntityManager().lock(entity, lockMode); + } + } + + @Override + public void lock(Object entity, LockModeType lockMode, Map properties) + { + try (EntityManagerSession session = getEntityManager()) { + session.getEntityManager().lock(entity, lockMode, properties); + } + } + + @Override + public void refresh(Object entity) + { + try (EntityManagerSession session = getEntityManager()) { + session.getEntityManager().refresh(entity); + } + } + + @Override + public void refresh(Object entity, Map properties) + { + try (EntityManagerSession session = getEntityManager()) { + session.getEntityManager().refresh(entity, properties); + } + } + + @Override + public void refresh(Object entity, LockModeType lockMode) + { + try (EntityManagerSession session = getEntityManager()) { + session.getEntityManager().refresh(entity, lockMode); + } + } + + @Override + public void refresh(Object entity, LockModeType lockMode, Map properties) + { + try (EntityManagerSession session = getEntityManager()) { + session.getEntityManager().refresh(entity, lockMode, properties); + } + } + + @Override + public void clear() + { + try (EntityManagerSession session = getEntityManager()) { + session.getEntityManager().clear(); + } + } + + @Override + public void detach(Object entity) + { + try (EntityManagerSession session = getEntityManager()) { + session.getEntityManager().detach(entity); + } + } + + @Override + public boolean contains(Object entity) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().contains(entity); + } + } + + @Override + public LockModeType getLockMode(Object entity) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().getLockMode(entity); + } + } + + @Override + public void setProperty(String propertyName, Object value) + { + properties.put(propertyName, value); + try (EntityManagerSession session = getEntityManager()) { + session.getEntityManager().setProperty(propertyName, value); + } + } + + @Override + public Map getProperties() + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().getProperties(); + } + } + + @Override + public Query createQuery(String qlString) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().createQuery(qlString); + } + } + + @Override + public TypedQuery createQuery(CriteriaQuery criteriaQuery) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().createQuery(criteriaQuery); + } + } + + @Override + public Query createQuery(CriteriaUpdate updateQuery) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().createQuery(updateQuery); + } + } + + @Override + public Query createQuery(CriteriaDelete deleteQuery) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().createQuery(deleteQuery); + } + } + + @Override + public TypedQuery createQuery(String sqlString, Class resultClass) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().createQuery(sqlString, resultClass); + } + } + + @Override + public Query createNamedQuery(String name) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().createNamedQuery(name); + } + } + + @Override + public TypedQuery createNamedQuery(String name, Class resultClass) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().createNamedQuery(name, resultClass); + } + } + + @Override + public Query createNativeQuery(String sqlString) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().createNativeQuery(sqlString); + } + } + + @Override + public Query createNativeQuery(String sqlString, Class resultClass) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().createNativeQuery(sqlString, resultClass); + } + } + + @Override + public Query createNativeQuery(String sqlString, String resultSetMapping) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().createNativeQuery(sqlString, resultSetMapping); + } + } + + @Override + public StoredProcedureQuery createNamedStoredProcedureQuery(String name) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().createNamedStoredProcedureQuery(name); + } + } + + @Override + public StoredProcedureQuery createStoredProcedureQuery(String procedureName) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().createStoredProcedureQuery(procedureName); + } + } + + @Override + public StoredProcedureQuery createStoredProcedureQuery(String procedureName, Class... resultClasses) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().createStoredProcedureQuery(procedureName, resultClasses); + } + } + + @Override + public StoredProcedureQuery createStoredProcedureQuery(String procedureName, String... resultSetMappings) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().createStoredProcedureQuery(procedureName, resultSetMappings); + } + } + + @Override + public void joinTransaction() + { + try (EntityManagerSession session = getEntityManager()) { + session.getEntityManager().joinTransaction(); + } + } + + @Override + public boolean isJoinedToTransaction() + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().isJoinedToTransaction(); + } + } + + @Override + public T unwrap(Class cls) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().unwrap(cls); + } + } + + @Override + public Object getDelegate() + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().getDelegate(); + } + } + + @Override + public void close() + { + throw new IllegalStateException("Not supported for transaction scoped entity managers"); + } + + @Override + public boolean isOpen() + { + return true; + } + + @Override + public EntityTransaction getTransaction() + { + throw new IllegalStateException("Not supported for JTA entity managers"); + } + + @Override + public EntityManagerFactory getEntityManagerFactory() + { + return entityManagerFactory; + } + + @Override + public CriteriaBuilder getCriteriaBuilder() + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().getCriteriaBuilder(); + } + } + + @Override + public Metamodel getMetamodel() + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().getMetamodel(); + } + } + + @Override + public EntityGraph createEntityGraph(Class rootType) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().createEntityGraph(rootType); + } + } + + @Override + public EntityGraph createEntityGraph(String graphName) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().createEntityGraph(graphName); + } + } + + @Override + public EntityGraph getEntityGraph(String graphName) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().getEntityGraph(graphName); + } + } + + @Override + public List> getEntityGraphs(Class entityClass) + { + try (EntityManagerSession session = getEntityManager()) { + return session.getEntityManager().getEntityGraphs(entityClass); + } + } +} diff --git a/jpalite-quarkus-extension/runtime/src/main/resources/META-INF/native-image/reflect-config.json b/jpalite-quarkus-extension/runtime/src/main/resources/META-INF/native-image/reflect-config.json new file mode 100644 index 0000000..31b2d37 --- /dev/null +++ b/jpalite-quarkus-extension/runtime/src/main/resources/META-INF/native-image/reflect-config.json @@ -0,0 +1,22 @@ +[ + { + "name": "io.jpalite.agroal.AgroalDataSourceProvider", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "allPublicFields": true, + "unsafeAllocated": true + }, + { + "name": "io.jpalite.extension.PropertyPersistenceUnitProvider", + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "allPublicFields": true, + "unsafeAllocated": true + } +] diff --git a/jpalite-quarkus-extension/runtime/src/main/resources/META-INF/native-image/resource-config.json b/jpalite-quarkus-extension/runtime/src/main/resources/META-INF/native-image/resource-config.json new file mode 100644 index 0000000..8a6fa00 --- /dev/null +++ b/jpalite-quarkus-extension/runtime/src/main/resources/META-INF/native-image/resource-config.json @@ -0,0 +1,10 @@ +{ + "resources": { + "includes": [ + { + "pattern": ".*/services/.*Provider$" + } + ], + "excludes": [] + } +} diff --git a/jpalite-quarkus-extension/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/jpalite-quarkus-extension/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000..26ce63e --- /dev/null +++ b/jpalite-quarkus-extension/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,9 @@ +name: JPALite Extension +#description: Do something useful. +metadata: +# keywords: +# - greeting-extension +# guide: ... # To create and publish this guide, see https://github.com/quarkiverse/quarkiverse/wiki#documenting-your-extension +# categories: +# - "miscellaneous" +# status: "preview" diff --git a/jpalite-quarkus-extension/runtime/src/main/resources/META-INF/services/io.jpalite.DataSourceProvider b/jpalite-quarkus-extension/runtime/src/main/resources/META-INF/services/io.jpalite.DataSourceProvider new file mode 100644 index 0000000..6dc438d --- /dev/null +++ b/jpalite-quarkus-extension/runtime/src/main/resources/META-INF/services/io.jpalite.DataSourceProvider @@ -0,0 +1,18 @@ +# +# 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. +# + +io.jpalite.agroal.AgroalDataSourceProvider diff --git a/jpalite-quarkus-extension/runtime/src/main/resources/META-INF/services/io.jpalite.PersistenceUnitProvider b/jpalite-quarkus-extension/runtime/src/main/resources/META-INF/services/io.jpalite.PersistenceUnitProvider new file mode 100644 index 0000000..86fec63 --- /dev/null +++ b/jpalite-quarkus-extension/runtime/src/main/resources/META-INF/services/io.jpalite.PersistenceUnitProvider @@ -0,0 +1,18 @@ +# +# 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. +# + +io.jpalite.extension.PropertyPersistenceUnitProvider diff --git a/jpalite-repository/jpalite-repository.iml b/jpalite-repository/jpalite-repository.iml new file mode 100644 index 0000000..77aa23c --- /dev/null +++ b/jpalite-repository/jpalite-repository.iml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/jpalite-repository/pom.xml b/jpalite-repository/pom.xml new file mode 100644 index 0000000..a6b94e6 --- /dev/null +++ b/jpalite-repository/pom.xml @@ -0,0 +1,71 @@ + + + + + 4.0.0 + + io.jpalite + jpalite-parent + 3.0.0 + ../pom.xml + + + jpalite-repository + jar + JPALite Repository + + + + jakarta.persistence + jakarta.persistence-api + + + jakarta.annotation + jakarta.annotation-api + + + jakarta.enterprise + jakarta.enterprise.cdi-api + + + jakarta.transaction + jakarta.transaction-api + + + io.jpalite + jpalite-core + ${project.version} + + + io.jpalite + jpalite-extension + ${project.version} + + + com.github.jsqlparser + jsqlparser + + + com.google.auto.service + auto-service + 1.1.1 + + + diff --git a/jpalite-repository/src/main/java/io/jpalite/repository/Direction.java b/jpalite-repository/src/main/java/io/jpalite/repository/Direction.java new file mode 100644 index 0000000..8e557ab --- /dev/null +++ b/jpalite-repository/src/main/java/io/jpalite/repository/Direction.java @@ -0,0 +1,55 @@ +/* + * 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.repository; + +public enum Direction +{ + ASC, + DESC, + NATURAL; + + /** + * Returns whether the direction is ascending. + * + * @return true if to sort descending + */ + public boolean isAscending() + { + return this.equals(ASC); + } + + /** + * Returns whether the direction is descending. + * + * @return true if to sort descending + */ + public boolean isDescending() + { + return this.equals(DESC); + } + + /** + * Returns where the direction is natural + * + * @return True if sort natural + */ + public boolean isNatural() + { + return this.equals(NATURAL); + } +} diff --git a/jpalite-repository/src/main/java/io/jpalite/repository/Filter.java b/jpalite-repository/src/main/java/io/jpalite/repository/Filter.java new file mode 100644 index 0000000..e82642f --- /dev/null +++ b/jpalite-repository/src/main/java/io/jpalite/repository/Filter.java @@ -0,0 +1,392 @@ +/* + * 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.repository; + +import java.util.*; +import java.util.stream.Stream; + +public class Filter +{ + public static final String AND = " AND "; + public static final String OR = " OR "; + + private List filters; + private FilterExpression filterExpressions; + private String condOper; + + public class FilterExpression + { + private String field; + private Operators operator; + private Object[] parameter; + + public FilterExpression(String field, Operators operator, Object... parameters) + { + this.field = field; + this.operator = operator; + if (operator.getNrValues() > -1 && operator.getNrValues() != parameters.length) { + throw new IllegalArgumentException("Operator " + operator.name() + " expects " + operator.getNrValues() + " parameters but " + parameters.length + " were received"); + }//if + this.parameter = parameters; + }//FilterExpression + + public String getExpression(Map params) + { + String paramName = field.replace('.', '_'); + return switch (operator) { + case PLUS_INTERVAL -> field + " + " + operator.getOperator() + " " + parameter[0]; + case MINUS_INTERVAL -> field + " - " + operator.getOperator() + " " + parameter[0]; + case BETWEEN -> { + params.put(paramName + "1", parameter[0]); + params.put(paramName + "2", parameter[1]); + yield field + " " + operator.getOperator() + " :" + paramName + "1 and :" + paramName + "2"; + } + case IN, NOTIN -> { + StringBuilder expr = new StringBuilder(field); + expr.append(" ") + .append(operator.getOperator()) + .append(" ("); + + int paramNr = 1; + for (Object param : parameter) { + params.put(paramName + paramNr, param); + if (paramNr > 1) { + expr.append(","); + }//if + expr.append(":" + paramName + paramNr); + paramNr++; + }//for + expr.append(")"); + yield expr.toString(); + } + case CONTAINS, CONTAINS_NOT, BEGINS_WITH, ENDS_WITH -> { + StringBuilder expr = new StringBuilder(field); + expr.append(" ") + .append(operator.getOperator()); + + int paramNr = 1; + for (Object param : parameter) { + params.put(paramName + paramNr, operator.getPrefix() + param + operator.getPostfix()); + expr.append(" :" + paramName + paramNr); + paramNr++; + }//for + yield expr.toString(); + } + default -> { + StringBuilder expr = new StringBuilder(field); + expr.append(" ") + .append(operator.getOperator()); + + int paramNr = 1; + for (Object param : parameter) { + params.put(paramName + paramNr, param); + if (paramNr > 1) { + expr.append(","); + }//if + expr.append(" :" + paramName + paramNr); + paramNr++; + }//for + yield expr.toString(); + } + }; + }//getExpression + + public String getExpression() + { + return switch (operator) { + case PLUS_INTERVAL -> field + " + " + operator.getOperator() + " " + parameter[0]; + case MINUS_INTERVAL -> field + " - " + operator.getOperator() + " " + parameter[0]; + case BETWEEN -> + field + " " + operator.getOperator() + " '" + parameter[0] + "' and '" + parameter[1] + "'"; + case IN, NOTIN -> { + StringBuilder expr = new StringBuilder(field); + expr.append(" ") + .append(operator.getOperator()) + .append(" ("); + + for (Object param : parameter) { + expr.append("'").append(param).append("'"); + }//for + expr.append(")"); + yield expr.toString(); + } + case CONTAINS, CONTAINS_NOT, BEGINS_WITH, ENDS_WITH -> { + StringBuilder expr = new StringBuilder(field); + expr.append(" ") + .append(operator.getOperator()); + + int paramNr = 1; + for (Object param : parameter) { + if (paramNr > 1) { + expr.append(","); + }//if + expr.append(" '") + .append(operator.getPrefix()) + .append(param) + .append(operator.getPostfix()) + .append("'"); + paramNr++; + }//for + yield expr.toString(); + } + default -> { + StringBuilder expr = new StringBuilder(field); + expr.append(" ") + .append(operator.getOperator()); + + int paramNr = 1; + for (Object param : parameter) { + if (paramNr > 1) { + expr.append(","); + }//if + expr.append(" '") + .append(param) + .append("'"); + paramNr++; + }//for + yield expr.toString(); + } + }; + }//getExpression + + @Override + public String toString() + { + return getExpression(new HashMap<>()); + } + + @Override + public boolean equals(Object other) + { + if (this == other) return true; + if (!(other instanceof FilterExpression expr)) return false; + + if (!Objects.equals(field, expr.field)) return false; + if (operator != expr.operator) return false; + if (parameter.length != expr.parameter.length) return false; + // Probably incorrect - comparing Object[] arrays with Arrays.equals + return Arrays.equals(parameter, expr.parameter); + } + + @Override + public int hashCode() + { + int vresult = field != null ? field.hashCode() : 0; + vresult = 31 * vresult + (operator != null ? operator.hashCode() : 0); + vresult = 31 * vresult + Arrays.hashCode(parameter); + return vresult; + } + }//FilterExpression + + public static Filter of(String field, Operators operator, Object... value) + { + return new Filter(field, operator, value); + }//of + + public static Filter of(Filter filter) + { + if (filter.isUnfiltered()) { + return new Filter(); + }//if + return new Filter(filter); + }//of + + public static Filter noFilter() + { + return new Filter(); + }//noFilter + + public boolean isUnfiltered() + { + return filters.isEmpty() && filterExpressions == null; + } + + /** + * The constructor for the Filter class. + */ + private Filter() + { + condOper = ""; + filters = new ArrayList<>(); + filterExpressions = null; + }//Filter + + /** + * The constructor for the Filter class. + * + * @param filter Array for filters + */ + private Filter(Filter filter) + { + this(); + filters.add(filter); + }//Filter + + private Filter(String field, Operators operator, Object... parameters) + { + this(); + + filterExpressions = new FilterExpression(field, operator, parameters); + }//Filter + + List getFilters() + { + return filters; + } + + String getCondOper() + { + return condOper; + } + + public Filter orWhere(Filter filter) + { + if (condOper.isBlank() && filters.isEmpty() && filterExpressions != null) { + Filter orFilter = new Filter(); + orFilter.filterExpressions = filterExpressions; + orFilter.condOper = OR; + filterExpressions = null; + filters.add(orFilter); + }//if + + filter.condOper = OR; + filters.add(filter); + return this; + }//orWhere + + public Filter andWhere(Filter filter) + { + if (condOper.isBlank() && filters.isEmpty() && filterExpressions != null) { + Filter andFilter = new Filter(); + andFilter.filterExpressions = filterExpressions; + andFilter.condOper = AND; + filterExpressions = null; + filters.add(andFilter); + }//if + filter.condOper = AND; + filters.add(filter); + return this; + }//andWhere + + public Filter removeWhere(String field) + { + if (filterExpressions != null && filterExpressions.field.equals(field)) { + filterExpressions = null; + } + else { + filters.forEach(f -> f.removeWhere(field)); + filters.removeIf(Filter::isUnfiltered); + }//else + + return this; + }//removeWhere + + public Stream stream() + { + return filters.stream(); + }//stream + + public String getExpression(Map params) + { + if (filterExpressions != null) { + return filterExpressions.getExpression(params); + }//if + else { + if (!filters.isEmpty()) { + StringBuilder buffer = new StringBuilder("("); + boolean first = true; + for (Filter filter : filters) { + if (!first) { + buffer.append(filter.getCondOper()); + }//if + else { + first = false; + }//else + + buffer.append(filter.getExpression(params)); + }//for + buffer.append(")"); + return buffer.toString(); + }//if + }//else + + return ""; + }//getExpression + + public String getExpression() + { + if (filterExpressions != null) { + return filterExpressions.getExpression(); + }//if + else { + if (!filters.isEmpty()) { + StringBuilder buffer = new StringBuilder("("); + boolean first = true; + for (Filter vFilter : filters) { + if (!first) { + buffer.append(vFilter.getCondOper()); + }//if + else { + first = false; + }//else + + buffer.append(vFilter.getExpression()); + }//for + buffer.append(")"); + return buffer.toString(); + }//if + }//else + + return ""; + } + + @Override + public String toString() + { + return getExpression(new HashMap<>()); + }//toString + + public static Filter fromExpression(String expression, Object... parameters) + { + if (expression == null || expression.isBlank()) { + return noFilter(); + }//if + FilterParser parser = new FilterParser(expression, parameters); + return parser.getFilter(); + } + + @Override + public boolean equals(Object other) + { + if (this == other) return true; + if (!(other instanceof Filter filter)) return false; + + if (!Objects.equals(filters, filter.filters)) return false; + if (!Objects.equals(filterExpressions, filter.filterExpressions)) return false; + return Objects.equals(condOper, filter.condOper); + } + + @Override + public int hashCode() + { + int vresult = filters != null ? filters.hashCode() : 0; + vresult = 31 * vresult + (filterExpressions != null ? filterExpressions.hashCode() : 0); + vresult = 31 * vresult + (condOper != null ? condOper.hashCode() : 0); + return vresult; + } +}//Filter diff --git a/jpalite-repository/src/main/java/io/jpalite/repository/FilterParser.java b/jpalite-repository/src/main/java/io/jpalite/repository/FilterParser.java new file mode 100644 index 0000000..d280fa0 --- /dev/null +++ b/jpalite-repository/src/main/java/io/jpalite/repository/FilterParser.java @@ -0,0 +1,295 @@ +/* + * 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.repository; + +import jakarta.persistence.PersistenceException; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.*; +import net.sf.jsqlparser.expression.operators.arithmetic.Addition; +import net.sf.jsqlparser.expression.operators.arithmetic.Subtraction; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.expression.operators.conditional.OrExpression; +import net.sf.jsqlparser.expression.operators.relational.*; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.select.PlainSelect; + +import java.util.ArrayList; +import java.util.List; + +class FilterParser extends ParserBase +{ + private Filter filter = Filter.noFilter(); + private final Object[] parameters; + private int paramNr = 0; + private Filter currentFilter; + private String col = null; + private String operationType = null; + private final List params = new ArrayList<>(); + + public FilterParser(String whereExpression, Object... params) + { + parameters = params; + currentFilter = filter; + try { + Statement statement = CCJSqlParserUtil.parse("select * from t where " + whereExpression); + statement.accept(this); + }//try + catch (JSQLParserException ex) { + throw new PersistenceException("Error parsing query", ex); + }//catch + } + + public Filter getFilter() + { + return filter; + } + + @Override + public void visit(PlainSelect plainSelect) + { + if (plainSelect.getWhere() != null) { + plainSelect.getWhere().accept(this); + } + } + + @Override + public void visit(Addition addition) + { + addition.getLeftExpression().accept(this); + operationType = addition.getStringExpression(); + addition.getRightExpression().accept(this); + } + + @Override + public void visit(Subtraction subtraction) + { + subtraction.getLeftExpression().accept(this); + operationType = subtraction.getStringExpression(); + subtraction.getRightExpression().accept(this); + } + + @Override + public void visit(JdbcParameter jdbcParameter) + { + params.add(parameters[paramNr++]); + } + + @Override + public void visit(JdbcNamedParameter jdbcNamedParameter) + { + params.add(parameters[paramNr++]); + } + + + @Override + public void visit(DoubleValue pValue) + { + params.add(pValue.getValue()); + } + + @Override + public void visit(LongValue pValue) + { + params.add(pValue.getValue()); + } + + @Override + public void visit(HexValue pValue) + { + params.add(pValue.getValue()); + } + + @Override + public void visit(DateValue pValue) + { + params.add(pValue.getValue()); + } + + @Override + public void visit(TimeValue pValue) + { + params.add(pValue.getValue()); + } + + @Override + public void visit(TimestampValue pValue) + { + params.add(pValue.getValue()); + } + + @Override + public void visit(StringValue pValue) + { + params.add(pValue.getValue()); + } + + @Override + public void visit(Column tableColumn) + { + col = tableColumn.getFullyQualifiedName(); + } + + @Override + public void visit(Parenthesis parenthesis) + { + parenthesis.getExpression().accept(this); + } + + @Override + public void visit(AndExpression andExpression) + { + andExpression.getLeftExpression().accept(this); + andExpression.getRightExpression().accept(this); + } + + @Override + public void visit(OrExpression orExpression) + { + boolean vRoot = (currentFilter.isUnfiltered() && currentFilter == filter); + + orExpression.getLeftExpression().accept(this); + Filter filter = currentFilter; + currentFilter = Filter.noFilter(); + orExpression.getRightExpression().accept(this); + + currentFilter = Filter.of(filter).orWhere(currentFilter); + if (vRoot) { + this.filter = currentFilter; + }//if + } + + private void addFilter(Operators pOperators) + { + Filter filter = Filter.of(col, pOperators, params.toArray()); + + if (currentFilter.isUnfiltered() && currentFilter == this.filter) { + currentFilter = filter; + this.filter = filter; + }//if + else { + if (currentFilter.isUnfiltered()) { + currentFilter = filter; + }//if + else { + currentFilter.andWhere(filter); + }//else + }//else + + col = null; + params.clear(); + } + + @Override + public void visit(Between between) + { + between.getLeftExpression().accept(this); + between.getBetweenExpressionStart().accept(this); + between.getBetweenExpressionEnd().accept(this); + addFilter(Operators.BETWEEN); + } + + @Override + public void visit(EqualsTo equalsTo) + { + equalsTo.getLeftExpression().accept(this); + equalsTo.getRightExpression().accept(this); + addFilter(Operators.EQUALS); + } + + @Override + public void visit(GreaterThan greaterThan) + { + greaterThan.getLeftExpression().accept(this); + greaterThan.getRightExpression().accept(this); + addFilter(Operators.BIGGER_THAN); + } + + @Override + public void visit(GreaterThanEquals greaterThanEquals) + { + greaterThanEquals.getLeftExpression().accept(this); + greaterThanEquals.getRightExpression().accept(this); + addFilter(Operators.BIGGER_OR_EQUAL); + } + + @Override + public void visit(InExpression inExpression) + { + if (inExpression.getLeftExpression() != null) { + inExpression.getLeftExpression().accept(this); + }//if + + if (inExpression.getRightItemsList() != null) { + inExpression.getRightItemsList().accept(this); + }//if + + addFilter(inExpression.isNot() ? Operators.NOTIN : Operators.IN); + } + + + @Override + public void visit(IsNullExpression isNullExpression) + { + isNullExpression.getLeftExpression().accept(this); + addFilter(isNullExpression.isNot() ? Operators.ISNOTNULL : Operators.ISNULL); + } + + @Override + public void visit(LikeExpression likeExpression) + { + likeExpression.getLeftExpression().accept(this); + likeExpression.getRightExpression().accept(this); + addFilter(likeExpression.isNot() ? Operators.CONTAINS_NOT : Operators.CONTAINS); + } + + @Override + public void visit(MinorThan minorThan) + { + minorThan.getLeftExpression().accept(this); + minorThan.getRightExpression().accept(this); + addFilter(Operators.SMALLER_THAN); + } + + @Override + public void visit(MinorThanEquals minorThanEquals) + { + minorThanEquals.getLeftExpression().accept(this); + minorThanEquals.getRightExpression().accept(this); + addFilter(Operators.SMALLER_OR_EQUAL); + } + + @Override + public void visit(NotEqualsTo notEqualsTo) + { + notEqualsTo.getLeftExpression().accept(this); + notEqualsTo.getRightExpression().accept(this); + addFilter(Operators.NOTEQUALS); + } + + @Override + public void visit(IntervalExpression intervalExpression) + { + if (operationType != null) { + params.add(intervalExpression.getParameter()); + addFilter(operationType.equals("+") ? Operators.PLUS_INTERVAL : Operators.MINUS_INTERVAL); + operationType = null; + }//if + } +} diff --git a/jpalite-repository/src/main/java/io/jpalite/repository/JPALiteRepositoryProcessor.java b/jpalite-repository/src/main/java/io/jpalite/repository/JPALiteRepositoryProcessor.java new file mode 100644 index 0000000..f37b404 --- /dev/null +++ b/jpalite-repository/src/main/java/io/jpalite/repository/JPALiteRepositoryProcessor.java @@ -0,0 +1,465 @@ +/* + * 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.repository; + +import com.google.auto.service.AutoService; +import jakarta.persistence.LockModeType; +import jakarta.persistence.QueryHint; + +import javax.annotation.processing.*; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.tools.Diagnostic; +import javax.tools.FileObject; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +@SupportedAnnotationTypes("io.jpalite.repository.Repository") +@SupportedSourceVersion(SourceVersion.RELEASE_21) +@AutoService(Processor.class) +@SuppressWarnings("java:S1192") //We are generating code here. Defining statics will impair the readability +public class JPALiteRepositoryProcessor extends AbstractProcessor +{ + private static final String DATE_FORMAT = "yyyy/MM/dd HH:mm:ss"; + + //---------------------------------------------------------------[ note ]--- + private void note(String msg) + { + processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, msg); + }//note + + //------------------------------------------------------------[ process ]--- + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) + { + + if (roundEnv.processingOver()) { + return false; + }//if + + note("Creating TradeSwitch® JPA Repositories"); + try { + Set set = roundEnv.getElementsAnnotatedWith(Repository.class); + + for (Element element : set) { + Repository annotation = element.getAnnotation(Repository.class); + if (annotation != null) { + generateRepo((TypeElement) element, annotation); + }//if + }//for + return true; + }//try + catch (Exception ex) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, ex.getMessage()); + }//catch + + return false; + }//process + + private void addJpaRepository(PrintWriter out, DeclaredType jpaRepository) + { + + String argType = jpaRepository.getTypeArguments().get(0).toString(); + String idType = jpaRepository.getTypeArguments().get(1).toString(); + + out.println("@Transactional"); + out.println("public void persist(" + argType + " entity) {"); + out.println(" EntityManager em = getEntityManager();"); + out.println(" if (!em.contains(entity)) {"); + out.println(" em.persist(entity);"); + out.println(" }"); + out.println("}"); + out.println(""); + + out.println("@Transactional"); + out.println("public void save(" + argType + " entity) {"); + out.println(" EntityManager em = getEntityManager();"); + out.println(" if (!em.contains(entity)) {"); + out.println(" if (em.getEntityManagerFactory().getPersistenceUnitUtil().getIdentifier(entity) != null) {"); + out.println(" em.merge(entity);"); + out.println(" } else {"); + out.println(" em.persist(entity);"); + out.println(" }"); + out.println(" }"); + out.println("}"); + out.println(""); + + out.println("@Transactional"); + out.println("public " + argType + " merge(" + argType + " entity) {"); + out.println(" EntityManager em = getEntityManager();"); + out.println(" if (!em.contains(entity)) {"); + out.println(" return (" + argType + ") em.merge(entity);"); + out.println(" }"); + out.println(" return (" + argType + ")entity;"); + out.println("}"); + out.println(""); + + out.println("public void refresh(" + argType + " entity) {"); + out.println(" EntityManager em = getEntityManager();"); + out.println(" em.refresh(entity);"); + out.println("}"); + out.println(""); + + out.println("public void lock(" + argType + " entity, LockModeType mode) {"); + out.println(" EntityManager em = getEntityManager();"); + out.println(" em.lock(entity, mode);"); + out.println("}"); + out.println(""); + + out.println("public " + argType + " findById(" + idType + " id) {"); + out.println(" EntityManager em = getEntityManager();"); + out.println(" return em.find(" + argType + ".class,id);"); + out.println("}"); + out.println(""); + + out.println("public " + argType + " findById(" + idType + " id, LockModeType mode) {"); + out.println(" EntityManager em = getEntityManager();"); + out.println(" return em.find(" + argType + ".class,id, mode);"); + out.println("}"); + out.println(""); + + out.println("public " + argType + " getReference(" + idType + " id) {"); + out.println(" EntityManager em = getEntityManager();"); + out.println(" return em.getReference(" + argType + ".class,id);"); + out.println("}"); + out.println(""); + + out.println("public " + argType + " clone(" + argType + " entity) {"); + out.println(" EntityManager em = getEntityManager();"); + out.println(" return ((JPALiteEntityManager)em).clone(entity);"); + out.println("}"); + out.println(""); + + out.println("public void delete(" + argType + " entity) {"); + out.println(" EntityManager em = getEntityManager();"); + out.println(" em.remove(entity);"); + out.println("}"); + out.println(""); + }//addJpaRepository + + private void addPagingRepository(PrintWriter out, DeclaredType pagingRepository) + { + String vArgType = pagingRepository.getTypeArguments().get(0).toString(); + String vEntityType = vArgType.substring(vArgType.lastIndexOf(".") + 1); + + out.println("public long count(Filter filter, Map hints) {"); + out.println(" EntityManager em = getEntityManager();"); + out.print(" String q=\"select count("); + out.print(vEntityType); + out.print(") from "); + out.print(vEntityType); + out.println("\";"); + out.println(" Map parameters = new HashMap<>();"); + out.println(" if (!filter.isUnfiltered()) q = q+\" where \"+filter.getExpression(parameters); "); + out.println(" TypedQuery query = em.createQuery(q,Long.class);"); + out.println(" if (hints != null && !hints.isEmpty()) {"); + out.println(" hints.entrySet().forEach(entry -> query.setHint(entry.getKey(), entry.getValue()));"); + out.println(" }"); + out.println(" if (!parameters.isEmpty()) parameters.entrySet().stream().forEach(entry -> query.setParameter(entry.getKey(), entry.getValue()));"); + out.println(" return query.getSingleResult();"); + out.println("}"); + out.println(""); + + out.print("public List<"); + out.print(vArgType); + out.println("> findAll(Pageable pageable, Filter filter, Map hints) {"); + out.println(" EntityManager em = getEntityManager();"); + out.println(" if (hints != null && !hints.isEmpty()) {"); + out.println(" hints.entrySet().stream().forEach(entry -> em.setProperty(entry.getKey(), entry.getValue()));"); + out.println(" }"); + + out.print(" String q=\"select "); + out.print(vEntityType); + out.print(" from "); + out.print(vEntityType); + out.println("\";"); + out.println(" Map parameters = new HashMap<>();"); + out.println(" if (!filter.isUnfiltered()) q = q+\" where \"+filter.getExpression(parameters); "); + out.println(" if (!pageable.getSort().isUnsorted()) q = q+\" order by \"+pageable.getSort().getExpression();"); + out.print(" TypedQuery<"); + out.print(vArgType); + out.print("> query = em.createQuery(q,"); + out.print(vArgType); + out.println(".class);"); + out.println(" if (hints != null && !hints.isEmpty()) {"); + out.println(" hints.entrySet().forEach(entry -> query.setHint(entry.getKey(), entry.getValue()));"); + out.println(" }"); + out.println(" if (!parameters.isEmpty()) parameters.entrySet().stream().forEach(entry -> query.setParameter(entry.getKey(), entry.getValue()));"); + out.println(" if (!pageable.isUnpaged()) query.setFirstResult(pageable.getPageIndex())"); + out.println(" .setMaxResults(pageable.getPageSize());"); + out.println(" return query.getResultList();"); + out.println("}"); + out.println(""); + }//addPagingRepository + + @SuppressWarnings("java:S3776") //Complexity cannot be reduced further + private void createMethod(PrintWriter out, ExecutableElement method) + { + Query[] queries = method.getAnnotationsByType(Query.class); + if (queries.length > 0) { + method.getAnnotationMirrors() + .forEach(p -> + { + if (!p.getAnnotationType().toString().equals(Query.class.getName())) { + out.println(p); + }//if + }); + + out.print("public final "); + + //The return type can either be a List or an object + + out.print(method.getReturnType()); + out.print(" "); + out.print(method.getSimpleName().toString()); + out.print("("); + + out.print(method.getParameters() + .stream() + .map(p -> p.asType() + " " + p.getSimpleName()) + .collect(Collectors.joining(","))); + out.println(") {"); + out.println(" EntityManager em = getEntityManager();"); + boolean returnCollection = false; + String returnType = method.getReturnType().toString(); + if (method.getReturnType() instanceof DeclaredType declaredType && !declaredType.getTypeArguments().isEmpty()) { + returnCollection = true; + returnType = declaredType.getTypeArguments().get(0).toString(); + }//if + + StringBuilder pageAndSort = new StringBuilder(); + Query query = queries[0]; + if (query.namedQuery()) { + out.print("TypedQuery<"); + out.print(returnType); + out.print("> query = em.createNamedQuery(\""); + out.print(query.value()); + out.print("\","); + out.print(returnType); + out.println(".class);"); + + method.getParameters() + .stream() + .filter(p -> p.asType().toString().equals(Pageable.class.getName())) + .forEach(p -> pageAndSort.append("query.setFirstResult(" + p.getSimpleName() + ".getPageIndex())\n") + .append(".setMaxResults(" + p.getSimpleName() + ".getPageSize());\n")); + }//if + else { + out.println("String queryStr = \"" + query.value() + "\";"); + + //Pageable and Sorting is only support for JPQL queries + if (!query.nativeQuery() && !query.updateQuery()) { + //Check for Pageable parameter + method.getParameters() + .stream() + .filter(p -> p.asType().toString().equals(Pageable.class.getName())) + .forEach(p -> + { + pageAndSort.append("query.setFirstResult(" + p.getSimpleName() + ".getPageIndex())\n") + .append(".setMaxResults(" + p.getSimpleName() + ".getPageSize());\n"); + out.println("queryStr += " + p.getSimpleName() + ".getSort().getOrderBy();"); + }); + method.getParameters() + .stream() + .filter(p -> p.asType().toString().equals(Sort.class.getName())) + .forEach(p -> out.println("queryStr += " + p.getSimpleName() + ".getOrderBy();")); + }//if + + if (query.nativeQuery()) { + out.println("jakarta.persistence.Query query = em.createNativeQuery(queryStr);"); + }//if + else if (query.updateQuery()) { + out.println("jakarta.persistence.Query query = em.createQuery(queryStr);"); + }//if + else { + out.print("TypedQuery<"); + out.print(returnType); + out.print("> query = em.createQuery(queryStr,"); + + out.print(returnType); + out.println(".class);"); + }//else + }//else + + if (!query.updateQuery() && query.lockMode() != LockModeType.NONE) { + out.print(" query.setLockMode("); + out.print("LockModeType." + query.lockMode().name()); + out.println(");"); + }//if + + if (method.getParameters() + .stream() + .anyMatch(p -> p.getAnnotation(QueryParam.class) != null)) { + + method.getParameters() + .stream() + .filter(p -> p.getAnnotation(QueryParam.class) != null) + .map(p -> "\"" + p.getAnnotation(QueryParam.class).value() + "\"," + p.getSimpleName()) + .forEach(p -> out.println("query.setParameter(" + p + ");")); + }//if + else { + AtomicInteger paramNr = new AtomicInteger(0); + method.getParameters() + .stream() + .filter(p -> !p.asType().toString().equals(Pageable.class.getName())) + .map(p -> paramNr.incrementAndGet() + "," + p.getSimpleName()) + .forEach(p -> out.println("query.setParameter(" + p + ");")); + }//else + + for (QueryHint hint : query.hints()) { + out.print("query.setHint(\""); + out.print(hint.name()); + out.print("\",\""); + out.print(hint.value()); + out.println("\");"); + }//for + + if (query.updateQuery()) { + out.println("return query.executeUpdate();"); + }//if + else { + out.println(pageAndSort); + + if (!returnCollection && query.catchNoResult()) { + out.println("try {"); + }//if + out.print(" return "); + if (query.nativeQuery()) { + out.print("("); + out.print(returnType); + out.print(") "); + }//if + out.print(" query."); + if (returnCollection) { + out.println("getResultList();"); + }//if + else { + out.println("getSingleResult();"); + }//else + + if (!returnCollection && query.catchNoResult()) { + out.println("}"); + out.println("catch (NoResultException ex) {"); + out.println(" return null;"); + out.println("}"); + }//if + }//else + + out.println("}"); + }//if + }//createMethod + + private void generateRepo(TypeElement repoElement, Repository annotation) throws IOException + { + + String packageName = annotation.name(); + if (packageName.isBlank()) { + packageName = repoElement.getQualifiedName() + "Impl"; + }//if + + int lastDot = packageName.lastIndexOf('.'); + if (lastDot == 0) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@Repository.name is invalid, expected qualified name"); + return; + }//if + + FileObject repoFile = processingEnv.getFiler().createSourceFile(packageName); + String repoClassName = packageName.substring(lastDot + 1); + packageName = packageName.substring(0, lastDot); + + try (PrintWriter out = new PrintWriter(repoFile.openWriter())) { + out.print("package "); + out.print(packageName); + out.println(";"); + out.println(); + out.println("import io.quarkus.arc.Arc;"); + out.println("import jakarta.enterprise.context.RequestScoped;"); + out.println("import jakarta.inject.Inject;"); + out.println("import jakarta.transaction.Transactional;"); + out.println("import jakarta.annotation.Generated;"); + out.println("import jakarta.persistence.*;"); + out.println("import java.util.Collections;"); + out.println("import java.util.List;"); + out.println("import java.util.HashMap;"); + out.println("import java.util.Map;"); + out.println("import io.jpalite.extension.TradeSwitchPersistenceProducer;"); + out.println("import io.jpalite.TradeSwitchEntityManager;"); + out.println("import io.jpalite.PersistenceUnit;"); + out.println("import io.jpalite.repository.*;"); + out.println("import io.jpalite.EntityState;"); + out.println(); + + SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT); + out.println("@Generated(value={\"Generated by " + getClass().getName() + "\"}," + + "date=\"" + dateFormat.format(new Date()) + "\"," + + "comments=\"JPALite Repository Generation\")"); + out.println("@RequestScoped"); + out.print("public class "); + out.print(repoClassName); + if (!annotation.abstractClass().isBlank()) { + out.print(" extends "); + out.print(annotation.abstractClass()); + out.print(" "); + }//if + out.print(" implements "); + out.print(repoElement.getQualifiedName()); + + out.println(" {"); + + out.println("public EntityManager getEntityManager() {"); + //out.println(" EntityManager em = Arc.container().instance(EntityManager.class).get();"); + out.println(" PersistenceProducer producer = Arc.container().instance(PersistenceProducer.class).get();"); + out.print(" return producer.getEntityManager(\""); + //out.print(" return em;"); + out.print(annotation.persistenceUnit()); + out.println("\");"); + out.println("}"); + + repoElement.getInterfaces() + .forEach(t -> + { + switch (((DeclaredType) t).asElement().toString()) { + case "io.jpalite.repository.JpaRepository" -> + addJpaRepository(out, (DeclaredType) t); + case "io.jpalite.repository.PagingRepository" -> + addPagingRepository(out, (DeclaredType) t); + default -> { + //Ignore the rest + } + } + }); + + for (Element vMethod : repoElement.getEnclosedElements()) { + createMethod(out, (ExecutableElement) vMethod); + }//for + + out.println("}"); + out.flush(); + }//try + catch (Exception ex) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Process Error:" + ex.getMessage()); + }//catch + }//generateRepo +}//JPALiteRepositoryProcessor diff --git a/jpalite-repository/src/main/java/io/jpalite/repository/JpaRepository.java b/jpalite-repository/src/main/java/io/jpalite/repository/JpaRepository.java new file mode 100644 index 0000000..c9eaaa9 --- /dev/null +++ b/jpalite-repository/src/main/java/io/jpalite/repository/JpaRepository.java @@ -0,0 +1,114 @@ +/* + * 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.repository; + +import jakarta.persistence.LockModeType; + +/** + * The JpaRepository is part of the JPALite Repository generation feature. + *

The implementation of the interface + * must not be coded and is generated by the JPALite Repository Generator + *

Remember to attach the interface + * to the Repository Generator using the @Repository annotation! + *

+ *

+ * Usage Example: + *

{@code
+ *
+ * @Repository
+ * public interface MyRepository implements JpaRepository {
+ * }
+ *
+ * }
+ *

+ * + * @param The Entity class type + * @param The Primary Key class type + */ +public interface JpaRepository extends RepositoryBase +{ + /** + * Save an entity. If the entity is new it will be persis. If it is an existing entity it will be merged + * + * @param entity The entity to save + */ + void save(E entity); + + /** + * Persist the entity if it is not attached + * + * @param entity The entity to persist + */ + void persist(E entity); + + /** + * Lock the current entity from being updated by another thread + * + * @param entity The entity + * @param lockModeType The locktype + */ + void lock(E entity, LockModeType lockModeType); + + /** + * Merge entity into the current context. + * + * @param entity The entity + * @return The replaced entity + */ + E merge(E entity); + + /** + * Replace the current entity with the value from storage + * + * @param entity the entity to refresh + */ + void refresh(E entity); + + /** + * Search for an entity given it's primary key + * + * @param id The primary key + * @return The retrieved entity or null if not found + */ + E findById(I id); + + E findById(I id, LockModeType lockModeType); + + /** + * Create a reference to an existing entity given the primary key + * + * @param id The primary key + * @return The entity + */ + E getReference(I id); + + /** + * Create a clone of an existing entity. + * + * @param entity + * @return The new entity + */ + E clone(E entity); + + /** + * Delete an attached entity from the repository. + * + * @param entity + */ + void delete(E entity); +} diff --git a/jpalite-repository/src/main/java/io/jpalite/repository/NullHandling.java b/jpalite-repository/src/main/java/io/jpalite/repository/NullHandling.java new file mode 100644 index 0000000..5ba7c83 --- /dev/null +++ b/jpalite-repository/src/main/java/io/jpalite/repository/NullHandling.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.repository; + +public enum NullHandling +{ + NATIVE, + FIRST, + LAST +} diff --git a/jpalite-repository/src/main/java/io/jpalite/repository/Operators.java b/jpalite-repository/src/main/java/io/jpalite/repository/Operators.java new file mode 100644 index 0000000..75a0ef3 --- /dev/null +++ b/jpalite-repository/src/main/java/io/jpalite/repository/Operators.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.repository; + +public enum Operators +{ + EQUALS("Equal to", 0, "=", "", "", 1), + NOTEQUALS("Not Equal to", 0, "!=", "", "", 1), + BEGINS_WITH("Begins with", 1, "ilike", "", "%", 1), + ENDS_WITH("Ends with", 1, "ilike", "%", "", 1), + CONTAINS("Contains", 1, "ilike", "%", "%", 1), + CONTAINS_NOT("Does Not Contain", 1, "not ilike", "%", "%", 1), + BIGGER_THAN("Bigger than", 2, ">", "", "", 1), + BIGGER_OR_EQUAL("Bigger than or equal to", 2, ">=", "", "", 1), + SMALLER_THAN("Smaller than", 2, "<", "", "", 1), + SMALLER_OR_EQUAL("Smaller than or equal to", 2, "<=", "", "", 1), + ISNULL("Is Not Set", 0, "is null", "", "", 0), + ISNOTNULL("Is Set", 0, "is not null", "", "", 0), + BETWEEN("Between", 1, "between", "", "", 2), + PLUS_INTERVAL("+ Interval", 3, "interval", "", "", 1), + MINUS_INTERVAL("- Interval", 3, "interval", "", "", 1), + IN("In", 3, "in", "(", ")", -1), + NOTIN("Not In", 3, "not in", "(", ")", -1); + + private final String comparator; + private final int category; + private final String label; + private final String prefix; + private final String postfix; + private final int nrvalues; + + private Operators(String label, int category, String comparator, String prefix, String postfix, int nrValues) + { + this.label = label; + this.category = category; + this.comparator = comparator; + this.prefix = prefix; + this.postfix = postfix; + nrvalues = nrValues; + }//Operators + + public String getLabel() + { + return label; + } + + public int getNrValues() + { + return nrvalues; + } + + public int getCategory() + { + return category; + } + + public String getOperator() + { + return comparator; + } + + public String getPrefix() + { + return prefix; + } + + public String getPostfix() + { + return postfix; + } +}//Operators diff --git a/jpalite-repository/src/main/java/io/jpalite/repository/Pageable.java b/jpalite-repository/src/main/java/io/jpalite/repository/Pageable.java new file mode 100644 index 0000000..8ffd24d --- /dev/null +++ b/jpalite-repository/src/main/java/io/jpalite/repository/Pageable.java @@ -0,0 +1,123 @@ +/* + * 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.repository; + +public class Pageable +{ + private final int pageNumber; + private final int pageSize; + private final Sort sort; + + public static Pageable unpaged() + { + return new Pageable(0, Integer.MAX_VALUE, Sort.unsorted()); + } + + public static Pageable of(Sort sort) + { + return new Pageable(0, Integer.MAX_VALUE, sort); + } + + public static Pageable of(int pageSize) + { + return new Pageable(0, pageSize, Sort.unsorted()); + } + + public static Pageable of(int pageNumber, int pageSize) + { + return new Pageable(pageNumber, pageSize, Sort.unsorted()); + } + + public static Pageable of(int pageNumber, int pageSize, Sort sort) + { + return new Pageable(pageNumber, pageSize, sort); + }//of + + public static Pageable fromExpression(String expression) + { + SortParser parser = new SortParser(expression); + return Pageable.of(parser.getOffset() / parser.getLimit(), parser.getLimit(), parser.getSort()); + } + + private Pageable(int pageNumber, int pageSize, Sort sort) + { + this.pageNumber = pageNumber; + this.pageSize = pageSize; + this.sort = sort; + } + + public int getPageNumber() + { + return pageNumber; + } + + + public int getPageSize() + { + return pageSize; + } + + public Sort getSort() + { + return sort; + } + + public int getPageIndex() + { + return pageNumber * pageSize; + } + + public boolean isUnpaged() + { + return (pageSize == Integer.MAX_VALUE); + } + + public Pageable next() + { + return new Pageable(getPageNumber() + 1, getPageSize(), getSort()); + } + + public Pageable previous() + { + return getPageNumber() == 0 ? this : new Pageable(getPageNumber() - 1, getPageSize(), getSort()); + } + + public Pageable first() + { + return new Pageable(0, getPageSize(), getSort()); + } + + public String getExpression() + { + StringBuilder stringBuilder = new StringBuilder(); + if (!sort.isUnsorted()) { + stringBuilder.append("order by ").append(sort.getExpression()); + }//if + if (!isUnpaged()) { + stringBuilder.append(" offset ").append(getPageIndex()) + .append(" limit ").append(getPageSize()); + }//if + return stringBuilder.toString(); + } + + @Override + public String toString() + { + return getExpression(); + } +} diff --git a/jpalite-repository/src/main/java/io/jpalite/repository/PagingRepository.java b/jpalite-repository/src/main/java/io/jpalite/repository/PagingRepository.java new file mode 100644 index 0000000..bfc8d69 --- /dev/null +++ b/jpalite-repository/src/main/java/io/jpalite/repository/PagingRepository.java @@ -0,0 +1,166 @@ +/* + * 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.repository; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * The PagingRepository is part of the TradeSwitch Repository generation feature. + *

The implementation of the interface + * must not be coded and is generated by the TradeSwitch Repository Generator + *

Remember to attach the interface + * to the Repository Generator using the @Repository annotation! + *

+ *

+ * Usage Example: + *

{@code
+ *
+ * @Repository
+ * public interface MyRepository implements PagingRepository {
+ * }
+ *
+ * }
+ *

+ * + * @param The Entity class the paging repository controls + */ +public interface PagingRepository extends RepositoryBase +{ + /** + * Retrieve ALL the entities in the repository. + *

+ * NOTE: This method should NOT be used on a repository that has a large number of entries + *

+ * + * @return A list of the all the entities + */ + default List findAll() + { + return findAll(Pageable.unpaged(), Filter.noFilter(), Collections.emptyMap()); + }//findAll + + default List findAll(Map hints) + { + return findAll(Pageable.unpaged(), Filter.noFilter(), hints); + }//findAll + + /** + * Retrieve ALL the entities in the repository using a pageable control + *

+ * NOTE: Take care not to specify a page size that cannot fit in memory + *

+ * + * @return A list of the entities based on the pageable control + */ + default List findAll(Pageable pageable) + { + return findAll(pageable, Filter.noFilter(), Collections.emptyMap()); + }//findAll + + default List findAll(Pageable pageable, Map hints) + { + return findAll(pageable, Filter.noFilter(), hints); + }//findAll + + /** + * Retrieve ALL the entities in the repository using a filter control + *

+ * NOTE: This method should NOT be used on a repository that has a large number of entries + *

+ * + * @param filter The filter control + * @return A list of the all the filtered entities + */ + default List findAll(Filter filter) + { + return findAll(Pageable.unpaged(), filter, Collections.emptyMap()); + }//findAll + + default List findAll(Filter filter, Map hints) + { + return findAll(Pageable.unpaged(), filter, hints); + }//findAll + + /** + * Retrieve ALL the entities in the repository order by the Sort control + *

+ * NOTE: This method should NOT be used on a repository that has a large number of entries + *

+ * + * @param sort The sort control + * @return A list of the all the entities + */ + default List findAll(Sort sort) + { + return findAll(Pageable.of(sort), Filter.noFilter(), Collections.emptyMap()); + }//findAll + + default List findAll(Sort sort, Map hints) + { + return findAll(Pageable.of(sort), Filter.noFilter(), hints); + }//findAll + + /** + * Retrieve the entities from repository using a pageable and filter control + *

+ * NOTE: This is the recommend method to retrieve entity from a large repository. Take care not to specify a page + * size that cannot fit in memory + *

+ * + * @param pageable The pageable control + * @param filter The filter control + * @return A list of the all the filtered entities using the pageable control to limit the number entities returned + */ + default List findAll(Pageable pageable, Filter filter) + { + return findAll(pageable, filter, Collections.emptyMap()); + }//findAll + + List findAll(Pageable pageable, Filter filter, Map hints); + + + default long count(Filter filter) + { + return count(filter, Collections.emptyMap()); + } + + /** + * Return the number of entities found in the repository. + * + * @return The number of entities + */ + default long count() + { + return count(Filter.noFilter(), Collections.emptyMap()); + } + + default long count(Map hints) + { + return count(Filter.noFilter(), hints); + } + + /** + * Return the number of entities found in the repository using the filter control + * + * @param filter The filter control + * @return The number of entities + */ + long count(Filter filter, Map hints); +}//PagingRepository diff --git a/jpalite-repository/src/main/java/io/jpalite/repository/ParserBase.java b/jpalite-repository/src/main/java/io/jpalite/repository/ParserBase.java new file mode 100644 index 0000000..393c73c --- /dev/null +++ b/jpalite-repository/src/main/java/io/jpalite/repository/ParserBase.java @@ -0,0 +1,909 @@ +/* + * 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.repository; + +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; + +class ParserBase implements StatementVisitor, SelectVisitor, FromItemVisitor, ItemsListVisitor, ExpressionVisitor +{ + @Override + public void visit(Analyze analyze) + { + //ignore + } + + @Override + public void visit(SavepointStatement savepointStatement) + { + //ignore + } + + @Override + public void visit(RollbackStatement rollbackStatement) + { + //ignore + } + + @Override + public void visit(Comment comment) + { + //ignore + } + + @Override + public void visit(Commit commit) + { + //ignore + } + + @Override + public void visit(Delete delete) + { + //ignore + } + + @Override + public void visit(Update update) + { + //ignore + } + + @Override + public void visit(Insert insert) + { + //ignore + } + + @Override + public void visit(Replace replace) + { + //ignore + } + + @Override + public void visit(Drop drop) + { + //ignore + } + + @Override + public void visit(Truncate truncate) + { + //ignore + } + + @Override + public void visit(CreateIndex createIndex) + { + //ignore + } + + @Override + public void visit(CreateSchema aThis) + { + //ignore + } + + @Override + public void visit(CreateTable createTable) + { + //ignore + } + + @Override + public void visit(CreateView createView) + { + //ignore + } + + @Override + public void visit(AlterView alterView) + { + //ignore + } + + @Override + public void visit(Alter alter) + { + //ignore + } + + @Override + public void visit(Statements stmts) + { + //ignore + } + + @Override + public void visit(Execute execute) + { + //ignore + } + + @Override + public void visit(SetStatement set) + { + //ignore + } + + @Override + public void visit(ResetStatement reset) + { + //ignore + } + + @Override + public void visit(ShowColumnsStatement set) + { + //ignore + } + + @Override + public void visit(ShowTablesStatement showTables) + { + //ignore + } + + @Override + public void visit(Merge merge) + { + //ignore + } + + @Override + public void visit(Select select) + { + select.getSelectBody().accept(this); + } + + @Override + public void visit(Upsert upsert) + { + //ignore + } + + @Override + public void visit(UseStatement use) + { + //ignore + } + + @Override + public void visit(Block block) + { + //ignore + } + + @Override + public void visit(DescribeStatement describe) + { + //ignore + } + + @Override + public void visit(ExplainStatement aThis) + { + //ignore + } + + @Override + public void visit(ShowStatement aThis) + { + //ignore + } + + @Override + public void visit(DeclareStatement aThis) + { + //ignore + } + + @Override + public void visit(Grant grant) + { + //ignore + } + + @Override + public void visit(CreateSequence createSequence) + { + //ignore + } + + @Override + public void visit(AlterSequence alterSequence) + { + //ignore + } + + @Override + public void visit(CreateFunctionalStatement createFunctionalStatement) + { + //ignore + } + + @Override + public void visit(CreateSynonym createSynonym) + { + //ignore + } + + @Override + public void visit(AlterSession alterSession) + { + //ignore + } + + @Override + public void visit(IfElseStatement aThis) + { + //ignore + } + + @Override + public void visit(RenameTableStatement renameTableStatement) + { + //ignore + } + + @Override + public void visit(PurgeStatement purgeStatement) + { + //ignore + } + + @Override + public void visit(AlterSystemStatement alterSystemStatement) + { + //ignore + } + + @Override + public void visit(UnsupportedStatement unsupportedStatement) + { + //ignore + } + + @Override + public void visit(PlainSelect plainSelect) + { + if (plainSelect.getWhere() != null) { + plainSelect.getWhere().accept(this); + } + } + + @Override + public void visit(SetOperationList setOpList) + { + //ignore + } + + @Override + public void visit(WithItem withItem) + { + //ignore + } + + @Override + public void visit(ValuesStatement aThis) + { + //ignore + } + + @Override + public void visit(BitwiseRightShift aThis) + { + //ignore + } + + @Override + public void visit(BitwiseLeftShift aThis) + { + //ignore + } + + @Override + public void visit(NullValue nullValue) + { + //ignore + } + + @Override + public void visit(Function function) + { + //ignore + } + + @Override + public void visit(SignedExpression signedExpression) + { + //ignore + } + + @Override + public void visit(JdbcParameter jdbcParameter) + { + //ignore + } + + @Override + public void visit(JdbcNamedParameter jdbcNamedParameter) + { + //ignore + } + + @Override + public void visit(DoubleValue doubleValue) + { + //ignore + } + + @Override + public void visit(LongValue longValue) + { + //ignore + } + + @Override + public void visit(HexValue hexValue) + { + //ignore + } + + @Override + public void visit(DateValue dateValue) + { + //ignore + } + + @Override + public void visit(TimeValue timeValue) + { + //ignore + } + + @Override + public void visit(TimestampValue timestampValue) + { + //ignore + } + + @Override + public void visit(Parenthesis parenthesis) + { + //ignore + } + + @Override + public void visit(StringValue stringValue) + { + //ignore + } + + @Override + public void visit(Addition addition) + { + addition.getLeftExpression().accept(this); + addition.getRightExpression().accept(this); + //ignore + } + + @Override + public void visit(Division division) + { + //ignore + } + + @Override + public void visit(IntegerDivision division) + { + //ignore + } + + @Override + public void visit(Multiplication multiplication) + { + //ignore + } + + @Override + public void visit(Subtraction subtraction) + { + //ignore + } + + @Override + public void visit(AndExpression andExpression) + { + //ignore + } + + @Override + public void visit(OrExpression orExpression) + { + //ignore + } + + @Override + public void visit(XorExpression orExpression) + { + //ignore + } + + @Override + public void visit(Between between) + { + //ignore + } + + @Override + public void visit(EqualsTo equalsTo) + { + //ignore + } + + @Override + public void visit(GreaterThan greaterThan) + { + //ignore + } + + @Override + public void visit(GreaterThanEquals greaterThanEquals) + { + //ignore + } + + @Override + public void visit(InExpression inExpression) + { + //ignore + } + + @Override + public void visit(FullTextSearch fullTextSearch) + { + //ignore + } + + @Override + public void visit(IsNullExpression isNullExpression) + { + //ignore + } + + @Override + public void visit(IsBooleanExpression isBooleanExpression) + { + //ignore + } + + @Override + public void visit(LikeExpression likeExpression) + { + //ignore + } + + @Override + public void visit(MinorThan minorThan) + { + //ignore + } + + @Override + public void visit(MinorThanEquals minorThanEquals) + { + //ignore + } + + @Override + public void visit(NotEqualsTo notEqualsTo) + { + //ignore + } + + @Override + public void visit(Column tableColumn) + { + //ignore + } + + @Override + public void visit(CaseExpression caseExpression) + { + //ignore + } + + @Override + public void visit(WhenClause whenClause) + { + //ignore + } + + @Override + public void visit(ExistsExpression existsExpression) + { + //ignore + } + + @Override + public void visit(AnyComparisonExpression anyComparisonExpression) + { + //ignore + } + + @Override + public void visit(Concat concat) + { + //ignore + } + + @Override + public void visit(Matches matches) + { + //ignore + } + + @Override + public void visit(BitwiseAnd bitwiseAnd) + { + //ignore + } + + @Override + public void visit(BitwiseOr bitwiseOr) + { + //ignore + } + + @Override + public void visit(BitwiseXor bitwiseXor) + { + //ignore + } + + @Override + public void visit(CastExpression cast) + { + //ignore + } + + @Override + public void visit(TryCastExpression cast) + { + //ignore + } + + @Override + public void visit(Modulo modulo) + { + //ignore + } + + @Override + public void visit(AnalyticExpression aexpr) + { + //ignore + } + + @Override + public void visit(ExtractExpression eexpr) + { + //ignore + } + + @Override + public void visit(IntervalExpression iexpr) + { + //ignore + } + + @Override + public void visit(OracleHierarchicalExpression oexpr) + { + //ignore + } + + @Override + public void visit(RegExpMatchOperator rexpr) + { + //ignore + } + + @Override + public void visit(JsonExpression jsonExpr) + { + //ignore + } + + @Override + public void visit(JsonOperator jsonExpr) + { + //ignore + } + + @Override + public void visit(RegExpMySQLOperator regExpMySQLOperator) + { + //ignore + } + + @Override + public void visit(UserVariable uservar) + { + //ignore + } + + @Override + public void visit(NumericBind bind) + { + //ignore + } + + @Override + public void visit(KeepExpression aexpr) + { + //ignore + } + + @Override + public void visit(MySQLGroupConcat groupConcat) + { + //ignore + } + + @Override + public void visit(ValueListExpression valueList) + { + //ignore + } + + @Override + public void visit(RowConstructor rowConstructor) + { + //ignore + } + + @Override + public void visit(RowGetExpression rowGetExpression) + { + //ignore + } + + @Override + public void visit(OracleHint hint) + { + //ignore + } + + @Override + public void visit(TimeKeyExpression timeKeyExpression) + { + //ignore + } + + @Override + public void visit(DateTimeLiteralExpression literal) + { + //ignore + } + + @Override + public void visit(NotExpression aThis) + { + //ignore + } + + @Override + public void visit(NextValExpression aThis) + { + //ignore + } + + @Override + public void visit(CollateExpression aThis) + { + //ignore + } + + @Override + public void visit(SimilarToExpression aThis) + { + //ignore + } + + @Override + public void visit(ArrayExpression aThis) + { + //ignore + } + + @Override + public void visit(ArrayConstructor aThis) + { + //ignore + } + + @Override + public void visit(VariableAssignment aThis) + { + //ignore + } + + @Override + public void visit(XMLSerializeExpr aThis) + { + //ignore + } + + @Override + public void visit(TimezoneExpression aThis) + { + //ignore + } + + @Override + public void visit(JsonAggregateFunction aThis) + { + //ignore + } + + @Override + public void visit(JsonFunction aThis) + { + //ignore + } + + @Override + public void visit(ConnectByRootOperator aThis) + { + //ignore + } + + @Override + public void visit(OracleNamedFunctionParameter aThis) + { + //ignore + } + + @Override + public void visit(AllColumns allColumns) + { + //ignore + } + + @Override + public void visit(AllTableColumns allTableColumns) + { + //ignore + } + + @Override + public void visit(AllValue allValue) + { + //ignore + } + + @Override + public void visit(IsDistinctExpression isDistinctExpression) + { + //ignore + } + + @Override + public void visit(GeometryDistance geometryDistance) + { + //ignore + } + + @Override + public void visit(ExpressionList expressionList) + { + expressionList.getExpressions().stream() + .forEach(e -> e.accept(this)); + } + + @Override + public void visit(NamedExpressionList namedExpressionList) + { + //ignore + } + + @Override + public void visit(MultiExpressionList multiExprList) + { + //ignore + } + + @Override + public void visit(Table tableName) + { + //ignore + } + + @Override + public void visit(SubSelect subSelect) + { + //ignore + } + + @Override + public void visit(SubJoin subjoin) + { + //ignore + } + + @Override + public void visit(LateralSubSelect lateralSubSelect) + { + //ignore + } + + @Override + public void visit(ValuesList valuesList) + { + //ignore + } + + @Override + public void visit(TableFunction tableFunction) + { + //ignore + } + + @Override + public void visit(ParenthesisFromItem aThis) + { + //ignore + } +} diff --git a/jpalite-repository/src/main/java/io/jpalite/repository/Query.java b/jpalite-repository/src/main/java/io/jpalite/repository/Query.java new file mode 100644 index 0000000..dc1ab68 --- /dev/null +++ b/jpalite-repository/src/main/java/io/jpalite/repository/Query.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.repository; + +import jakarta.persistence.LockModeType; +import jakarta.persistence.QueryHint; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Query +{ + String value(); + + boolean namedQuery() default false; + + boolean nativeQuery() default false; + + boolean updateQuery() default false; + + boolean catchNoResult() default false; + + LockModeType lockMode() default LockModeType.NONE; + + QueryHint[] hints() default {}; +} diff --git a/jpalite-repository/src/main/java/io/jpalite/repository/QueryParam.java b/jpalite-repository/src/main/java/io/jpalite/repository/QueryParam.java new file mode 100644 index 0000000..8da8c8d --- /dev/null +++ b/jpalite-repository/src/main/java/io/jpalite/repository/QueryParam.java @@ -0,0 +1,30 @@ +/* + * 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.repository; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface QueryParam +{ + String value(); +} diff --git a/jpalite-repository/src/main/java/io/jpalite/repository/Repository.java b/jpalite-repository/src/main/java/io/jpalite/repository/Repository.java new file mode 100644 index 0000000..78392d4 --- /dev/null +++ b/jpalite-repository/src/main/java/io/jpalite/repository/Repository.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.repository; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Repository +{ + /** + * The name of the repository + * + * @return The name + */ + String name() default ""; + + String persistenceUnit() default ""; + + /** + * The name of the abstract class + * + * @return + */ + String abstractClass() default ""; +} diff --git a/jpalite-repository/src/main/java/io/jpalite/repository/RepositoryBase.java b/jpalite-repository/src/main/java/io/jpalite/repository/RepositoryBase.java new file mode 100644 index 0000000..a231aed --- /dev/null +++ b/jpalite-repository/src/main/java/io/jpalite/repository/RepositoryBase.java @@ -0,0 +1,29 @@ +/* + * 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.repository; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceException; + +public interface RepositoryBase +{ + default EntityManager getEntityManager() + { + throw new PersistenceException("Not implemented"); + } +} diff --git a/jpalite-repository/src/main/java/io/jpalite/repository/Sort.java b/jpalite-repository/src/main/java/io/jpalite/repository/Sort.java new file mode 100644 index 0000000..398e36d --- /dev/null +++ b/jpalite-repository/src/main/java/io/jpalite/repository/Sort.java @@ -0,0 +1,166 @@ +/* + * 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.repository; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class Sort +{ + private static final Sort UNSORTED = new Sort(); + + private final List sortOrders; + + public static Sort by(List orders) + { + if (orders == null) { + throw new IllegalArgumentException("Orders cannot by null"); + }//if + + return orders.isEmpty() ? Sort.unsorted() : new Sort(orders); + }//by + + public static Sort by(SortOrder... orders) + { + if (orders == null) { + throw new IllegalArgumentException("You have to provide at least one order to sort by"); + } + + return new Sort(Arrays.asList(orders)); + }//by + + public static Sort by(String... fields) + { + return (fields.length == 0) ? Sort.unsorted() : new Sort(SortOrder.DEFAULT_DIRECTION, Arrays.asList(fields)); + }//by + + public static Sort by(Direction direction, String... fields) + { + + if (direction == null) { + throw new IllegalArgumentException("Direction cannot be null"); + } + if (fields == null || fields.length == 0) { + throw new IllegalArgumentException("You have to provide at least one field to sort by"); + } + + return Sort.by(Arrays.stream(fields) + .map(it -> new SortOrder(direction, it)) + .toList()); + }//by + + public static Sort byExpression(String orderByExpression) + { + SortParser vParser = new SortParser("order by " + orderByExpression); + return vParser.getSort(); + }//byExpression + + public static Sort unsorted() + { + return UNSORTED; + }//unsorted + + public boolean isUnsorted() + { + return sortOrders.isEmpty(); + }//isUnsorted + + private Sort() + { + sortOrders = Collections.emptyList(); + } + + private Sort(List orders) + { + this.sortOrders = new ArrayList<>(orders); + }//Sort + + private Sort(Direction direction, List fields) + { + if (fields == null || fields.isEmpty()) { + throw new IllegalArgumentException("You have to provide at least one field to sort by"); + } + + this.sortOrders = fields.stream() + .map(field -> new SortOrder(direction, field)) + .toList(); + }//Sort + + public Sort and(Sort sort) + { + if (sort == null) { + throw new IllegalArgumentException("Sort cannot be null"); + } + + List newSortOrder = new ArrayList<>(sortOrders); + newSortOrder.addAll(sort.sortOrders); + + return Sort.by(newSortOrder); + }//and + + public Sort descending() + { + return withDirection(Direction.DESC); + }//descending + + public Sort ascending() + { + return withDirection(Direction.ASC); + }//ascending + + public Sort naturally() + { + return withDirection(Direction.NATURAL); + }//naturally + + public Sort withDirection(Direction direction) + { + return Sort.by(sortOrders.stream().map(it -> it.with(direction)).toList()); + }//withDirection + + public Stream stream() + { + return sortOrders.stream(); + }//stream + + public String getExpression() + { + return stream() + .map(p -> p.getField() + (p.getDirection() == Direction.NATURAL ? "" : " " + p.getDirection()) + (p.getNullHandling() == NullHandling.NATIVE ? "" : " NULLS " + p.getNullHandling())) + .collect(Collectors.joining(", ")); + } + + @Override + public String toString() + { + return getExpression(); + } + + public String getOrderBy() + { + if (sortOrders.isEmpty()) { + return ""; + }//if + + return " order by " + getExpression(); + }//getOrderBy +}//Sort diff --git a/jpalite-repository/src/main/java/io/jpalite/repository/SortOrder.java b/jpalite-repository/src/main/java/io/jpalite/repository/SortOrder.java new file mode 100644 index 0000000..03f6cea --- /dev/null +++ b/jpalite-repository/src/main/java/io/jpalite/repository/SortOrder.java @@ -0,0 +1,64 @@ +/* + * 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.repository; + +public class SortOrder +{ + private static final NullHandling DEFAULT_NULL_HANDLING = NullHandling.NATIVE; + public static final Direction DEFAULT_DIRECTION = Direction.ASC; + private final String field; + private final Direction direction; + private final NullHandling nullHandling; + + public SortOrder(Direction direction, String field, NullHandling nullHandling) + { + this.field = field; + this.direction = direction; + this.nullHandling = nullHandling; + } + + public SortOrder(Direction direction, String field) + { + this(direction, field, DEFAULT_NULL_HANDLING); + } + + public SortOrder() + { + this(DEFAULT_DIRECTION, null, DEFAULT_NULL_HANDLING); + } + + public SortOrder with(Direction direction) + { + return new SortOrder(direction, field, nullHandling); + } + + public String getField() + { + return field; + } + + public Direction getDirection() + { + return direction; + } + + public NullHandling getNullHandling() + { + return nullHandling; + } +} diff --git a/jpalite-repository/src/main/java/io/jpalite/repository/SortParser.java b/jpalite-repository/src/main/java/io/jpalite/repository/SortParser.java new file mode 100644 index 0000000..b7c2ff8 --- /dev/null +++ b/jpalite-repository/src/main/java/io/jpalite/repository/SortParser.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.repository; + +import jakarta.persistence.PersistenceException; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.select.OrderByElement; +import net.sf.jsqlparser.statement.select.OrderByVisitor; +import net.sf.jsqlparser.statement.select.PlainSelect; + +class SortParser extends ParserBase implements OrderByVisitor +{ + private Sort sort = Sort.unsorted(); + private int limit = Integer.MAX_VALUE; + private int offset = 0; + + public SortParser(String expression) + { + try { + Statement statement = CCJSqlParserUtil.parse("select * from T " + expression); + statement.accept(this); + }//try + catch (JSQLParserException ex) { + throw new PersistenceException("Error parsing query", ex); + }//catch + } + + public Sort getSort() + { + return sort; + } + + public int getLimit() + { + return limit; + } + + public int getOffset() + { + return offset; + } + + @Override + public void visit(OrderByElement orderBy) + { + if (orderBy.getExpression() instanceof Column vColumn) { + orderBy.getExpression().accept(this); + + Direction direction = Direction.NATURAL; + if (orderBy.isAscDescPresent()) { + direction = orderBy.isAsc() ? Direction.ASC : Direction.DESC; + }//if + NullHandling handling = NullHandling.NATIVE; + if (orderBy.getNullOrdering() != null) { + handling = (orderBy.getNullOrdering() == OrderByElement.NullOrdering.NULLS_FIRST) ? NullHandling.FIRST : NullHandling.LAST; + }//if + + sort = sort.and(Sort.by(new SortOrder(direction, vColumn.getColumnName(), handling))); + }//if + } + + @Override + public void visit(PlainSelect plainSelect) + { + if (plainSelect.getOrderByElements() != null) { + plainSelect.getOrderByElements().forEach(o -> o.accept(this)); + }//if + + if (plainSelect.getLimit() != null && plainSelect.getLimit().getRowCount() instanceof LongValue aLimit) { + this.limit = (int) aLimit.getValue(); + }//if + + if (plainSelect.getOffset() != null && plainSelect.getOffset().getOffset() instanceof LongValue aOffset) { + this.offset = (int) aOffset.getValue(); + }//if + } +} diff --git a/jpalite-repository/src/test/java/io/jpalite/repository/FilterParserTest.java b/jpalite-repository/src/test/java/io/jpalite/repository/FilterParserTest.java new file mode 100644 index 0000000..fc45e7b --- /dev/null +++ b/jpalite-repository/src/test/java/io/jpalite/repository/FilterParserTest.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.repository; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class FilterParserTest +{ + + @Test + void whenSingleEqualExpression() + { + FilterParser parser = new FilterParser("test = ?", "1234"); + Filter expectedFilter = Filter.of("test", Operators.EQUALS, "1234"); + assertEquals(expectedFilter, parser.getFilter()); + } + + @Test + void whenSingleNotEqualExpression() + { + FilterParser parser = new FilterParser("test <> ?", "1234"); + Filter expectedFilter = Filter.of("test", Operators.NOTEQUALS, "1234"); + assertEquals(expectedFilter, parser.getFilter()); + } + + @Test + void whenSingleBiggerThanExpression() + { + FilterParser parser = new FilterParser("test > ?", 1234); + Filter expectedFilter = Filter.of("test", Operators.BIGGER_THAN, 1234); + assertEquals(expectedFilter, parser.getFilter()); + + parser = new FilterParser("test >= ?", 1234); + expectedFilter = Filter.of("test", Operators.BIGGER_OR_EQUAL, 1234); + assertEquals(expectedFilter, parser.getFilter()); + } + + @Test + void whenSingleSmallerThanExpression() + { + FilterParser parser = new FilterParser("test < ?", 1234); + Filter expectedFilter = Filter.of("test", Operators.SMALLER_THAN, 1234); + assertEquals(expectedFilter, parser.getFilter()); + + parser = new FilterParser("test <= ?", 1234); + expectedFilter = Filter.of("test", Operators.SMALLER_OR_EQUAL, 1234); + assertEquals(expectedFilter, parser.getFilter()); + } + + @Test + void whenSingleContainsExpression() + { + FilterParser parser = new FilterParser("test ilike ?", "1234"); + Filter expectedFilter = Filter.of("test", Operators.CONTAINS, "1234"); + assertEquals(expectedFilter, parser.getFilter()); + + parser = new FilterParser("test not ilike ?", "1234"); + expectedFilter = Filter.of("test", Operators.CONTAINS_NOT, "1234"); + assertEquals(expectedFilter, parser.getFilter()); + } + + + @Test + void whenSingleBetweenExpression() + { + FilterParser parser = new FilterParser("test between ? and ?", 1234, 9999); + Filter expectedFilter = Filter.of("test", Operators.BETWEEN, 1234, 9999); + assertEquals(expectedFilter, parser.getFilter()); + } + + @Test + void whenPlusIntervalExpression() + { + FilterParser parser = new FilterParser("test + interval '5 days'"); + Filter expectedFilter = Filter.of("test", Operators.PLUS_INTERVAL, "'5 days'"); + assertEquals(expectedFilter, parser.getFilter()); + assertEquals("test + interval '5 days'", expectedFilter.getExpression()); + } + + @Test + void whenMinusIntervalExpression() + { + FilterParser parser = new FilterParser("test - interval '5 days'"); + Filter expectedFilter = Filter.of("test", Operators.MINUS_INTERVAL, "'5 days'"); + assertEquals(expectedFilter, parser.getFilter()); + assertEquals("test - interval '5 days'", expectedFilter.getExpression()); + } + + @Test + void whenSingleInExpression() + { + FilterParser parser = new FilterParser("test in (?,?,?)", 1234, 5555, 9999); + Filter expectedFilter = Filter.of("test", Operators.IN, 1234, 5555, 9999); + assertEquals(expectedFilter, parser.getFilter()); + + parser = new FilterParser("test not in (?,?,?)", 1234, 5555, 9999); + expectedFilter = Filter.of("test", Operators.NOTIN, 1234, 5555, 9999); + assertEquals(expectedFilter, parser.getFilter()); + } + + @Test + void whenSingleIsNullExpression() + { + FilterParser parser = new FilterParser("test is null"); + Filter expectedFilter = Filter.of("test", Operators.ISNULL); + assertEquals(expectedFilter, parser.getFilter()); + + parser = new FilterParser("test is not null"); + expectedFilter = Filter.of("test", Operators.ISNOTNULL); + assertEquals(expectedFilter, parser.getFilter()); + } + + @Test + void whenMultipleAndExpression() + { + FilterParser parser = new FilterParser("test = ? and id =?", "1234", 9999); + Filter expectedFilter = Filter.of("test", Operators.EQUALS, "1234") + .andWhere(Filter.of("id", Operators.EQUALS, 9999)); + assertEquals(expectedFilter, parser.getFilter()); + + parser = new FilterParser("test = ? and id =? and try = ?", "1234", 9999, 4444); + expectedFilter = Filter.of("test", Operators.EQUALS, "1234") + .andWhere(Filter.of("id", Operators.EQUALS, 9999)) + .andWhere(Filter.of("try", Operators.EQUALS, 4444)); + assertEquals(expectedFilter, parser.getFilter()); + } + + @Test + void whenMultipleWithParenthesisAndExpression() + { + FilterParser parser = new FilterParser("(test = ? and id =? and try = ?)", "1234", 9999, 4444); + Filter expectedFilter = Filter.of("test", Operators.EQUALS, "1234") + .andWhere(Filter.of("id", Operators.EQUALS, 9999)) + .andWhere(Filter.of("try", Operators.EQUALS, 4444)); + assertEquals(expectedFilter, parser.getFilter()); + } + + @Test + void whenMultipleWithParenthesisAndOrExpression() + { + FilterParser parser = new FilterParser("((test = :test1 AND test2 = :test21) OR test3 != :test31)", "1234", 9999, 4444); + Filter expectedFilter = Filter.of(Filter.of("test", Operators.EQUALS, "1234") + .andWhere(Filter.of("test2", Operators.EQUALS, 9999))) + .orWhere(Filter.of("test3", Operators.NOTEQUALS, 4444)); + assertEquals(expectedFilter, parser.getFilter()); + } + + @Test + void whenExpressionIncludesParameters() + { + FilterParser parser = new FilterParser("((test = '1234' AND test2 = 9999) OR test3 != 4444)"); + Filter expectedFilter = Filter.of(Filter.of("test", Operators.EQUALS, "1234") + .andWhere(Filter.of("test2", Operators.EQUALS, 9999L))) + .orWhere(Filter.of("test3", Operators.NOTEQUALS, 4444L)); + assertEquals(expectedFilter, parser.getFilter()); + } + +} diff --git a/jpalite-repository/src/test/java/io/jpalite/repository/FilterTest.java b/jpalite-repository/src/test/java/io/jpalite/repository/FilterTest.java new file mode 100644 index 0000000..ebdbbba --- /dev/null +++ b/jpalite-repository/src/test/java/io/jpalite/repository/FilterTest.java @@ -0,0 +1,312 @@ +/* + * 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.repository; + +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +class FilterTest +{ + + @Test + void noFilter() + { + Filter filter = Filter.noFilter(); + assertTrue(filter.isUnfiltered()); + assertEquals("", filter.getExpression()); + + filter = Filter.of(Filter.noFilter()); + assertTrue(filter.isUnfiltered()); + assertEquals("", filter.getExpression()); + + filter = Filter.noFilter(); + filter.andWhere(Filter.of("test", Operators.EQUALS, 1)); + assertFalse(filter.isUnfiltered()); + assertEquals("(test = '1')", filter.getExpression()); + + filter = Filter.of("test", Operators.EQUALS, 1); + assertFalse(filter.isUnfiltered()); + } + + @Test + void withSingleExpression_UsingEquals_GetExpression() + { + Filter filter = Filter.of("test", Operators.EQUALS, 2); + Map parameters = new HashMap<>(); + String expression = filter.getExpression(parameters); + assertEquals("test = :test1", expression); + assertEquals(1, parameters.size()); + assertEquals(2, parameters.get("test1")); + + filter = Filter.of("test", Operators.NOTEQUALS, 2); + parameters = new HashMap<>(); + expression = filter.getExpression(parameters); + assertEquals("test != :test1", expression); + assertEquals(1, parameters.size()); + assertEquals(2, parameters.get("test1")); + } + + @Test + void withSingleExpression_UsingBiggerThan_GetExpression() + { + Filter filter = Filter.of("test", Operators.BIGGER_THAN, 2); + Map parameters = new HashMap<>(); + String expression = filter.getExpression(parameters); + assertEquals("test > :test1", expression); + assertEquals(1, parameters.size()); + assertEquals(2, parameters.get("test1")); + + filter = Filter.of("test", Operators.BIGGER_OR_EQUAL, 2); + parameters = new HashMap<>(); + expression = filter.getExpression(parameters); + assertEquals("test >= :test1", expression); + assertEquals(1, parameters.size()); + assertEquals(2, parameters.get("test1")); + } + + @Test + void withSingleExpression_UsingSmallerThan_GetExpression() + { + Filter filter = Filter.of("test", Operators.SMALLER_THAN, 2); + Map parameters = new HashMap<>(); + String expression = filter.getExpression(parameters); + assertEquals("test < :test1", expression); + assertEquals(1, parameters.size()); + assertEquals(2, parameters.get("test1")); + + filter = Filter.of("test", Operators.SMALLER_OR_EQUAL, 2); + parameters = new HashMap<>(); + expression = filter.getExpression(parameters); + assertEquals("test <= :test1", expression); + assertEquals(1, parameters.size()); + assertEquals(2, parameters.get("test1")); + } + + @Test + void withSingleExpression_UsingIN_GetExpression() + { + Filter filter = Filter.of("test", Operators.IN, 2, 3, 4); + Map parameters = new HashMap<>(); + String expression = filter.getExpression(parameters); + assertEquals("test in (:test1,:test2,:test3)", expression); + assertEquals(3, parameters.size()); + assertEquals(2, parameters.get("test1")); + assertEquals(3, parameters.get("test2")); + assertEquals(4, parameters.get("test3")); + + filter = Filter.of("test", Operators.NOTIN, 2, 3, 4); + parameters = new HashMap<>(); + expression = filter.getExpression(parameters); + assertEquals("test not in (:test1,:test2,:test3)", expression); + assertEquals(3, parameters.size()); + assertEquals(2, parameters.get("test1")); + assertEquals(3, parameters.get("test2")); + assertEquals(4, parameters.get("test3")); + } + + @Test + void withSingleExpression_UsingBetween_GetExpression() + { + Filter filter = Filter.of("test", Operators.BETWEEN, 2, 4); + Map parameters = new HashMap<>(); + String expression = filter.getExpression(parameters); + assertEquals("test between :test1 and :test2", expression); + assertEquals(2, parameters.size()); + assertEquals(2, parameters.get("test1")); + assertEquals(4, parameters.get("test2")); + } + + + @Test + void withSingleExpression_UsingIsNUll_GetExpression() + { + Filter filter = Filter.of("test", Operators.ISNULL); + Map parameters = new HashMap<>(); + String expression = filter.getExpression(parameters); + assertEquals("test is null", expression); + assertTrue(parameters.isEmpty()); + + filter = Filter.of("test", Operators.ISNOTNULL); + parameters = new HashMap<>(); + expression = filter.getExpression(parameters); + assertEquals("test is not null", expression); + assertTrue(parameters.isEmpty()); + } + + @Test + void withSingleExpression_UsingContains_GetExpression() + { + Filter filter = Filter.of("test", Operators.CONTAINS, "test"); + Map parameters = new HashMap<>(); + String expression = filter.getExpression(parameters); + assertEquals("test ilike :test1", expression); + assertEquals(1, parameters.size()); + assertEquals("%test%", parameters.get("test1")); + + filter = Filter.of("test", Operators.CONTAINS_NOT, "test"); + parameters = new HashMap<>(); + expression = filter.getExpression(parameters); + assertEquals("test not ilike :test1", expression); + assertEquals(1, parameters.size()); + assertEquals("%test%", parameters.get("test1")); + } + + @Test + void withANDExpression_UsingEquals_GetExpression() + { + Filter filter = Filter.of("test", Operators.EQUALS, "test") + .andWhere(Filter.of("test2", Operators.EQUALS, "123")); + Map parameters = new HashMap<>(); + String expression = filter.getExpression(parameters); + assertEquals("(test = :test1 AND test2 = :test21)", expression); + assertEquals(2, parameters.size()); + assertEquals("test", parameters.get("test1")); + assertEquals("123", parameters.get("test21")); + assertEquals("(test = 'test' AND test2 = '123')", filter.getExpression()); + + parameters = new HashMap<>(); + filter.andWhere(Filter.of("test3", Operators.NOTEQUALS, "testing")); + + expression = filter.getExpression(parameters); + assertEquals("(test = :test1 AND test2 = :test21 AND test3 != :test31)", expression); + assertEquals(3, parameters.size()); + assertEquals("test", parameters.get("test1")); + assertEquals("123", parameters.get("test21")); + assertEquals("testing", parameters.get("test31")); + assertEquals("(test = 'test' AND test2 = '123' AND test3 != 'testing')", filter.getExpression()); + + filter.removeWhere("test3"); + parameters = new HashMap<>(); + expression = filter.getExpression(parameters); + assertEquals("(test = :test1 AND test2 = :test21)", expression); + assertEquals(2, parameters.size()); + assertEquals("test", parameters.get("test1")); + assertEquals("123", parameters.get("test21")); + assertEquals("(test = 'test' AND test2 = '123')", filter.getExpression()); + } + + @Test + void withORExpression_UsingEquals_GetExpression() + { + Filter filter = Filter.of("test", Operators.EQUALS, "test") + .orWhere(Filter.of("test2", Operators.EQUALS, "123")); + Map parameters = new HashMap<>(); + String expression = filter.getExpression(parameters); + assertEquals("(test = :test1 OR test2 = :test21)", expression); + assertEquals(2, parameters.size()); + assertEquals("test", parameters.get("test1")); + assertEquals("123", parameters.get("test21")); + + parameters = new HashMap<>(); + filter.orWhere(Filter.of("test3", Operators.NOTEQUALS, "testing")); + + expression = filter.getExpression(parameters); + assertEquals("(test = :test1 OR test2 = :test21 OR test3 != :test31)", expression); + assertEquals(3, parameters.size()); + assertEquals("test", parameters.get("test1")); + assertEquals("123", parameters.get("test21")); + assertEquals("testing", parameters.get("test31")); + } + + @Test + void withORAndANDExpression_UsingEquals_GetExpression() + { + Filter filter = Filter.of(Filter.of("test", Operators.EQUALS, "test") + .andWhere(Filter.of("test2", Operators.EQUALS, "123"))) + .orWhere(Filter.of("test3", Operators.NOTEQUALS, "testing")); + Map parameters = new HashMap<>(); + String expression = filter.getExpression(parameters); + + assertEquals("((test = :test1 AND test2 = :test21) OR test3 != :test31)", expression); + assertEquals(3, parameters.size()); + assertEquals("test", parameters.get("test1")); + assertEquals("123", parameters.get("test21")); + assertEquals("testing", parameters.get("test31")); + } + + @Test + void withNestedExpression_UsingEquals_GetExpression() + { + Filter filter = Filter.of("test", Operators.EQUALS, "test") + .andWhere(Filter.of("test2", Operators.EQUALS, "123") + .orWhere(Filter.of("test3", Operators.NOTEQUALS, "testing"))); + Map parameters = new HashMap<>(); + String expression = filter.getExpression(parameters); + + assertEquals("(test = :test1 AND (test2 = :test21 OR test3 != :test31))", expression); + assertEquals(3, parameters.size()); + assertEquals("test", parameters.get("test1")); + assertEquals("123", parameters.get("test21")); + assertEquals("testing", parameters.get("test31")); + } + + @Test + void withExpressionCreateFilterWithParameters() + { + Filter filter = Filter.fromExpression("((test = :t1 AND test2 = :t2) OR test3 != :t3)", + "test", "123", "testing"); + + Map parameters = new HashMap<>(); + String expression = filter.getExpression(parameters); + assertEquals("((test = :test1 AND test2 = :test21) OR test3 != :test31)", expression); + assertEquals(3, parameters.size()); + assertEquals("test", parameters.get("test1")); + assertEquals("123", parameters.get("test21")); + assertEquals("testing", parameters.get("test31")); + } + + @Test + void withExpressionCreateFilter() + { + Filter filter = Filter.fromExpression("((test = 'test' AND test2 = '123') OR test3 != 'testing')"); + + Map parameters = new HashMap<>(); + String expression = filter.getExpression(parameters); + assertEquals("((test = :test1 AND test2 = :test21) OR test3 != :test31)", expression); + assertEquals(3, parameters.size()); + assertEquals("test", parameters.get("test1")); + assertEquals("123", parameters.get("test21")); + assertEquals("testing", parameters.get("test31")); + } + + @Test + void withPlusIntervalCreateFilter() + { + Filter filter = Filter.fromExpression("test + interval '5 days'"); + + Map parameters = new HashMap<>(); + String expression = filter.getExpression(parameters); + assertEquals("test + interval '5 days'", expression); + assertEquals(0, parameters.size()); + } + + @Test + void withMinusIntervalCreateFilter() + { + Filter filter = Filter.fromExpression("test - interval '5 days'"); + + Map parameters = new HashMap<>(); + String expression = filter.getExpression(parameters); + assertEquals("test - interval '5 days'", expression); + assertEquals(0, parameters.size()); + } +} diff --git a/jpalite-repository/src/test/java/io/jpalite/repository/PageableTest.java b/jpalite-repository/src/test/java/io/jpalite/repository/PageableTest.java new file mode 100644 index 0000000..7ae264a --- /dev/null +++ b/jpalite-repository/src/test/java/io/jpalite/repository/PageableTest.java @@ -0,0 +1,79 @@ +/* + * 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.repository; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class PageableTest +{ + @Test + void getPageNumberAndSize() + { + Pageable test = Pageable.of(2, 10, Sort.by("test")); + assertEquals(10, test.getPageSize()); + assertEquals(2, test.getPageNumber()); + assertEquals("order by test ASC offset 20 limit 10", test.getExpression()); + } + + @Test + void isUnpaged() + { + Pageable test = Pageable.of(Sort.unsorted()); + assertTrue(test.isUnpaged()); + assertEquals("", test.getExpression()); + } + + @Test + void testPaging() + { + Pageable test = Pageable.of(1, 10); + assertEquals(10, test.getPageSize()); + assertEquals(1, test.getPageNumber()); + assertEquals(" offset 10 limit 10", test.getExpression()); + + test = test.next(); + assertEquals(2, test.getPageNumber()); + assertEquals(20, test.getPageIndex()); + assertEquals(" offset 20 limit 10", test.getExpression()); + + test = test.next(); + assertEquals(3, test.getPageNumber()); + assertEquals(" offset 30 limit 10", test.getExpression()); + + test = test.previous(); + assertEquals(2, test.getPageNumber()); + assertEquals(" offset 20 limit 10", test.getExpression()); + + test = test.first(); + assertEquals(0, test.getPageNumber()); + assertEquals(0, test.getPageIndex()); + assertEquals(" offset 0 limit 10", test.getExpression()); + } + + @Test + void testOfExpression() + { + Pageable test = Pageable.fromExpression("order by test asc offset 100 limit 100"); + assertEquals(1, test.getPageNumber()); + assertEquals(100, test.getPageSize()); + assertEquals("order by test ASC offset 100 limit 100", test.getExpression()); + } +} diff --git a/jpalite-repository/src/test/java/io/jpalite/repository/SortParserTest.java b/jpalite-repository/src/test/java/io/jpalite/repository/SortParserTest.java new file mode 100644 index 0000000..79f0007 --- /dev/null +++ b/jpalite-repository/src/test/java/io/jpalite/repository/SortParserTest.java @@ -0,0 +1,69 @@ +/* + * 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.repository; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class SortParserTest +{ + + @Test + void withSimpleASCOrderBy() + { + SortParser parser = new SortParser("order by t1 asc"); + assertEquals("t1 ASC", parser.getSort().getExpression()); + } + + @Test + void withSimpleDESCNullsFirstOrderBy() + { + SortParser parser = new SortParser("order by t1 asc nulls first"); + assertEquals("t1 ASC NULLS FIRST", parser.getSort().getExpression()); + } + + @Test + void withSimpleNaturalNullsFirstOrderBy() + { + SortParser parser = new SortParser("order by t1 nulls first"); + assertEquals("t1 NULLS FIRST", parser.getSort().getExpression()); + } + + @Test + void withMultipleColumnsSortAsc() + { + SortParser parser = new SortParser("order by t1 asc, t2 asc"); + assertEquals("t1 ASC, t2 ASC", parser.getSort().getExpression()); + } + + @Test + void withMultipleColumnsSortAscDesc() + { + SortParser parser = new SortParser("order by t1 asc nulls first, t2 desc nulls last"); + assertEquals("t1 ASC NULLS FIRST, t2 DESC NULLS LAST", parser.getSort().getExpression()); + } + + @Test + void withLimits() + { + SortParser parser = new SortParser("offset 5 limit 10"); + assertEquals(5, parser.getOffset()); + assertEquals(10, parser.getLimit()); + } +} diff --git a/jpalite-repository/src/test/java/io/jpalite/repository/SortTest.java b/jpalite-repository/src/test/java/io/jpalite/repository/SortTest.java new file mode 100644 index 0000000..cae0854 --- /dev/null +++ b/jpalite-repository/src/test/java/io/jpalite/repository/SortTest.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.repository; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SortTest +{ + @Test + void withUnsorted() + { + Sort sort = Sort.unsorted(); + assertTrue(sort.isUnsorted()); + assertEquals("", sort.getExpression()); + } + + @Test + void withSimpleSortByFieldExpression() + { + Sort sort = Sort.by("test"); + assertEquals("test ASC", sort.getExpression()); + } + + @Test + void withSimpleSortByField() + { + Sort sort = Sort.by("test"); + assertEquals(" order by test ASC", sort.getOrderBy()); + } + + @Test + void withSimpleSortByDirectionFields() + { + Sort sort = Sort.by(Direction.DESC, "test", "test2"); + assertEquals(" order by test DESC, test2 DESC", sort.getOrderBy()); + } + + @Test + void withSortOrderByDirectionFields() + { + Sort sort = Sort.by(new SortOrder(Direction.DESC, "test", NullHandling.FIRST)) + .and(Sort.by(new SortOrder(Direction.NATURAL, "test2", NullHandling.LAST))); + assertEquals(" order by test DESC NULLS FIRST, test2 NULLS LAST", sort.getOrderBy()); + } + + @Test + void withSortExpression() + { + Sort sort = Sort.byExpression("test ASC, test2 DESC NULLS FIRST"); + assertEquals(" order by test ASC, test2 DESC NULLS FIRST", sort.getOrderBy()); + } +} diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000..2066d75 --- /dev/null +++ b/lombok.config @@ -0,0 +1 @@ +lombok.log.fieldName=LOG diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..29b1f2c --- /dev/null +++ b/pom.xml @@ -0,0 +1,175 @@ + + + + + + 4.0.0 + + io.jpalite + jpalite-parent + 3.0.0 + pom + + JPALite Parent + Parent POM definition for JPALite + + + true + 21 + 21 + + 3.10.2 + 2.0.7 + 1.18.30 + + 3.11.0 + 3.1.2 + 3.1.2 + 3.1.1 + 3.1.1 + + + 5.9.2 + + + + jpalite-core + jpalite-repository + jpalite-maven-plugin + jpalite-quarkus-extension + + + + + + io.quarkus.platform + quarkus-bom + ${quarkus.platform.version} + pom + import + + + org.projectlombok + lombok + ${lombok.version} + provided + + + com.github.jsqlparser + jsqlparser + 4.5 + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + slf4j-ext + ${slf4j.version} + + + org.slf4j + slf4j-simple + ${slf4j.version} + test + + + + + + + org.junit.jupiter + junit-jupiter + ${junit-jupiter.version} + test + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + ${maven-deploy-plugin.version} + + + + + + + release + + + Nexus + Nexus Group + https://nexus.frei.dev/repository/maven-public/ + default + + + TradeSwitch + TradeSwitch Repo + https://nexus.frei.dev/repository/tradeswitch.release/ + default + + + + + + TradeSwitch + Internal TradeSwitch Release Repository + https://nexus.frei.dev/repository/tradeswitch.release/ + + + + + + dev + + true + + + + + Nexus + Nexus Group + https://nexus.frei.dev/repository/maven-public/ + default + + + TradeSwitch + TradeSwitch Repo + https://nexus.frei.dev/repository/tradeswitch.dev/ + default + + + + + + TradeSwitch + Internal TradeSwitch Release Repository + https://nexus.frei.dev/repository/tradeswitch.dev/ + + + + + diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000..ebc500e --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,31 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# +version: "1.0" + +#Specify inspection profile for code analysis +profile: + name: qodana.starter + +#Enable inspections +#include: +# - name: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +projectJDK: 21 #(Applied in CI/CD pipeline) + +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +#bootstrap: sh ./prepare-qodana.sh + +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) + +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-jvm:latest