From 538ba78ac1dc2e9670380329ad989c9df0ab546b Mon Sep 17 00:00:00 2001 From: James Moger Date: Wed, 3 Aug 2011 22:01:42 -0400 Subject: [PATCH 1/1] Initial commit of iciql. --- .checkstyle | 10 + .classpath | 33 + .gitignore | 8 + .project | 23 + LICENSE | 201 +++ NOTICE | 62 + README.markdown | 45 + build.xml | 356 ++++ checkstyle.xml | 109 ++ docs/00_index.mkd | 65 + docs/01_model_classes.mkd | 170 ++ docs/02_table_versioning.mkd | 29 + docs/02_usage.mkd | 164 ++ docs/03_natural_syntax.mkd | 23 + docs/04_examples.mkd | 118 ++ docs/04_tools.mkd | 95 ++ docs/05_building.mkd | 33 + docs/05_javadoc.mkd | 7 + docs/05_releases.mkd | 38 + docs/resources/iciql-favicon.png | Bin 0 -> 386 bytes docs/resources/iciql.css | 152 ++ docs/resources/iciql.png | Bin 0 -> 1888 bytes docs/resources/iciql.xcf | Bin 0 -> 6543 bytes docs/resources/javadoc.css | 52 + docs/resources/markdown.css | 67 + docs/resources/prettify.css | 1 + docs/resources/prettify.js | 33 + docs/resources/site_footer.html | 7 + docs/resources/site_header.html | 32 + src/com/iciql/CompareType.java | 44 + src/com/iciql/Condition.java | 46 + src/com/iciql/ConditionAndOr.java | 37 + src/com/iciql/Constants.java | 42 + src/com/iciql/Db.java | 470 ++++++ src/com/iciql/DbInspector.java | 196 +++ src/com/iciql/DbUpgrader.java | 81 + src/com/iciql/DbVersion.java | 55 + src/com/iciql/Define.java | 88 + src/com/iciql/Filter.java | 25 + src/com/iciql/Function.java | 149 ++ src/com/iciql/Iciql.java | 383 +++++ src/com/iciql/IciqlException.java | 37 + src/com/iciql/ModelUtils.java | 324 ++++ src/com/iciql/OrderExpression.java | 55 + src/com/iciql/Query.java | 451 +++++ src/com/iciql/QueryCondition.java | 69 + src/com/iciql/QueryJoin.java | 37 + src/com/iciql/QueryJoinCondition.java | 43 + src/com/iciql/QueryWhere.java | 148 ++ src/com/iciql/RuntimeToken.java | 57 + src/com/iciql/SQLDialect.java | 236 +++ src/com/iciql/SQLStatement.java | 128 ++ src/com/iciql/SelectColumn.java | 57 + src/com/iciql/SelectTable.java | 113 ++ src/com/iciql/TableDefinition.java | 669 ++++++++ src/com/iciql/TableInspector.java | 674 ++++++++ src/com/iciql/TestCondition.java | 115 ++ src/com/iciql/Token.java | 35 + src/com/iciql/UpdateColumn.java | 35 + src/com/iciql/UpdateColumnIncrement.java | 55 + src/com/iciql/UpdateColumnSet.java | 52 + src/com/iciql/ValidationRemark.java | 131 ++ src/com/iciql/build/Build.java | 234 +++ src/com/iciql/build/BuildSite.java | 320 ++++ src/com/iciql/bytecode/And.java | 46 + src/com/iciql/bytecode/ArrayGet.java | 49 + src/com/iciql/bytecode/CaseWhen.java | 62 + src/com/iciql/bytecode/ClassReader.java | 1454 +++++++++++++++++ src/com/iciql/bytecode/Constant.java | 38 + src/com/iciql/bytecode/ConstantNumber.java | 70 + src/com/iciql/bytecode/ConstantString.java | 55 + src/com/iciql/bytecode/Function.java | 47 + src/com/iciql/bytecode/Not.java | 55 + src/com/iciql/bytecode/Null.java | 44 + src/com/iciql/bytecode/Operation.java | 111 ++ src/com/iciql/bytecode/Or.java | 47 + src/com/iciql/bytecode/Variable.java | 51 + src/com/iciql/bytecode/package.html | 25 + src/com/iciql/package.html | 25 + src/com/iciql/util/GenerateModels.java | 192 +++ src/com/iciql/util/JdbcUtils.java | 253 +++ .../iciql/util/Slf4jStatementListener.java | 94 ++ src/com/iciql/util/StatementBuilder.java | 157 ++ src/com/iciql/util/StatementLogger.java | 182 +++ src/com/iciql/util/StringUtils.java | 308 ++++ src/com/iciql/util/Utils.java | 267 +++ src/com/iciql/util/WeakIdentityHashMap.java | 243 +++ src/com/iciql/util/package.html | 25 + tests/com/iciql/test/AliasMapTest.java | 50 + tests/com/iciql/test/AnnotationsTest.java | 177 ++ tests/com/iciql/test/ClobTest.java | 112 ++ tests/com/iciql/test/ConcurrencyTest.java | 178 ++ tests/com/iciql/test/IciqlSuite.java | 36 + tests/com/iciql/test/ModelsTest.java | 181 ++ tests/com/iciql/test/RuntimeQueryTest.java | 92 ++ tests/com/iciql/test/SamplesTest.java | 406 +++++ tests/com/iciql/test/StatementLoggerTest.java | 39 + tests/com/iciql/test/UpdateTest.java | 160 ++ .../com/iciql/test/models/ComplexObject.java | 64 + tests/com/iciql/test/models/Customer.java | 49 + tests/com/iciql/test/models/Order.java | 65 + tests/com/iciql/test/models/Product.java | 82 + .../test/models/ProductAnnotationOnly.java | 94 ++ .../models/ProductInheritedAnnotation.java | 63 + .../test/models/ProductMixedAnnotation.java | 94 ++ .../test/models/ProductNoCreateTable.java | 59 + .../com/iciql/test/models/SupportedTypes.java | 133 ++ tools/ant-googlecode-0.0.3.jar | Bin 0 -> 17983 bytes 108 files changed, 13658 insertions(+) create mode 100644 .checkstyle create mode 100644 .classpath create mode 100644 .gitignore create mode 100644 .project create mode 100644 LICENSE create mode 100644 NOTICE create mode 100644 README.markdown create mode 100644 build.xml create mode 100644 checkstyle.xml create mode 100644 docs/00_index.mkd create mode 100644 docs/01_model_classes.mkd create mode 100644 docs/02_table_versioning.mkd create mode 100644 docs/02_usage.mkd create mode 100644 docs/03_natural_syntax.mkd create mode 100644 docs/04_examples.mkd create mode 100644 docs/04_tools.mkd create mode 100644 docs/05_building.mkd create mode 100644 docs/05_javadoc.mkd create mode 100644 docs/05_releases.mkd create mode 100644 docs/resources/iciql-favicon.png create mode 100644 docs/resources/iciql.css create mode 100644 docs/resources/iciql.png create mode 100644 docs/resources/iciql.xcf create mode 100644 docs/resources/javadoc.css create mode 100644 docs/resources/markdown.css create mode 100644 docs/resources/prettify.css create mode 100644 docs/resources/prettify.js create mode 100644 docs/resources/site_footer.html create mode 100644 docs/resources/site_header.html create mode 100644 src/com/iciql/CompareType.java create mode 100644 src/com/iciql/Condition.java create mode 100644 src/com/iciql/ConditionAndOr.java create mode 100644 src/com/iciql/Constants.java create mode 100644 src/com/iciql/Db.java create mode 100644 src/com/iciql/DbInspector.java create mode 100644 src/com/iciql/DbUpgrader.java create mode 100644 src/com/iciql/DbVersion.java create mode 100644 src/com/iciql/Define.java create mode 100644 src/com/iciql/Filter.java create mode 100644 src/com/iciql/Function.java create mode 100644 src/com/iciql/Iciql.java create mode 100644 src/com/iciql/IciqlException.java create mode 100644 src/com/iciql/ModelUtils.java create mode 100644 src/com/iciql/OrderExpression.java create mode 100644 src/com/iciql/Query.java create mode 100644 src/com/iciql/QueryCondition.java create mode 100644 src/com/iciql/QueryJoin.java create mode 100644 src/com/iciql/QueryJoinCondition.java create mode 100644 src/com/iciql/QueryWhere.java create mode 100644 src/com/iciql/RuntimeToken.java create mode 100644 src/com/iciql/SQLDialect.java create mode 100644 src/com/iciql/SQLStatement.java create mode 100644 src/com/iciql/SelectColumn.java create mode 100644 src/com/iciql/SelectTable.java create mode 100644 src/com/iciql/TableDefinition.java create mode 100644 src/com/iciql/TableInspector.java create mode 100644 src/com/iciql/TestCondition.java create mode 100644 src/com/iciql/Token.java create mode 100644 src/com/iciql/UpdateColumn.java create mode 100644 src/com/iciql/UpdateColumnIncrement.java create mode 100644 src/com/iciql/UpdateColumnSet.java create mode 100644 src/com/iciql/ValidationRemark.java create mode 100644 src/com/iciql/build/Build.java create mode 100644 src/com/iciql/build/BuildSite.java create mode 100644 src/com/iciql/bytecode/And.java create mode 100644 src/com/iciql/bytecode/ArrayGet.java create mode 100644 src/com/iciql/bytecode/CaseWhen.java create mode 100644 src/com/iciql/bytecode/ClassReader.java create mode 100644 src/com/iciql/bytecode/Constant.java create mode 100644 src/com/iciql/bytecode/ConstantNumber.java create mode 100644 src/com/iciql/bytecode/ConstantString.java create mode 100644 src/com/iciql/bytecode/Function.java create mode 100644 src/com/iciql/bytecode/Not.java create mode 100644 src/com/iciql/bytecode/Null.java create mode 100644 src/com/iciql/bytecode/Operation.java create mode 100644 src/com/iciql/bytecode/Or.java create mode 100644 src/com/iciql/bytecode/Variable.java create mode 100644 src/com/iciql/bytecode/package.html create mode 100644 src/com/iciql/package.html create mode 100644 src/com/iciql/util/GenerateModels.java create mode 100644 src/com/iciql/util/JdbcUtils.java create mode 100644 src/com/iciql/util/Slf4jStatementListener.java create mode 100644 src/com/iciql/util/StatementBuilder.java create mode 100644 src/com/iciql/util/StatementLogger.java create mode 100644 src/com/iciql/util/StringUtils.java create mode 100644 src/com/iciql/util/Utils.java create mode 100644 src/com/iciql/util/WeakIdentityHashMap.java create mode 100644 src/com/iciql/util/package.html create mode 100644 tests/com/iciql/test/AliasMapTest.java create mode 100644 tests/com/iciql/test/AnnotationsTest.java create mode 100644 tests/com/iciql/test/ClobTest.java create mode 100644 tests/com/iciql/test/ConcurrencyTest.java create mode 100644 tests/com/iciql/test/IciqlSuite.java create mode 100644 tests/com/iciql/test/ModelsTest.java create mode 100644 tests/com/iciql/test/RuntimeQueryTest.java create mode 100644 tests/com/iciql/test/SamplesTest.java create mode 100644 tests/com/iciql/test/StatementLoggerTest.java create mode 100644 tests/com/iciql/test/UpdateTest.java create mode 100644 tests/com/iciql/test/models/ComplexObject.java create mode 100644 tests/com/iciql/test/models/Customer.java create mode 100644 tests/com/iciql/test/models/Order.java create mode 100644 tests/com/iciql/test/models/Product.java create mode 100644 tests/com/iciql/test/models/ProductAnnotationOnly.java create mode 100644 tests/com/iciql/test/models/ProductInheritedAnnotation.java create mode 100644 tests/com/iciql/test/models/ProductMixedAnnotation.java create mode 100644 tests/com/iciql/test/models/ProductNoCreateTable.java create mode 100644 tests/com/iciql/test/models/SupportedTypes.java create mode 100644 tools/ant-googlecode-0.0.3.jar diff --git a/.checkstyle b/.checkstyle new file mode 100644 index 0000000..3eb52c0 --- /dev/null +++ b/.checkstyle @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..7e65762 --- /dev/null +++ b/.classpath @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bed9c05 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/build +/doclava +/ext +/javadoc +/site +/*.zip +/*.jar +/build.properties diff --git a/.project b/.project new file mode 100644 index 0000000..f2a8359 --- /dev/null +++ b/.project @@ -0,0 +1,23 @@ + + + iciql + + + + + + org.eclipse.jdt.core.javabuilder + + + + + net.sf.eclipsecs.core.CheckstyleBuilder + + + + + + org.eclipse.jdt.core.javanature + net.sf.eclipsecs.core.CheckstyleNature + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..753842b --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + 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. \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..8b6516c --- /dev/null +++ b/NOTICE @@ -0,0 +1,62 @@ +iciql +Copyright 2004-2011 H2 Group +Copyright 2011 James Moger + +This is an aggregated NOTICE file for the projects included +in this distribution or linked to by this distribution. + +--------------------------------------------------------------------------- +google-code-prettify +--------------------------------------------------------------------------- + google-code-prettify, released under the + Apache Software License, Version 2.0. + + http://code.google.com/p/google-code-prettify + +--------------------------------------------------------------------------- +JUnit +--------------------------------------------------------------------------- + JUnit, released under the + Common Public License. + + http://junit.org + +--------------------------------------------------------------------------- +ant-googlecode +--------------------------------------------------------------------------- + ant-googlecode, released under the + New BSD License + + http://code.google.com/p/ant-googlecode + +--------------------------------------------------------------------------- +H2 Database +--------------------------------------------------------------------------- + h2database, released under the + Eclipse Public License, Version 1.0. + + http://code.google.com/p/h2database + +--------------------------------------------------------------------------- +MarkdownPapers +--------------------------------------------------------------------------- + MarkdownPapers, released under the + Apache Software License, Version 2.0. + + http://markdown.tautua.org + +--------------------------------------------------------------------------- +doclava +--------------------------------------------------------------------------- + doclava, released under the + Apache Software License, Version 2.0. + + http://code.google.com/p/doclava + +--------------------------------------------------------------------------- +SLF4J +--------------------------------------------------------------------------- + SLF4J, released under the + MIT/X11 License. + + http://www.slf4j.org \ No newline at end of file diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..a5fc39c --- /dev/null +++ b/README.markdown @@ -0,0 +1,45 @@ +iciql +================= +iciql **is**... + +- a model-based, database access wrapper for JDBC +- for modest database schemas and basic statement generation +- for those who want to write code, instead of SQL, using IDE completion and compile-time type-safety +- small (100KB) with no runtime dependencies +- pronounced *icicle* (although it could be French: *ici ql* - here query language) +- a friendly fork of the H2 [JaQu][jaqu] project + +iciql **is not**... + +- a complete alternative to JDBC +- designed to compete with more powerful database query tools like [jOOQ][jooq] or [Querydsl][querydsl] +- designed to compete with enterprise [ORM][orm] tools like [Hibernate][hibernate] or [mybatis][mybatis] + +Supported Databases +================= +- [H2](http://h2database.com) +- Support for others is planned and should only require creating a simple "dialect" class. + +Java Runtime Requirement +================= +iciql requires a Java 6 Runtime Environment (JRE) or a Java 6 Development Kit (JDK). + +Current Release +================= +documentation @ [iciql.com](http://iciql.com) +issues, binaries, & source @ [Google Code][googlecode]
+sources @ [Github][iciqlsrc] + +License +================= +iciql is distributed under the terms of the [Apache Software Foundation license, version 2.0][apachelicense] + +[jaqu]: http://h2database.com/html/jaqu.html "H2 JaQu project" +[orm]: http://en.wikipedia.org/wiki/Object-relational_mapping "Object Relational Mapping" +[jooq]: http://jooq.sourceforge.net "jOOQ" +[querydsl]: http://source.mysema.com/display/querydsl/Querydsl "Querydsl" +[hibernate]: http://www.hibernate.org "Hibernate" +[mybatis]: http://www.mybatis.org "mybatis" +[iciqlsrc]: http://github.com/gitblit/iciql "iciql git repository" +[googlecode]: http://code.google.com/p/iciql "iciql project management" +[apachelicense]: http://www.apache.org/licenses/LICENSE-2.0 "Apache License, Version 2.0" \ No newline at end of file diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..4ab3c65 --- /dev/null +++ b/build.xml @@ -0,0 +1,356 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Building iciql binaries ${iq.version} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Building iciql website ${iq.version} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Uploading iciql ${iq.version} binaries + + + + + + + + + + + Uploading iciql ${iq.version} website + + + + + + + + + + + + + diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..49b091b --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/00_index.mkd b/docs/00_index.mkd new file mode 100644 index 0000000..3371ce2 --- /dev/null +++ b/docs/00_index.mkd @@ -0,0 +1,65 @@ +## Overview + +iciql **is**... + +- a model-based, database access wrapper for JDBC +- for modest database schemas and basic statement generation +- for those who want to write code, instead of SQL, using IDE completion and compile-time type-safety +- small (100KB) with no runtime dependencies +- pronounced *icicle* (although it could be French: *ici ql* - here query language) +- a friendly fork of the H2 [JaQu][jaqu] project + +iciql **is not**... + +- a complete alternative to JDBC +- designed to compete with more powerful database query tools like [jOOQ][jooq] or [Querydsl][querydsl] +- designed to compete with enterprise [ORM][orm] tools like [Hibernate][hibernate] or [mybatis][mybatis] + +### Example Usage + + + + + + + +
iciqlsql
+%BEGINCODE% +Product p = new Product(); +List<Product> restock = db.from(p).where(p.unitsInStock).is(0).select(); +List<Product> all = db.executeQuery(Product.class, "select * from products"); +%ENDCODE% + +
+select * from products p where p.unitsInStock = 0
+select * from products +
+ +### Supported Databases +[H2](http://h2database.com) + +Support for others is planned and should only require creating a simple "dialect" class. + +### Java Runtime Requirement + +iciql requires a Java 6 Runtime Environment (JRE) or a Java 6 Development Kit (JDK). + +### Current Release + +**%VERSION%** ([zip](http://code.google.com/p/iciql/downloads/detail?name=%ZIP%)|[jar](http://code.google.com/p/iciql/downloads/detail?name=%JAR%))   *released %BUILDDATE%* + +issues, binaries, & source @ [Google Code][googlecode]
+sources @ [Github][github] + +### License +iciql is distributed under the terms of the [Apache Software Foundation license, version 2.0][apachelicense] + +[jaqu]: http://h2database.com/html/jaqu.html "H2 JaQu project" +[orm]: http://en.wikipedia.org/wiki/Object-relational_mapping "Object Relational Mapping" +[jooq]: http://jooq.sourceforge.net "jOOQ" +[querydsl]: http://source.mysema.com/display/querydsl/Querydsl "Querydsl" +[hibernate]: http://www.hibernate.org "Hibernate" +[mybatis]: http://www.mybatis.org "mybatis" +[github]: http://github.com/gitblit/iciql "iciql git repository" +[googlecode]: http://code.google.com/p/iciql "iciql project management" +[apachelicense]: http://www.apache.org/licenses/LICENSE-2.0 "Apache License, Version 2.0" \ No newline at end of file diff --git a/docs/01_model_classes.mkd b/docs/01_model_classes.mkd new file mode 100644 index 0000000..3ca7630 --- /dev/null +++ b/docs/01_model_classes.mkd @@ -0,0 +1,170 @@ +## Model Classes +A model class represents a single table within your database. Fields within your model class represent columns in the table. The object types of your fields are reflectively mapped to SQL types by iciql at runtime. + +Models can be manually written using one of two approaches: *annotation configuration* or *interface configuration*. Both approaches can be used within a project and both can be used within a single model class, although that is discouraged. + +Alternatively, model classes can be automatically generated by iciql using the model generation tool. Please see the [tools](tools.html) page for details. + +### Supported Data Types + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
java.lang.StringVARCHAR *(maxLength > 0)* or TEXT *(maxLength == 0)*
java.lang.BooleanBIT
java.lang.ByteTINYINT
java.lang.ShortSMALLINT
java.lang.IntegerINT
java.lang.LongBIGINT
java.lang.FloatREAL
java.lang.DoubleDOUBLE
java.math.BigDecimalDECIMAL
java.sql.DateDATE
java.sql.TimeTIME
java.sql.TimestampTIMESTAMP
java.util.DateTIMESTAMP
+ +**NOTE:**
+The reverse lookup used for model generation, SQL type -> Java type, contains more mappings.
+Please consult the `com.iciql.ModelUtils` class for details. + +### Unsupported Types +- Java primitives (use their object counterparts instead) +- binary types (BLOB, etc) +- array types +- custom types + +### Configuration Rules +1. field mappings must be Objects not primitives +2. the model class must have a default public constructor + +### Configuration Limitations +1. index names can not be specified +2. triggers, views, and other advanced database features are unimplemented + +## Annotation Configuration +The recommended approach to setup a model class is to annotate the class and field declarations. + +### advantages + +- annotated fields may have any scope +- annotated models support annotated field inheritance making it possible to design a single base class that defines the fields and then create table subclasses that specify the table mappings. +- model runtime dependency is limited to the small, portable `com.iciql.Iciql` class file which contains the annotation definitions + +### disadvantages + +- more verbose model classes +- indexes are defined using "fragile" string column names +- compound primary keys are defined using "fragile" string column names + +### Example Annotated Model +%BEGINCODE% +import com.iciql.Iciql.IQColumn; +import com.iciql.Iciql.IQIndex; +import com.iciql.Iciql.IQTable; + +@IQTable +@IQIndex(standard = {"productName", "category"}) +public class Product { + + @IQColumn(primaryKey = true) + public Integer productId; + + @IQColumn(maxLength = 200, trimString = true) + public String productName; + + @IQColumn(maxLength = 50, trimString = true) + public String category; + + @IQColumn + public Double unitPrice; + + @IQColumn(name = "units") + public Integer unitsInStock; + + @IQColumn + private Integer reorderQuantity; + + public Product() { + // default constructor + } +} +%ENDCODE% + +## Interface Configuration (deprecated) +Alternatively, you may map your model classes using the original JaQu interface approach by implementing the `com.iciql.Iciql` interface. + +This is a less verbose configuration style, but it comes at the expense of introducing a compile-time dependency on the logic of the iciql library. This might be a deterrent, for example, if you were serializing your model classes to another process that may not have the iciql library. + +The `com.iciql.Iciql` interface specifies a single method, *defineIQ()*. In your implementation of *defineIQ()* you would use static method calls to set: + +- the table name (if it's not the class name) +- the column name (if it's not the field name) +- the max length of a string field +- the primaryKey (single field or compound) +- any indexes (single field or compound) + +### advantages + +- less verbose model class +- compile-time index definitions +- compile-time compound primary key definitions + +### disadvantages + +- only **public** fields of the model class are reflectively mapped as columns. all other scoped fields and inherited fields are ignored. +- model runtime dependency on entire iciql library +- *defineIQ()* is called from a static synchronized block which may be a bottleneck for highly concurrent systems + +### Example Interface Model +%BEGINCODE% +import com.iciql.Iciql; + +public class Product implements Iciql { + public Integer productId; + public String productName; + public String category; + public Double unitPrice; + public Integer unitsInStock; + + // this field is ignored because it is not public + Integer reorderQuantity; + + public Product() { + } + + @Override + public void defineIQ() { + com.iciql.Define.primaryKey(productId); + com.iciql.Define.columnName(unitsInStock, "units"); + com.iciql.Define.maxLength(productName, 200); + com.iciql.Define.maxLength(category, 50); + com.iciql.Define.index(productName, category); + } +} +%ENDCODE% \ No newline at end of file diff --git a/docs/02_table_versioning.mkd b/docs/02_table_versioning.mkd new file mode 100644 index 0000000..0c778cb --- /dev/null +++ b/docs/02_table_versioning.mkd @@ -0,0 +1,29 @@ +## Database & Table Versioning + +Iciql supports an optional, simple versioning mechanism. There are two parts to the mechanism. + +1. You must supply an implementation of `com.iciql.DbUpgrader` to your `com.iciql.Db` instance. +2. One or more of your table model classes must set the *version* field of the `com.iciql.IQTable` annotation
+AND/OR
+Your `com.iciql.DbUpgrader` implementation must set the *version* field of the `com.iciql.IQDatabase` annotation + +### How does it work? +If you choose to use versioning, iciql will maintain a table within your database named *_iq_versions* which is defined as: + + CREATE TABLE _IQ_VERSIONS(SCHEMANAME TEXT NOT NULL, TABLENAME TEXT NOT NULL, VERSION INT NOT NULL) + +This database table is automatically created if and only if at least one of your model classes specifies a *version* > 0. + +When you generate a statement, iciql will compare the annotated version field of your model class to its last known value in the *_iq_versions* table. If *_iq_versions* lags behind the model annotation, iciql will immediately call the registered `com.iciql.DbUpgrader` implementation before generating and executing the current statement. + +When an upgrade scenario is identified, the current version and the annotated version information is passed to either: + +- `DbUpgrader.upgradeDatabase(db, fromVersion, toVersion)` +- `DbUpgrader.upgradeTable(db, schema, table, fromVersion, toVersion)` + +both of which allow for non-linear upgrades. If the upgrade method call is successful and returns *true*, iciql will update the *_iq_versions* table with the annotated version number. + +The actual upgrade procedure is beyond the scope of iciql and is your responsibility to implement. This is simply a mechanism to automatically identify when an upgrade is necessary. + +**NOTE:**
+The database entry of the *_iq_versions* table is specified as SCHEMANAME='' and TABLENAME=''. \ No newline at end of file diff --git a/docs/02_usage.mkd b/docs/02_usage.mkd new file mode 100644 index 0000000..d872ee0 --- /dev/null +++ b/docs/02_usage.mkd @@ -0,0 +1,164 @@ +## Usage + +Aside from this brief usage guide, please consult the [examples](examples.html), the [javadoc](javadoc.html) and the [source code](https://code.google.com/p/iciql/source/browse). + +### Instantiating a Db + +Use one of the static utility methods to instantiate a Db instance: + + Db.open(String url, String user, String password); + Db.open(String url, String user, char[] password); + Db.open(Connection conn); + Db.open(DataSource dataSource); + +### Compile-Time Statements + +You compose your statements using the builder pattern where each method call returns an object that is used to specify the next part of the statement. Through clever usage of generics, pioneered by the original JaQu project, compile-time safety flows through the statement. + +%BEGINCODE% +Db db = Db.open("jdbc:h2:mem:", "sa", "sa"); +db.insertAll(Product.getList()); +db.insertAll(Customer.getList()); + +List<Product> restock = + db.from(p). + where(p.unitsInStock). + is(0).orderBy(p.productId).select(); + +for (Product product : restock) { + db.from(p). + set(p.unitsInStock).to(25). + where(p.productId).is(product.productId).update(); +} +db.close(); +%ENDCODE% + +Please see the [examples](examples.html) page for more code samples. + +### Dynamic Runtime Queries + +Iciql gives you compile-time type-safety, but it becomes inconvenient if your design requires more dynamic statement generation. For these scenarios iciql offers some runtime query support through a hybrid approach or a pure JDBC approach. + +#### Where String Fragment Approach +This approach is a mixture of iciql and jdbc. It uses the traditional prepared statement *field=?* tokens with iciql compile-time model class type checking. There is no field token type-safety checking. +%BEGINCODE% +List<Product> restock = db.from(p).where("unitsInStock=? and productName like ? order by productId", 0, "Chef%").select(); +%ENDCODE% + +#### Db.executeQuery Approaches +There may be times when the hybrid approach is still too restrictive and you'd prefer to write straight SQL. You can do that too and use iciql to build objects from your ResultSet, but be careful: + +1. Make sure to _select *_ in your query otherwise db.bindResultSet() will throw a RuntimeException +2. There is no model class type checking nor field type checking. + +%BEGINCODE% +List<Product> allProducts = db.executeQuery(Product.class, "select * from products"); +List<Product> restock = db.executeQuery(Product.class, "select * from products where unitsInStock=?", 0); +%ENDCODE% + +Or if you want access to the raw *ResultSet* before building your model object instances... + +%BEGINCODE% +ResultSet rs = db.executeQuery("select * from products"); +List<Product> allProducts = db.buildObjects(Product.class, rs); +// This method ensures the creating statement is closed +JdbcUtils.closeSilently(rs, true); +%ENDCODE% + +### JDBC Statements, ResultSets, and Exception Handling + +Iciql opens and closes all JDBC objects automatically. SQLExceptions thrown during execution of a statement (except for *close()* calls), will be caught, wrapped, and rethrown as an `IciqlException`, which is a RuntimeException. + +Iciql does not throw any [checked exceptions](http://en.wikipedia.org/wiki/Exception_handling#Checked_exceptions). + +### Statement Logging + +Iciql provides a mechanism to log generated statements to the console, to SLF4J, or to your own logging framework. Exceptions are not logged using this mechanism; exceptions are wrapped and rethrown as `IciqlException`, which is a RuntimeException. + +#### Console Logging +%BEGINCODE% +StatmentLogger.activeConsoleLogger(); +StatmentLogger.deactiveConsoleLogger(); +%ENDCODE% + +#### SLF4J Logging +%BEGINCODE% +Slf4jStatementListener slf4j = new Slf4jStatementListener(); +slf4j.setLevel(StatementType.CREATE, Level.WARN); +slf4j.setLevel(StatementType.DELETE, Level.WARN); +slf4j.setLevel(StatementType.MERGE, Level.OFF); +StatmentLogger.registerListener(slf4j); +StatmentLogger.unregisterListener(slf4j); +%ENDCODE% + +#### Custom Logging +%BEGINCODE% +StatementListener custom = new StatementListener() { + public void logStatement(StatementType type, String statement) { + // do log + } +}; +StatmentLogger.registerListener(custom); +StatmentLogger.unregisterListener(custom); +%ENDCODE% + +## Understanding Aliases and Model Classes +Consider the following example: +%BEGINCODE% +Product p = new Product(); +List<Product> restock = db.from(p).where(p.unitsInStock).is(0).select(); +%ENDCODE% + +The Product model class instance named **p** is an *alias* object. An *alias* is simply an instance of your model class that is only used to build the compile-time/runtime representation of your table. + +1. *Alias* instances are **NOT** thread-safe and must not be used concurrently. +2. *Alias* instances have no other purpose than to provide a compile-time/runtime map of your table. +3. If you inspected an *alias* instance after using one you would find that it's fields have been assigned numeric values.
These values have no meaning. They are assigned from a static counter in `com.iciql.Utils.newObject()` during execution of the *db.from()* method. + +If your statement is a query, like in the above example, iciql will generate new instances of your *alias* model class and return them as a list where each entry of the list represents a row from the JDBC `ResultSet`. + +### Why are Aliases not thread-safe? + +The _db.from(p)_ call reinstantiates each member field of p. Those reinstantiated fields are then subsequently used in clauses like _where(p.unitsInStock)_. If your *alias* instance is shared concurrently then its highly probable that when _queryA_ executes, _queryC_ has reinstantiated all the *alias* fields and broken _queryA's_ runtime field mapping. + +Depending on your design, you might consider using a [ThreadLocal](http://download.oracle.com/javase/6/docs/api/java/lang/ThreadLocal.html) variable if you do not want to keep instantiating *alias* instances. A utility function is included for easily creating ThreadLocal variables. + +%BEGINCODE% +final ThreadLocal<Product> p = Utils.newThreadLocal(Product.class); +db.from(p.get()).select(); +%ENDCODE% + +## Best Practices + +1. Close your *Db* instances when you are done with them, this closes the underlying connection or directs the pool to "close" the connection. +2. Aliases instances are not thread-safe so DO NOT SHARE an alias!
Consider using a *ThreadLocal* alias instance with the `com.iciql.Utils.newThreadLocal()` utility method. + + + +
Not Thread-SafeThread-Safe
+%BEGINCODE% +final Product p = new Product(); +for (int i = 0; i < 5; i++) { + Thread thread = new Thread(new Runnable() { + public void run() { + // from(p) reinstantiates p's fields + db.from(p).select(); + } + }, "Thread-" + i); + thread.start(); +} +%ENDCODE% + +%BEGINCODE% +final ThreadLocal<Product> p = Utils.newThreadLocal(Product.class); +for (int i = 0; i < 5; i++) { + Thread thread = new Thread(new Runnable() { + public void run() { + // p.get() returns a Product instance unique to this thread + db.from(p.get()).select(); + } + }, "Thread-" + i); + thread.start(); +} +%ENDCODE% +
\ No newline at end of file diff --git a/docs/03_natural_syntax.mkd b/docs/03_natural_syntax.mkd new file mode 100644 index 0000000..a91c5b8 --- /dev/null +++ b/docs/03_natural_syntax.mkd @@ -0,0 +1,23 @@ +## Natural Syntax + +**work-in-progress** + +The original JaQu source offers partial support for Java expressions in *where* clauses. + +This works by decompiling a Java expression, at runtime, to an SQL condition. The expression is written as an anonymous inner class implementation of the `com.iciql.Filter` interface. +A proof-of-concept decompiler is included, but is incomplete. + +The proposed syntax is: +%BEGINCODE% +long count = db.from(co). + where(new Filter() { public boolean where() { + return co.id == x + && co.name.equals(name) + && co.value == new BigDecimal("1") + && co.amount == 1L + && co.birthday.before(new java.util.Date()) + && co.created.before(java.sql.Timestamp.valueOf("2005-05-05 05:05:05")) + && co.time.before(java.sql.Time.valueOf("23:23:23")); + } + }).selectCount(); +%ENDCODE% \ No newline at end of file diff --git a/docs/04_examples.mkd b/docs/04_examples.mkd new file mode 100644 index 0000000..d4f98da --- /dev/null +++ b/docs/04_examples.mkd @@ -0,0 +1,118 @@ +## Select Statements + +%BEGINCODE% +// select * from products +List<Product> allProducts = db.from(p).select(); + +// select * from customers where region='WA' +Customer c = new Customer(); +List<Customer> waCustomers = db.from(c). where(c.region).is("WA").select(); + +// select with generation of new anonymous inner class +List<ProductPrice> productPrices = + db.from(p). + orderBy(p.productId). + select(new ProductPrice() {{ + productName = p.productName; + category = p.category; + price = p.unitPrice; + }}); +%ENDCODE% + +## Insert Statements + +%BEGINCODE% +// single record insertion +db.insert(singleProduct); + +// single record insertion with primary key retrieval +Long key = db.insertAndGetKey(singleProduct); + +// batch record insertion +db.insertAll(myProducts); + +// batch insertion with primary key retrieval +List<Long> myKeys = db.insertAllAndGetKeys(list); +%ENDCODE% + +## Update Statements + +%BEGINCODE% +// single record update +db.update(singleProduct); + +// batch record updates +db.updateAll(myProducts); + +// update query +db.from(p).set(p.productName).to("updated") + .increment(p.unitPrice).by(3.14) + .increment(p.unitsInStock).by(2) + .where(p.productId).is(1).update(); +%ENDCODE% + +## Merge Statements +Merge statements currently generate the [H2 merge syntax](http://h2database.com/html/grammar.html#merge). + +%BEGINCODE% +Product pChang = db.from(p).where(p.productName).is("Chang").selectFirst(); +pChang.unitPrice = 19.5; +pChang.unitsInStock = 16; +db.merge(pChang); +%ENDCODE% + +## Delete Statements + +%BEGINCODE% +// single record deletion +db.delete(singleProduct); + +// batch record deletion +db.deleteAll(myProducts); + +// delete query +db.from(p).where(p.productId).atLeast(10).delete(); + +%ENDCODE% + +## Inner Join Statements + +%BEGINCODE% +final Customer c = new Customer(); +final Order o = new Order(); +List<CustOrder> orders = + db.from(c). + innerJoin(o).on(c.customerId).is(o.customerId). + where(o.total).lessThan(new BigDecimal("500.00")). + orderBy(1). + select(new CustOrder() {{ + customerId = c.customerId; + orderId = o.orderId; + total = o.total; + }}); +%ENDCODE% + +## Dynamic Queries + +Dynamic queries skip all field type checking and, depending on which approach you use, may skip model class/table name checking too. + +%BEGINCODE% +// where fragment with object parameters +List<Product> restock = db.from(p).where("unitsInStock=? and productName like ? order by productId", 0, "Chef%").select(); + +// statement with binding to your model class +List<Product> allProducts = db.executeQuery(Product.class, "select * from products"); + +// statement with object parameters and binding to your model class +List<Product> restock = db.executeQuery(Product.class, "select * from products where unitsInStock=?", 0); + +/** + * If you want to process the intermediate ResultSet + * yourself make sure to use the closeSilently() method + * to ensure the parent statement is closed too. + */ +ResultSet rs = db.executeQuery("select * from products"); +List<Product> allProducts = db.buildObjects(Product.class, rs); +JdbcUtils.closeSilently(rs, true); + +%ENDCODE% \ No newline at end of file diff --git a/docs/04_tools.mkd b/docs/04_tools.mkd new file mode 100644 index 0000000..569689e --- /dev/null +++ b/docs/04_tools.mkd @@ -0,0 +1,95 @@ +## Model Generation +If you do not have or do not want to annotate your existing model classes, you can use the included model generation tool to create iciql model classes. + + java -jar iciql.jar + +### Parameters + + + + + + + + + + +
-urlJDBC url for the database*REQUIRED*
-usernameusername for JDBC connection*optional*
-passwordpassword for JDBC connection*optional*
-schemathe target schema for model generation*default:* all schemas
-tablethe target table for model generation*default:* all tables
-packagethe destination package name for generated models*default:* default package
-folderthe output folder for .java files*default:* current folder
-annotateSchemainclude the schema name in the class annotations*default:* true
-trimStringsannotate trimStrings=true for any VARCHAR string mappings   *default:* false
+ +## Model Validation +Iciql can validate your model classes against your database to ensure that your models are optimally defined and are consistent with the current table and index definitions. + +Each `com.iciql.ValidationRemark` returned by the validation has an associated level from the following enum: +%BEGINCODE% +public static enum Level { + CONSIDER, WARN, ERROR; +} +%ENDCODE% + +A typical validation may output recommendations for adjusting a model field annotation such as setting the *maxLength* of a string to match the length of its linked VARCHAR column. + +### Sample Model Validation using JUnit 4 +%BEGINCODE% +import static org.junit.Assert.assertTrue; + +import java.sql.SQLException; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ErrorCollector; + +import com.iciql.Db; +import com.iciql.DbInspector; +import com.iciql.ValidationRemark; +import com.iciql.test.models.Product; +import com.iciql.test.models.ProductAnnotationOnly; +import com.iciql.test.models.ProductMixedAnnotation; + +public class ValidateModels { + + /* + * The ErrorCollector Rule allows execution of a test to continue after the + * first problem is found and report them all at once + */ + @Rule + public ErrorCollector errorCollector = new ErrorCollector(); + + private Db db; + + @Before + public void setUp() { + db = Db.open("jdbc:h2:mem:", "sa", "sa"); + } + + @After + public void tearDown() { + db.close(); + } + + @Test + public void testValidateModels() { + DbInspector inspector = new DbInspector(db); + validateModel(inspector, new Product()); + validateModel(inspector, new ProductAnnotationOnly()); + validateModel(inspector, new ProductMixedAnnotation()); + } + + private void validateModel(DbInspector inspector, Object o) { + List<ValidationRemark> remarks = inspector.validateModel(o, false); + assertTrue("Validation remarks are null for " + o.getClass().getName(), remarks != null); + log("Validation remarks for " + o.getClass().getName()); + for (ValidationRemark remark : remarks) { + log(remark.toString()); + if (remark.isError()) { + errorCollector.addError(new SQLException(remark.toString())); + } + } + } + + private void log(String message) { + System.out.println(message); + } +} +%ENDCODE% \ No newline at end of file diff --git a/docs/05_building.mkd b/docs/05_building.mkd new file mode 100644 index 0000000..cb8381e --- /dev/null +++ b/docs/05_building.mkd @@ -0,0 +1,33 @@ +## Building from Source + +[Eclipse](http://eclipse.org) is recommended for development as the project settings are preconfigured. + +Additionally, [eclipse-cs](http://eclipse-cs.sourceforge.net), [FindBugs](http://findbugs.sourceforge.net), and [EclEmma](http://www.eclemma.org) are recommended development tools. + +### Build Dependencies (bundled) +- [google-code-prettify](http://code.google.com/p/google-code-prettify) (Apache 2.0) + +### Build Dependencies (downloaded during build) +- [H2 Database](http://h2database.com) (Eclipse Public License 1.0) +- [JUnit](http://junit.org) (Common Public License) +- [commons-net](http://commons.apache.org/net) (Apache 2.0) +- [ant-googlecode](http://code.google.com/p/ant-googlecode) (New BSD) +- [MarkdownPapers](http://markdown.tautua.org) (Apache 2.0) +- [doclava](http://code.google.com/p/doclava) (Apache 2.0) +- [SLF4J](http://www.slf4j.org) (MIT/X11) + +### Instructions +1. Clone the git repository from [GoogleCode][iciqlsrc]. +2. Import the iciql project into your Eclipse workspace.
+*There will be some build errors.* +3. Using Ant, execute the `build.xml` script in the project root.
+*This will download all necessary build dependencies.* +4. Select your iciql project root and **Refresh** the project, this should correct all build problems. + +## Contributing +Patches welcome in any form. + +Contributions must be your own original work and must licensed under the [Apache License, Version 2.0][apachelicense], the same license used by iciql. + +[iciqlsrc]: https://code.google.com/p/iciql/ "iciql git repository" +[apachelicense]: http://www.apache.org/licenses/LICENSE-2.0 "Apache License, Version 2.0" \ No newline at end of file diff --git a/docs/05_javadoc.mkd b/docs/05_javadoc.mkd new file mode 100644 index 0000000..c2e911d --- /dev/null +++ b/docs/05_javadoc.mkd @@ -0,0 +1,7 @@ +
+packages + | tree + | deprecated + | index +
+ diff --git a/docs/05_releases.mkd b/docs/05_releases.mkd new file mode 100644 index 0000000..9f19d24 --- /dev/null +++ b/docs/05_releases.mkd @@ -0,0 +1,38 @@ +## Release History + +### Current Release +**%VERSION%** ([zip](http://code.google.com/p/iciql/downloads/detail?name=%ZIP%)|[jar](http://code.google.com/p/iciql/downloads/detail?name=%JAR%))   *released %BUILDDATE%* + +- initial release (API v1) + +*API changes compared to JaQu from H2 1.3.157 sources* + +- deprecated model class interface configuration +- added *Db.open(Connection conn)* method, changed constructor to default scope +- added *Db.registerDialect* static methods to register custom dialects +- added *Query.where(String fragment, Object... args)* method to build a runtime query fragment when compile-time queries are too strict +- added *Db.executeQuery(String query, Object... args)* to execute a complete sql query with optional arguments +- added *Db.executeQuery(Class modelClass, String query, Object... args)* to execute a complete sql query, with optional arguments, and build objects from the result +- added *Db.buildObjects(Class modelClass, ResultSet rs)* method to build objects from the ResultSet of a plain sql query +- added *ThreadLocal<T> com.iciql.Utils.newThreadLocal(final Class<? extends T> clazz)* method +- added optional console statement logger and SLF4J statement logger +- throw *IciqlException* (which is a RuntimeException) instead of RuntimeException +- synchronized *Db.classMap* for concurrent sharing of a Db instance +- Database/table versioning uses the _iq_versions table, the _ jq_versions table, if present, is ignored +- Changed the following class names: + - org.h2.jaqu.Table => com.iciql.Iciql + - org.h2.jaqu.JQSchema => com.iciql.IQSchema + - org.h2.jaqu.JQDatabase => com.iciql.IQDatabase + - org.h2.jaqu.JQIndex => com.iciql.IQIndex + - org.h2.jaqu.JQTable => com.iciql.IQTable + - org.h2.jaqu.JQColumn => com.iciql.IQColumn +- Changed the following method names: + - org.h2.jaqu.Table.define() => com.iciql.Iciql.defineIQ() + - QueryConditon.bigger => QueryCondition.exceeds + - QueryConditon.biggerEqual => QueryCondition.atLeast + - QueryConditon.smaller => QueryCondition.lessThan + - QueryConditon.smallEqual => QueryCondition.atMost + +### Older Releases + +none \ No newline at end of file diff --git a/docs/resources/iciql-favicon.png b/docs/resources/iciql-favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..dda916bf0410491f71c1a1d5571e8fdc97bcb85c GIT binary patch literal 386 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|*pj^6U4S$Y z{B+)352QE?JR*x37`TN&n2}-D90{Nxdx@v7EBkGBUVafXIeqs6peanAE{-7s4vM|VuHK6_?vNuo_x;^`*l6$$}*Ig_`!nk<}rE9CG0)w@1v zyRvvRSI=4KI(c!;Z<**l?&*A*N=pthAKTG+?&5T|4=uhW?4y~;+E$O&zerK%#AKPHTU=-m8$9YkG)&F#NWrzGdO?u z!4+!G=X$ehJ|EvSEpvlSo2ANxzRYW#-xxHyTx0)+X{+A|&%3?!-YfUl=kKgLwmntJ zYO_hilU)7oiO!qOy_-FiiFeygk$9!+6^FIsiuOFxz3!!)>%XmN^7^diPx#24YJ`L;wH)0002_L%V+f000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipe? z4+bQaVn*u#000?uMObu0Z*6U5Zgc=ca%Ew3Wn>_CX>@2HM@dakSAh-}000KNNklv^L?wYZ2`2V~ zM$`leF)A8}_y`23MifjDjYw1?nqaj+A}_J1fIRvfKkT{f+;wMn@9o`NAw9`tw|8gH zf95|k=bSk^g?{OmBt?Zj)n+|V45Xy^rAtZ&TFuF+1%<%KBbEwLz6mIa4FK(ERhwl8 zlE=t1mU1X>07`*Uw3-&9F^xbQ(1unuF*}evMgmyMF%o4!8Cp%vqEXv`R-iR<78C*_ zAuJWJ#>;^L86%<1FU~2fRi$ra&H`tg_NEXR31azQDh7&u%A0`!0zA+v(h$?!Q&&yv z*3t8R5!w_bjFECsmUpaTA3&xt^hTmort^q^5j@&muV-X@I#g!?FuELs!7 zP`TI85omKL;GL8h+UQR74&$Q$-Dz)^^8MC3EC*rXp@%lcQm#TgZJEPHdy+xEIhuh)e$!v1N6@T;K-a62UqS zG|9|(7p-=o)y#}egk!8mt7oL%MBqtahUh;H>;~QjUPr4=hpvJz$sjjulQjWY16*y@ zPuq|o!1cgn;OP(>7liStMb-k~6={FF*xCWI%%=eJ(55Dru%Qq(tQ~k*$hr#n0vKvF zv?QjEFgXP*6tZpymPdpjHv@0vh!raNaJ?JQstIjMfcJzf#7n&iOb3Qb$A*b#TnK!h zp(9*AM5|+HGYD8|{oVpB04|j!dl~R3a16N1LC%K0o?)A;==(}(qypxkm483^w`K5O z2+-k@t#X|YTp;(yfyrp~t5xp+@EY1|1$IjK((Y()pUtpca-C}}kyp{GAtdeM(&3UH zwRR|-tcFjK1Zy?32Tw-b=GLE%5q=V67l>mxmH^k>m%M-pCeY{gr&ufqnZ-h z%2inR`y=Y^OQHcO`lZhZ``x;)N~pcZtoth>>W=!q5_X|nzqMkb`>gCmHac7~%re+% zjJk+->KqQCqpu08mg`z8FnkfRY>wGMd9%CQCM{Za-i zcz_E`SWxFi0~;;Us)2ieb-&ihQ-0S6>Xju(w58ntHe8}WQKh$WKNga7)N(W zzHL~Xe0!Tk+9PPSN1VXgNbOz%F)qe@v;kNm19N|`)<)F#gBXP6p40bYq|v};`M|nK zG7|pJVMFm!)RSd-RAqtDCL8c6XtP4>qaFX10Ta<`XT%`I!3yLgoG`5_yU!YUX=49J zg;u;HL6vAz1uO%`0X4vE!as_pKT5P&4UWQ?2RthS^J0kpsL-J`63fAf--dMn9dc?V zP1+dJCs@2RaJrlU&dPJps!2T16Owbd@1E7!Zp_k+_J#02XKJRepZ zU2c%r$6KBm zVMu<@xJK6}#Ae39AW(mLP`w#ej#d|k^y?7b0l6_2Il=u2IOUiMak;g!uH%+^;B(-& z0Ndg!N3z6d5dEj5oN;<}mv{6WjsKnhVmXe4D$Zr`RzW#h&Ggx^prZ;sUOCgSZwVvl zd`lqa?$sS$FiI5Xx)kz6_IW{=*ZG2-`M1w5nDbz!CH`L}Dxt=wA~wBU3E>v*|CahC ap8N}bxmkWTel%PF0000%U!-q)8LUskDVQbrL&q9+HAchzY1^)KsF11ZrrJT*uyQ;-BCj zk`#o4ct{Kq@K7pVNb^=f5en_b+F%(oZVC?-RfMXd+Fc>MsTVnZh>ab4XLk4UojWtT zGu|}`sF6VAN_Xep@1Api&N*|>Hye$0osOJs>xgv3I@<~52x>DoP}ZU<3Vt3!tvvV> zBCaUwQFYW$qdx4kS#AU558)Y(V%ny*p02L;?tb)%2iJY6x4pH${dnX|tp7yh(BWsl z!Xs;>XsoL>+TIlHjU8tU(h4>7^`Gl(kM#9)#*S~RZs>@%^>p_1MjGU!sW_Oxp}?U1J(_Ow5RHqYgj3N&~PE^aiac9(zE7oE1dO~|SPIL7DwxH?2sJ;Ke_$Jz^dEjP1Sb6*I!G!zD)Is$r@xY~@ zSadva>4UcvZ<+71`sChHQpE<;jeKubt=ieo!z+GRI!KG^(bqp(vS$BqaW`oaxzqUC z-?H=X#558Lu1sg%0OfYv5ETGaf%Yj&=pr{B_KN6~+j(15N)hNCg>?Q6uYgALfxifm zyVSB)U-RU*-jfSzv=F)_#I2KC0*{=U6>4!wjm&QdVLeMVSQDL-Lfn2FI=O~<|E$)V zLi~tAQp$=8@iGM{|D6>w9}Cg!%G?xU8_VoiA!7+7B4?f@Au3s>>OYXd{C8dJH~u}J zYt6?GFgP+X?Rgfw`twflGv+Ng)4AKS4UWpGUv_y6| zeg&rV_G-!Gu>_X3;@3vU49&2{Mz6)GdPP4(898oXW7(@C231hj7`cis0&lO{?gc3u z9~p;dv*ROioZj1m0*?W_a?QYL^OH#}nV&=}Yh1g6eU!%w^4ZB`62iIoD61q>(`hB0 zolY^fI2woDV7M}4CXa#nI*Jx6LS=b)jA$}7gMsCl)Fjc^Ft$(*4SLZ`!XQdcEY-4I23|L&1P42e8pw z1zi(C$3kD;64U~kLUU5YMK`Is8Z4^$m&`#gj}N^KDEH^dq@J{R&wd*&!G@2$#kLKkO*o}s*kcpGGI!c)m?<{jnvIc zM<1=!6@7W#-qWKgsn3%pQ{%_BXo{8%J^2Ip%A9fZi(59Qlc~2G5p%gw_6v6}f-{r5 zOTQ&_<_UCODl5V&} z6tje>0A2vMk(g~NzZa*r5|XGXg3$iQl%xI8L-ZW-=HeStk5QO-+_&&6Wb>Z`;6Ho& z-y?d((H$bmflky1w!~(I$qVyPN%%ZXF9_3m1~2dJrybQpbOOBSG1zwDiFvs+k3iFZ z=1n9?l8V=V4%faEtRs-6v#|N%%d*cn6lZ;V*6CwY?1FpdaLGxrO1f!^23FfH`;3=S zvrMEgpW2csDb-@teNe-e?q!pHqtuaPVbUmdUZb4B>8x|SP0jv8!tejXbj)=Fl9D`4 zsT+7gn8hCfuaZsHz5ueerwQXfzQ%j-Y;P2YuLJii$i%0l%X$8>2<0=~3zhttZ|Vf#A6 zot3n+%@V_3=L=0wD|E+R89f-QBzIZbGe5olq5fg!-Cy=^qhhMtI`qzjIAix}yG-*LK#Xvf$;taYhf5z>-R26*;0pWBUVbxP5w~8COZqSesQWUSg_N0vfoYfD zXfhW!ms%ok&`$S&x$zOGd5J@FVXHR%SWgw|26r!#x3thkQf&%@k4J~4fJ`_fgo%bi zs?8)8nMqJO&5pw9kotY;{k&3dXKi@iNv1}ENCuqD=Kp_fj~#TE)4*tR>|o|Z1!G#w zqmqh;v4NR6ERS8G7JQ>oW-O6l%88FfXjM`UUa6#foIc0Iv>b0{JY4)M`!l;4xEJ<( zKmN?-KfIk%x7}&Dv>PO|?jY*-{dnD1ft_J&u;z;*pBIBPDpqN#_7}t;o{%qC=0;)r[j]=l[j];r.sort(function(q,m){return q.name"); +for(l=b.firstChild;l;l=l.nextSibling)H(l,f,i);if(b.firstChild||!/^(?:br|link|img)$/.test(o))f.push("");break;case 3:case 4:f.push(y(b.nodeValue));break}}function O(b){function f(c){if(c.charAt(0)!=="\\")return c.charCodeAt(0);switch(c.charAt(1)){case "b":return 8;case "t":return 9;case "n":return 10;case "v":return 11;case "f":return 12;case "r":return 13;case "u":case "x":return parseInt(c.substring(2),16)||c.charCodeAt(1);case "0":case "1":case "2":case "3":case "4":case "5":case "6":case "7":return parseInt(c.substring(1), +8);default:return c.charCodeAt(1)}}function i(c){if(c<32)return(c<16?"\\x0":"\\x")+c.toString(16);c=String.fromCharCode(c);if(c==="\\"||c==="-"||c==="["||c==="]")c="\\"+c;return c}function o(c){var d=c.substring(1,c.length-1).match(RegExp("\\\\u[0-9A-Fa-f]{4}|\\\\x[0-9A-Fa-f]{2}|\\\\[0-3][0-7]{0,2}|\\\\[0-7]{1,2}|\\\\[\\s\\S]|-|[^-\\\\]","g"));c=[];for(var a=[],k=d[0]==="^",e=k?1:0,h=d.length;e122)){s<65||g>90||a.push([Math.max(65,g)|32,Math.min(s,90)|32]);s<97||g>122||a.push([Math.max(97,g)&-33,Math.min(s,122)&-33])}}a.sort(function(v,w){return v[0]-w[0]||w[1]-v[1]});d=[];g=[NaN,NaN];for(e=0;eh[0]){h[1]+1>h[0]&&a.push("-"); +a.push(i(h[1]))}}a.push("]");return a.join("")}function l(c){for(var d=c.source.match(RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g")),a=d.length,k=[],e=0,h=0;e=2&&c==="[")d[e]=o(g);else if(c!=="\\")d[e]=g.replace(/[a-zA-Z]/g,function(s){s=s.charCodeAt(0);return"["+String.fromCharCode(s&-33,s|32)+"]"})}return d.join("")}for(var n=0,r=false,j=false,q=0,m=b.length;q=0;l-=16)o.push(" ".substring(0,l));l=n+1;break;case "\n":f=0;break;default:++f}if(!o)return i;o.push(i.substring(l));return o.join("")}}function I(b, +f,i,o){if(f){b={source:f,c:b};i(b);o.push.apply(o,b.d)}}function B(b,f){var i={},o;(function(){for(var r=b.concat(f),j=[],q={},m=0,t=r.length;m=0;)i[c.charAt(d)]=p;p=p[1];c=""+p;if(!q.hasOwnProperty(c)){j.push(p);q[c]=null}}j.push(/[\0-\uffff]/);o=O(j)})();var l=f.length;function n(r){for(var j=r.c,q=[j,z],m=0,t=r.source.match(o)||[],p={},c=0,d=t.length;c=5&&"lang-"===k.substring(0,5))&&!(e&&typeof e[1]==="string")){h=false;k=P}h||(p[a]=k)}g=m;m+=a.length;if(h){h=e[1];var s=a.indexOf(h),v=s+h.length;if(e[2]){v=a.length-e[2].length;s=v-h.length}k=k.substring(5);I(j+g,a.substring(0,s),n,q);I(j+g+s,h,Q(k,h),q);I(j+g+v,a.substring(v),n,q)}else q.push(j+g,k)}r.d=q}return n}function x(b){var f=[],i=[];if(b.tripleQuotedStrings)f.push([A,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/, +null,"'\""]);else b.multiLineStrings?f.push([A,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"]):f.push([A,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"]);b.verbatimStrings&&i.push([A,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null]);if(b.hashComments)if(b.cStyleComments){f.push([C,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"]);i.push([A,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/, +null])}else f.push([C,/^#[^\r\n]*/,null,"#"]);if(b.cStyleComments){i.push([C,/^\/\/[^\r\n]*/,null]);i.push([C,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}b.regexLiterals&&i.push(["lang-regex",RegExp("^"+Z+"(/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/)")]);b=b.keywords.replace(/^\s+|\s+$/g,"");b.length&&i.push([R,RegExp("^(?:"+b.replace(/\s+/g,"|")+")\\b"),null]);f.push([z,/^\s+/,null," \r\n\t\u00a0"]);i.push([J,/^@[a-z_$][a-z_$@0-9]*/i,null],[S,/^@?[A-Z]+[a-z][A-Za-z_$@0-9]*/, +null],[z,/^[a-z_$][a-z_$@0-9]*/i,null],[J,/^(?:0x[a-f0-9]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+\-]?\d+)?)[a-z]*/i,null,"0123456789"],[E,/^.[^\s\w\.$@\'\"\`\/\#]*/,null]);return B(f,i)}function $(b){function f(D){if(D>r){if(j&&j!==q){n.push("");j=null}if(!j&&q){j=q;n.push('')}var T=y(p(i.substring(r,D))).replace(e?d:c,"$1 ");e=k.test(T);n.push(T.replace(a,s));r=D}}var i=b.source,o=b.g,l=b.d,n=[],r=0,j=null,q=null,m=0,t=0,p=Y(window.PR_TAB_WIDTH),c=/([\r\n ]) /g, +d=/(^| ) /gm,a=/\r\n?|\n/g,k=/[ \r\n]$/,e=true,h=window._pr_isIE6();h=h?b.b.tagName==="PRE"?h===6?" \r\n":h===7?" 
\r":" \r":" 
":"
";var g=b.b.className.match(/\blinenums\b(?::(\d+))?/),s;if(g){for(var v=[],w=0;w<10;++w)v[w]=h+'
  • ';var F=g[1]&&g[1].length?g[1]-1:0;n.push('
    1. ");s=function(){var D=v[++F%10];return j?""+D+'':D}}else s=h; +for(;;)if(m");j=null}n.push(o[m+1]);m+=2}else if(t");g&&n.push("
    ");b.a=n.join("")}function u(b,f){for(var i=f.length;--i>=0;){var o=f[i];if(G.hasOwnProperty(o))"console"in window&&console.warn("cannot override language handler %s",o);else G[o]=b}}function Q(b,f){b&&G.hasOwnProperty(b)||(b=/^\s*1&&m.charAt(0)==="<"){if(!ba.test(m))if(ca.test(m)){f.push(m.substring(9,m.length-3));n+=m.length-12}else if(da.test(m)){f.push("\n");++n}else if(m.indexOf(V)>=0&&m.replace(/\s(\w+)\s*=\s*(?:\"([^\"]*)\"|'([^\']*)'|(\S+))/g,' $1="$2$3$4"').match(/[cC][lL][aA][sS][sS]=\"[^\"]*\bnocode\b/)){var t=m.match(W)[2],p=1,c;c=j+1;a:for(;c=0;){var e=p.indexOf(";",k);if(e>=0){var h=p.substring(k+3,e),g=10;if(h&&h.charAt(0)==="x"){h=h.substring(1);g=16}var s=parseInt(h,g);isNaN(s)||(p=p.substring(0,k)+String.fromCharCode(s)+p.substring(e+1))}}a=p.replace(ea,"<").replace(fa,">").replace(ga,"'").replace(ha,'"').replace(ia," ").replace(ja, +"&")}f.push(a);n+=a.length}}o={source:f.join(""),h:r};var v=o.source;b.source=v;b.c=0;b.g=o.h;Q(i,v)(b);$(b)}catch(w){if("console"in window)console.log(w&&w.stack?w.stack:w)}}var A="str",R="kwd",C="com",S="typ",J="lit",E="pun",z="pln",P="src",V="nocode",Z=function(){for(var b=["!","!=","!==","#","%","%=","&","&&","&&=","&=","(","*","*=","+=",",","-=","->","/","/=",":","::",";","<","<<","<<=","<=","=","==","===",">",">=",">>",">>=",">>>",">>>=","?","@","[","^","^=","^^","^^=","{","|","|=","||","||=", +"~","break","case","continue","delete","do","else","finally","instanceof","return","throw","try","typeof"],f="(?:^^|[+-]",i=0;i:&a-z])/g,"\\$1");f+=")\\s*";return f}(),L=/&/g,M=//g,X=/\"/g,ea=/</g,fa=/>/g,ga=/'/g,ha=/"/g,ja=/&/g,ia=/ /g,ka=/[\r\n]/g,K=null,aa=RegExp("[^<]+| + + + + + +
    + + +
    \ No newline at end of file diff --git a/src/com/iciql/CompareType.java b/src/com/iciql/CompareType.java new file mode 100644 index 0000000..eb401bb --- /dev/null +++ b/src/com/iciql/CompareType.java @@ -0,0 +1,44 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +/** + * An enumeration of compare operations. + */ + +enum CompareType { + EQUAL("=", true), EXCEEDS(">", true), AT_LEAST(">=", true), LESS_THAN("<", true), AT_MOST("<=", true), NOT_EQUAL( + "<>", true), IS_NOT_NULL("IS NOT NULL", false), IS_NULL("IS NULL", false), LIKE("LIKE", true); + + private String text; + private boolean hasRightExpression; + + CompareType(String text, boolean hasRightExpression) { + this.text = text; + this.hasRightExpression = hasRightExpression; + } + + String getString() { + return text; + } + + boolean hasRightExpression() { + return hasRightExpression; + } + +} diff --git a/src/com/iciql/Condition.java b/src/com/iciql/Condition.java new file mode 100644 index 0000000..df6b033 --- /dev/null +++ b/src/com/iciql/Condition.java @@ -0,0 +1,46 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +/** + * A condition contains one or two operands and a compare operation. + * + * @param + * the operand type + */ + +class Condition implements Token { + CompareType compareType; + A x, y; + + Condition(A x, A y, CompareType compareType) { + this.compareType = compareType; + this.x = x; + this.y = y; + } + + public void appendSQL(SQLStatement stat, Query query) { + query.appendSQL(stat, x); + stat.appendSQL(" "); + stat.appendSQL(compareType.getString()); + if (compareType.hasRightExpression()) { + stat.appendSQL(" "); + query.appendSQL(stat, y); + } + } +} diff --git a/src/com/iciql/ConditionAndOr.java b/src/com/iciql/ConditionAndOr.java new file mode 100644 index 0000000..4d1cd0e --- /dev/null +++ b/src/com/iciql/ConditionAndOr.java @@ -0,0 +1,37 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +/** + * An OR or an AND condition. + */ + +enum ConditionAndOr implements Token { + AND("AND"), OR("OR"); + + private String text; + + ConditionAndOr(String text) { + this.text = text; + } + + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL(text); + } + +} diff --git a/src/com/iciql/Constants.java b/src/com/iciql/Constants.java new file mode 100644 index 0000000..6bbbc71 --- /dev/null +++ b/src/com/iciql/Constants.java @@ -0,0 +1,42 @@ +/* + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +/** + * Iciql constants. + */ +public class Constants { + + public static final String NAME = "iciql"; + + // The build script extracts this exact line so be careful editing it + // and only use A-Z a-z 0-9 .-_ in the string. + public static final String VERSION = "0.5.0-SNAPSHOT"; + + // The build script extracts this exact line so be careful editing it + // and only use A-Z a-z 0-9 .-_ in the string. + public static final String VERSION_DATE = "PENDING"; + + // The build script extracts this exact line so be careful editing it + // and only use A-Z a-z 0-9 .-_ in the string. + public static final String API_CURRENT = "1"; + + // The build script extracts this exact line so be careful editing it + // and only use A-Z a-z 0-9 .-_ in the string. + public static final String API_PREVIOUS = "1"; + +} diff --git a/src/com/iciql/Db.java b/src/com/iciql/Db.java new file mode 100644 index 0000000..3f86d15 --- /dev/null +++ b/src/com/iciql/Db.java @@ -0,0 +1,470 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; + +import javax.sql.DataSource; + +import com.iciql.DbUpgrader.DefaultDbUpgrader; +import com.iciql.Iciql.IQDatabase; +import com.iciql.Iciql.IQTable; +import com.iciql.SQLDialect.DefaultSQLDialect; +import com.iciql.SQLDialect.H2Dialect; +import com.iciql.util.JdbcUtils; +import com.iciql.util.StringUtils; +import com.iciql.util.Utils; +import com.iciql.util.WeakIdentityHashMap; + +/** + * This class represents a connection to a database. + */ + +public class Db { + + /** + * This map It holds unique tokens that are generated by functions such as + * Function.sum(..) in "db.from(p).select(Function.sum(p.unitPrice))". It + * doesn't actually hold column tokens, as those are bound to the query + * itself. + */ + private static final Map TOKENS; + + private static final Map> DIALECTS; + + private final Connection conn; + private final Map, TableDefinition> classMap = Collections + .synchronizedMap(new HashMap, TableDefinition>()); + private final SQLDialect dialect; + private DbUpgrader dbUpgrader = new DefaultDbUpgrader(); + private final Set> upgradeChecked = Collections.synchronizedSet(new HashSet>()); + + static { + TOKENS = Collections.synchronizedMap(new WeakIdentityHashMap()); + DIALECTS = Collections.synchronizedMap(new HashMap>()); + DIALECTS.put("org.h2", H2Dialect.class); + } + + private Db(Connection conn) { + this.conn = conn; + dialect = getDialect(conn.getClass().getCanonicalName()); + dialect.configureDialect(conn); + } + + public static void registerDialect(Connection conn, Class dialectClass) { + registerDialect(conn.getClass().getCanonicalName(), dialectClass); + } + + public static void registerDialect(String connClass, Class dialectClass) { + DIALECTS.put(connClass, dialectClass); + } + + SQLDialect getDialect(String clazz) { + // try dialect by connection class name + Class dialectClass = DIALECTS.get(clazz); + int lastDot = 0; + while (dialectClass == null) { + // try dialect by package name + int nextDot = clazz.indexOf('.', lastDot); + if (nextDot > -1) { + String pkg = clazz.substring(0, nextDot); + lastDot = nextDot + 1; + dialectClass = DIALECTS.get(pkg); + } else { + dialectClass = DefaultSQLDialect.class; + } + } + return instance(dialectClass); + } + + static X registerToken(X x, Token token) { + TOKENS.put(x, token); + return x; + } + + static Token getToken(Object x) { + return TOKENS.get(x); + } + + private static T instance(Class clazz) { + try { + return clazz.newInstance(); + } catch (Exception e) { + throw new IciqlException(e); + } + } + + public static Db open(String url, String user, String password) { + try { + Connection conn = JdbcUtils.getConnection(null, url, user, password); + return new Db(conn); + } catch (SQLException e) { + throw convert(e); + } + } + + /** + * Create a new database instance using a data source. This method is fast, + * so that you can always call open() / close() on usage. + * + * @param ds + * the data source + * @return the database instance. + */ + public static Db open(DataSource ds) { + try { + return new Db(ds.getConnection()); + } catch (SQLException e) { + throw convert(e); + } + } + + public static Db open(Connection conn) { + return new Db(conn); + } + + public static Db open(String url, String user, char[] password) { + try { + Properties prop = new Properties(); + prop.setProperty("user", user); + prop.put("password", password); + Connection conn = JdbcUtils.getConnection(null, url, prop); + return new Db(conn); + } catch (SQLException e) { + throw convert(e); + } + } + + private static Error convert(Exception e) { + return new Error(e); + } + + public void insert(T t) { + Class clazz = t.getClass(); + define(clazz).createTableIfRequired(this).insert(this, t, false); + } + + public long insertAndGetKey(T t) { + Class clazz = t.getClass(); + return define(clazz).createTableIfRequired(this).insert(this, t, true); + } + + /** + * Merge usually INSERTS if the record does not exist or UPDATES the record + * if it does exist. Not all databases support MERGE and the syntax varies + * with the database. + * + * If the dialect does not support merge an IciqlException will be thrown. + * + * @param t + */ + public void merge(T t) { + if (!getDialect().supportsMerge()) { + throw new IciqlException("Merge is not supported by this SQL dialect"); + } + Class clazz = t.getClass(); + define(clazz).createTableIfRequired(this).merge(this, t); + } + + public void update(T t) { + Class clazz = t.getClass(); + define(clazz).createTableIfRequired(this).update(this, t); + } + + public void delete(T t) { + Class clazz = t.getClass(); + define(clazz).createTableIfRequired(this).delete(this, t); + } + + public Query from(T alias) { + Class clazz = alias.getClass(); + define(clazz).createTableIfRequired(this); + return Query.from(this, alias); + } + + @SuppressWarnings("unchecked") + public List buildObjects(Class modelClass, ResultSet rs) { + List result = new ArrayList(); + TableDefinition def = (TableDefinition) define(modelClass).createTableIfRequired(this); + try { + while (rs.next()) { + T item = Utils.newObject(modelClass); + def.readRow(item, rs); + result.add(item); + } + } catch (SQLException e) { + throw new IciqlException(e); + } + return result; + } + + Db upgradeDb() { + if (!upgradeChecked.contains(dbUpgrader.getClass())) { + // flag as checked immediately because calls are nested. + upgradeChecked.add(dbUpgrader.getClass()); + + IQDatabase model = dbUpgrader.getClass().getAnnotation(IQDatabase.class); + if (model.version() > 0) { + DbVersion v = new DbVersion(); + DbVersion dbVersion = + // (SCHEMA="" && TABLE="") == DATABASE + from(v).where(v.schema).is("").and(v.table).is("").selectFirst(); + if (dbVersion == null) { + // database has no version registration, but model specifies + // version: insert DbVersion entry and return. + DbVersion newDb = new DbVersion(model.version()); + insert(newDb); + } else { + // database has a version registration: + // check to see if upgrade is required. + if ((model.version() > dbVersion.version) && (dbUpgrader != null)) { + // database is an older version than the model + boolean success = dbUpgrader + .upgradeDatabase(this, dbVersion.version, model.version()); + if (success) { + dbVersion.version = model.version(); + update(dbVersion); + } + } + } + } + } + return this; + } + + void upgradeTable(TableDefinition model) { + if (!upgradeChecked.contains(model.getModelClass())) { + // flag is checked immediately because calls are nested + upgradeChecked.add(model.getModelClass()); + + if (model.tableVersion > 0) { + // table is using iciql version tracking. + DbVersion v = new DbVersion(); + String schema = StringUtils.isNullOrEmpty(model.schemaName) ? "" : model.schemaName; + DbVersion dbVersion = from(v).where(v.schema).like(schema).and(v.table).like(model.tableName) + .selectFirst(); + if (dbVersion == null) { + // table has no version registration, but model specifies + // version: insert DbVersion entry + DbVersion newTable = new DbVersion(model.tableVersion); + newTable.schema = schema; + newTable.table = model.tableName; + insert(newTable); + } else { + // table has a version registration: + // check if upgrade is required + if ((model.tableVersion > dbVersion.version) && (dbUpgrader != null)) { + // table is an older version than model + boolean success = dbUpgrader.upgradeTable(this, schema, model.tableName, + dbVersion.version, model.tableVersion); + if (success) { + dbVersion.version = model.tableVersion; + update(dbVersion); + } + } + } + } + } + } + + TableDefinition define(Class clazz) { + TableDefinition def = getTableDefinition(clazz); + if (def == null) { + upgradeDb(); + def = new TableDefinition(clazz); + def.mapFields(); + classMap.put(clazz, def); + if (Iciql.class.isAssignableFrom(clazz)) { + T t = instance(clazz); + Iciql table = (Iciql) t; + Define.define(def, table); + } else if (clazz.isAnnotationPresent(IQTable.class)) { + // annotated classes skip the Define().define() static + // initializer + T t = instance(clazz); + def.mapObject(t); + } + } + return def; + } + + public synchronized void setDbUpgrader(DbUpgrader upgrader) { + if (!upgrader.getClass().isAnnotationPresent(IQDatabase.class)) { + throw new IciqlException("DbUpgrader must be annotated with " + IQDatabase.class.getSimpleName()); + } + this.dbUpgrader = upgrader; + upgradeChecked.clear(); + } + + SQLDialect getDialect() { + return dialect; + } + + public Connection getConnection() { + return conn; + } + + public void close() { + try { + conn.close(); + } catch (Exception e) { + throw new IciqlException(e); + } + } + + public TestCondition test(A x) { + return new TestCondition(x); + } + + public void insertAll(List list) { + for (T t : list) { + insert(t); + } + } + + public List insertAllAndGetKeys(List list) { + List identities = new ArrayList(); + for (T t : list) { + identities.add(insertAndGetKey(t)); + } + return identities; + } + + public void updateAll(List list) { + for (T t : list) { + update(t); + } + } + + public void deleteAll(List list) { + for (T t : list) { + delete(t); + } + } + + PreparedStatement prepare(String sql, boolean returnGeneratedKeys) { + try { + if (returnGeneratedKeys) { + return conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); + } + return conn.prepareStatement(sql); + } catch (SQLException e) { + throw new IciqlException(e); + } + } + + @SuppressWarnings("unchecked") + TableDefinition getTableDefinition(Class clazz) { + return (TableDefinition) classMap.get(clazz); + } + + /** + * Run a SQL query directly against the database. + * + * Be sure to close the ResultSet with + * + *
    +	 * JdbcUtils.closeSilently(rs, true);
    +	 * 
    + * + * @param sql + * the SQL statement + * @param args + * optional object arguments for x=? tokens in query + * @return the result set + */ + public ResultSet executeQuery(String sql, Object... args) { + try { + if (args.length == 0) { + return conn.createStatement().executeQuery(sql); + } else { + PreparedStatement stat = conn.prepareStatement(sql); + int i = 1; + for (Object arg : args) { + stat.setObject(i++, arg); + } + return stat.executeQuery(); + } + } catch (SQLException e) { + throw new IciqlException(e); + } + } + + /** + * Run a SQL query directly against the database and map the results to the + * model class. + * + * @param modelClass + * the model class to bind the query ResultSet rows into. + * @param sql + * the SQL statement + * @return the result set + */ + public List executeQuery(Class modelClass, String sql, Object... args) { + ResultSet rs = null; + try { + if (args.length == 0) { + rs = conn.createStatement().executeQuery(sql); + } else { + PreparedStatement stat = conn.prepareStatement(sql); + int i = 1; + for (Object arg : args) { + stat.setObject(i++, arg); + } + rs = stat.executeQuery(); + } + return buildObjects(modelClass, rs); + } catch (SQLException e) { + throw new IciqlException(e); + } finally { + JdbcUtils.closeSilently(rs, true); + } + } + + /** + * Run a SQL statement directly against the database. + * + * @param sql + * the SQL statement + * @return the update count + */ + public int executeUpdate(String sql) { + Statement stat = null; + try { + stat = conn.createStatement(); + int updateCount = stat.executeUpdate(sql); + return updateCount; + } catch (SQLException e) { + throw new IciqlException(e); + } finally { + JdbcUtils.closeSilently(stat); + } + } +} diff --git a/src/com/iciql/DbInspector.java b/src/com/iciql/DbInspector.java new file mode 100644 index 0000000..fb1e5d6 --- /dev/null +++ b/src/com/iciql/DbInspector.java @@ -0,0 +1,196 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; + +import com.iciql.Iciql.IQTable; +import com.iciql.util.JdbcUtils; +import com.iciql.util.StringUtils; +import com.iciql.util.Utils; + +/** + * Class to inspect a model and a database for the purposes of model validation + * and automatic model generation. This class finds the available schemas and + * tables and serves as the entry point for model generation and validation. + */ +public class DbInspector { + + private Db db; + private DatabaseMetaData metaData; + private Class dateTimeClass = java.util.Date.class; + + public DbInspector(Db db) { + this.db = db; + } + + /** + * Set the preferred class to store date and time. Possible values are: + * java.util.Date (default) and java.sql.Timestamp. + * + * @param dateTimeClass + * the new class + */ + public void setPreferredDateTimeClass(Class dateTimeClass) { + this.dateTimeClass = dateTimeClass; + } + + /** + * Generates models class skeletons for schemas and tables. If the table + * name is undefined, models will be generated for every table within the + * specified schema. Additionally, if no schema is defined, models will be + * generated for all schemas and all tables. + * + * @param schema + * the schema name (optional) + * @param table + * the table name (optional) + * @param packageName + * the package name (optional) + * @param annotateSchema + * (includes schema name in annotation) + * @param trimStrings + * (trims strings to maxLength of column) + * @return a list of complete model classes as strings, each element a class + */ + public List generateModel(String schema, String table, String packageName, boolean annotateSchema, + boolean trimStrings) { + try { + List models = Utils.newArrayList(); + List tables = getTables(schema, table); + for (TableInspector t : tables) { + t.read(metaData); + String model = t.generateModel(packageName, annotateSchema, trimStrings); + models.add(model); + } + return models; + } catch (SQLException s) { + throw new IciqlException(s); + } + } + + /** + * Validates a model. + * + * @param model + * an instance of the model class + * @param throwOnError + * if errors should cause validation to fail + * @return a list of validation remarks + */ + public List validateModel(T model, boolean throwOnError) { + try { + TableInspector inspector = getTable(model); + inspector.read(metaData); + @SuppressWarnings("unchecked") + Class clazz = (Class) model.getClass(); + TableDefinition def = db.define(clazz); + return inspector.validate(def, throwOnError); + } catch (SQLException s) { + throw new IciqlException(s); + } + } + + private DatabaseMetaData getMetaData() throws SQLException { + if (metaData == null) { + metaData = db.getConnection().getMetaData(); + } + return metaData; + } + + /** + * Get the table in the database based on the model definition. + * + * @param model + * an instance of the model class + * @return the table inspector + */ + private TableInspector getTable(T model) throws SQLException { + @SuppressWarnings("unchecked") + Class clazz = (Class) model.getClass(); + TableDefinition def = db.define(clazz); + boolean forceUpperCase = getMetaData().storesUpperCaseIdentifiers(); + String schema = (forceUpperCase && def.schemaName != null) ? def.schemaName.toUpperCase() : def.schemaName; + String table = forceUpperCase ? def.tableName.toUpperCase() : def.tableName; + List tables = getTables(schema, table); + return tables.get(0); + } + + /** + * Returns a list of tables. This method always returns at least one + * element. If no table is found, an exception is thrown. + * + * @param schema + * the schema name + * @param table + * the table name + * @return a list of table inspectors (always contains at least one element) + */ + private List getTables(String schema, String table) throws SQLException { + ResultSet rs = null; + try { + rs = getMetaData().getSchemas(); + ArrayList schemaList = Utils.newArrayList(); + while (rs.next()) { + schemaList.add(rs.getString("TABLE_SCHEM")); + } + JdbcUtils.closeSilently(rs); + + String iciqlTables = DbVersion.class.getAnnotation(IQTable.class).name(); + + List tables = Utils.newArrayList(); + if (schemaList.size() == 0) { + schemaList.add(null); + } + for (String s : schemaList) { + rs = getMetaData().getTables(null, s, null, new String[] { "TABLE" }); + while (rs.next()) { + String t = rs.getString("TABLE_NAME"); + if (!t.equalsIgnoreCase(iciqlTables)) { + tables.add(new TableInspector(s, t, getMetaData().storesUpperCaseIdentifiers(), dateTimeClass)); + } + } + } + + if (StringUtils.isNullOrEmpty(schema) && StringUtils.isNullOrEmpty(table)) { + // all schemas and tables + return tables; + } + // schema subset OR table subset OR exact match + List matches = Utils.newArrayList(); + for (TableInspector t : tables) { + if (t.matches(schema, table)) { + matches.add(t); + } + } + if (matches.size() == 0) { + throw new IciqlException(MessageFormat.format("Failed to find schema={0} table={1}", + schema == null ? "" : schema, table == null ? "" : table)); + } + return matches; + } finally { + JdbcUtils.closeSilently(rs); + } + } + +} diff --git a/src/com/iciql/DbUpgrader.java b/src/com/iciql/DbUpgrader.java new file mode 100644 index 0000000..c4ab36b --- /dev/null +++ b/src/com/iciql/DbUpgrader.java @@ -0,0 +1,81 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +import com.iciql.Iciql.IQDatabase; + +/** + * Interface which defines a class to handle table changes based on model + * versions. An implementation of DbUpgrader must be annotated with the + * IQDatabase annotation, which defines the expected database version + * number. + */ +public interface DbUpgrader { + + /** + * Defines method interface to handle database upgrades. This method is only + * called if your DbUpgrader implementation is annotated with + * IQDatabase. + * + * @param db + * the database + * @param fromVersion + * the old version + * @param toVersion + * the new version + * @return true for successful upgrade. If the upgrade is successful, the + * version registry is automatically updated. + */ + boolean upgradeDatabase(Db db, int fromVersion, int toVersion); + + /** + * Defines method interface to handle table upgrades. + * + * @param db + * the database + * @param schema + * the schema + * @param table + * the table + * @param fromVersion + * the old version + * @param toVersion + * the new version + * @return true for successful upgrade. If the upgrade is successful, the + * version registry is automatically updated. + */ + boolean upgradeTable(Db db, String schema, String table, int fromVersion, int toVersion); + + /** + * The default database upgrader. It throws runtime exception instead of + * handling upgrade requests. + */ + @IQDatabase(version = 0) + public static class DefaultDbUpgrader implements DbUpgrader { + + public boolean upgradeDatabase(Db db, int fromVersion, int toVersion) { + throw new IciqlException("Please provide your own DbUpgrader implementation."); + } + + public boolean upgradeTable(Db db, String schema, String table, int fromVersion, int toVersion) { + throw new IciqlException("Please provide your own DbUpgrader implementation."); + } + + } + +} diff --git a/src/com/iciql/DbVersion.java b/src/com/iciql/DbVersion.java new file mode 100644 index 0000000..50d24a7 --- /dev/null +++ b/src/com/iciql/DbVersion.java @@ -0,0 +1,55 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +import com.iciql.Iciql.IQColumn; +import com.iciql.Iciql.IQTable; + +/** + * A system table to track database and table versions. + */ +@IQTable(name = "_iq_versions", primaryKey = "schemaName tableName", memoryTable = true) +public class DbVersion { + + @IQColumn(name = "schemaName", allowNull = false) + String schema = ""; + + @IQColumn(name = "tableName", allowNull = false) + String table = ""; + + @IQColumn(name = "version") + Integer version; + + public DbVersion() { + // nothing to do + } + + /** + * Constructor for defining a version entry. Both the schema and the table + * are empty strings, which means this is the row for the 'database'. + * + * @param version + * the database version + */ + public DbVersion(int version) { + this.schema = ""; + this.table = ""; + this.version = version; + } + +} diff --git a/src/com/iciql/Define.java b/src/com/iciql/Define.java new file mode 100644 index 0000000..54e435f --- /dev/null +++ b/src/com/iciql/Define.java @@ -0,0 +1,88 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +import com.iciql.Iciql.IndexType; + +/** + * This class provides utility methods to define primary keys, indexes, and set + * the name of the table. + */ + +public class Define { + + private static TableDefinition currentTableDefinition; + private static Iciql currentTable; + + public static void primaryKey(Object... columns) { + checkInDefine(); + currentTableDefinition.setPrimaryKey(columns); + } + + public static void index(Object... columns) { + checkInDefine(); + currentTableDefinition.addIndex(IndexType.STANDARD, columns); + } + + public static void uniqueIndex(Object... columns) { + checkInDefine(); + currentTableDefinition.addIndex(IndexType.UNIQUE, columns); + } + + public static void hashIndex(Object column) { + checkInDefine(); + currentTableDefinition.addIndex(IndexType.HASH, new Object[] { column }); + } + + public static void uniqueHashIndex(Object column) { + checkInDefine(); + currentTableDefinition.addIndex(IndexType.UNIQUE_HASH, new Object[] { column }); + } + + public static void columnName(Object column, String columnName) { + checkInDefine(); + currentTableDefinition.setColumnName(column, columnName); + } + + public static void maxLength(Object column, int length) { + checkInDefine(); + currentTableDefinition.setMaxLength(column, length); + } + + public static void tableName(String tableName) { + checkInDefine(); + currentTableDefinition.setTableName(tableName); + } + + static synchronized void define(TableDefinition tableDefinition, Iciql table) { + currentTableDefinition = tableDefinition; + currentTable = table; + tableDefinition.mapObject(table); + table.defineIQ(); + currentTable = null; + currentTableDefinition = null; + } + + private static void checkInDefine() { + if (currentTable == null) { + throw new IciqlException("This method may only be called " + + "from within the define() method, and the define() method " + "is called by the framework."); + } + } + +} diff --git a/src/com/iciql/Filter.java b/src/com/iciql/Filter.java new file mode 100644 index 0000000..99dbdc3 --- /dev/null +++ b/src/com/iciql/Filter.java @@ -0,0 +1,25 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +/** + * Represents the WHERE clause of a query. + */ +public interface Filter { + boolean where(); +} diff --git a/src/com/iciql/Function.java b/src/com/iciql/Function.java new file mode 100644 index 0000000..f2c9e30 --- /dev/null +++ b/src/com/iciql/Function.java @@ -0,0 +1,149 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +import com.iciql.util.Utils; + +/** + * This class provides static methods that represents common SQL functions. + */ +public class Function implements Token { + + // must be a new instance + private static final Long COUNT_STAR = Long.valueOf(0); + + protected Object[] x; + private String name; + + protected Function(String name, Object... x) { + this.name = name; + this.x = x; + } + + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL(name).appendSQL("("); + int i = 0; + for (Object o : x) { + if (i++ > 0) { + stat.appendSQL(","); + } + query.appendSQL(stat, o); + } + stat.appendSQL(")"); + } + + public static Long count() { + return COUNT_STAR; + } + + public static Integer length(Object x) { + return Db.registerToken(Utils.newObject(Integer.class), new Function("LENGTH", x)); + } + + @SuppressWarnings("unchecked") + public static T sum(T x) { + return (T) Db.registerToken(Utils.newObject(x.getClass()), new Function("SUM", x)); + } + + public static Long count(Object x) { + return Db.registerToken(Utils.newObject(Long.class), new Function("COUNT", x)); + } + + public static Boolean isNull(Object x) { + return Db.registerToken(Utils.newObject(Boolean.class), new Function("", x) { + public void appendSQL(SQLStatement stat, Query query) { + query.appendSQL(stat, x[0]); + stat.appendSQL(" IS NULL"); + } + }); + } + + public static Boolean isNotNull(Object x) { + return Db.registerToken(Utils.newObject(Boolean.class), new Function("", x) { + public void appendSQL(SQLStatement stat, Query query) { + query.appendSQL(stat, x[0]); + stat.appendSQL(" IS NOT NULL"); + } + }); + } + + public static Boolean not(Boolean x) { + return Db.registerToken(Utils.newObject(Boolean.class), new Function("", x) { + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL("NOT "); + query.appendSQL(stat, x[0]); + } + }); + } + + public static Boolean or(Boolean... x) { + return Db.registerToken(Utils.newObject(Boolean.class), new Function("", (Object[]) x) { + public void appendSQL(SQLStatement stat, Query query) { + int i = 0; + for (Object o : x) { + if (i++ > 0) { + stat.appendSQL(" OR "); + } + query.appendSQL(stat, o); + } + } + }); + } + + public static Boolean and(Boolean... x) { + return Db.registerToken(Utils.newObject(Boolean.class), new Function("", (Object[]) x) { + public void appendSQL(SQLStatement stat, Query query) { + int i = 0; + for (Object o : x) { + if (i++ > 0) { + stat.appendSQL(" AND "); + } + query.appendSQL(stat, o); + } + } + }); + } + + @SuppressWarnings("unchecked") + public static X min(X x) { + Class clazz = (Class) x.getClass(); + X o = Utils.newObject(clazz); + return Db.registerToken(o, new Function("MIN", x)); + } + + @SuppressWarnings("unchecked") + public static X max(X x) { + Class clazz = (Class) x.getClass(); + X o = Utils.newObject(clazz); + return Db.registerToken(o, new Function("MAX", x)); + } + + public static Boolean like(String x, String pattern) { + Boolean o = Utils.newObject(Boolean.class); + return Db.registerToken(o, new Function("LIKE", x, pattern) { + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL("("); + query.appendSQL(stat, x[0]); + stat.appendSQL(" LIKE "); + query.appendSQL(stat, x[1]); + stat.appendSQL(")"); + } + }); + } + +} diff --git a/src/com/iciql/Iciql.java b/src/com/iciql/Iciql.java new file mode 100644 index 0000000..7267d33 --- /dev/null +++ b/src/com/iciql/Iciql.java @@ -0,0 +1,383 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A class that implements this interface can be used as a database table. + *

    + * You may implement the Table interface on your model object and optionally use + * IQColumn annotations (which imposes a compile-time and runtime-dependency on + * iciql), or may choose to use the IQTable and IQColumn annotations only (which + * imposes a compile-time and runtime-dependency on this file only). + *

    + * If a class is annotated with IQTable and at the same time implements Table, + * the define() method is not called. + *

    + * Supported data types: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    java.lang.StringVARCHAR (maxLength > 0) or TEXT (maxLength == 0)
    java.lang.BooleanBIT
    java.lang.ByteTINYINT
    java.lang.ShortSMALLINT
    java.lang.IntegerINT
    java.lang.LongBIGINT
    java.lang.FloatREAL
    java.lang.DoubleDOUBLE
    java.math.BigDecimalDECIMAL
    java.sql.DateDATE
    java.sql.TimeTIME
    java.sql.TimestampTIMESTAMP
    java.util.DateTIMESTAMP
    + *

    + * Unsupported data types: binary types (BLOB, etc), and custom types. + *

    + * Table and field mapping: by default, the mapped table name is the class name + * and the public fields are reflectively mapped, by their name, to columns. As + * an alternative, you may specify both the table and column definition by + * annotations. + *

    + * Table Interface: you may set additional parameters such as table name, + * primary key, and indexes in the define() method. + *

    + * Annotations: you may use the annotations with or without implementing the + * Table interface. The annotations allow you to decouple your model completely + * from iciql other than this file. + *

    + * Automatic model generation: you may automatically generate model classes as + * strings with the Db and DbInspector objects: + * + *

    + * Db db = Db.open("jdbc:h2:mem:", "sa", "sa");
    + * DbInspector inspector = new DbInspector(db);
    + * List<String> models =
    + *         inspector.generateModel(schema, table, packageName,
    + *         annotateSchema, trimStrings)
    + * 
    + * + * Or you may use the GenerateModels tool to generate and save your classes to + * the file system: + * + *
    + * java -jar iciql.jar
    + *      -url "jdbc:h2:mem:"
    + *      -user sa -password sa -schema schemaName -table tableName
    + *      -package packageName -folder destination
    + *      -annotateSchema false -trimStrings true
    + * 
    + * + * Model validation: you may validate your model class with DbInspector object. + * The DbInspector will report errors, warnings, and suggestions: + * + *
    + * Db db = Db.open("jdbc:h2:mem:", "sa", "sa");
    + * DbInspector inspector = new DbInspector(db);
    + * List<Validation> remarks = inspector.validateModel(new MyModel(), throwOnError);
    + * for (Validation remark : remarks) {
    + * 	System.out.println(remark);
    + * }
    + * 
    + */ +public interface Iciql { + + /** + * An annotation for a database. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface IQDatabase { + + /** + * If set to a non-zero value, iciql maintains a "_iq_versions" table + * within your database. The version number is used to call to a + * registered DbUpgrader implementation to perform relevant ALTER + * statements. Default: 0. You must specify a DbUpgrader on your Db + * object to use this parameter. + */ + int version() default 0; + + } + + /** + * An annotation for a schema. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface IQSchema { + + /** + * The schema may be optionally specified. Default: unspecified. + */ + String name() default ""; + + } + + /** + * Enumeration defining the four index types. + */ + public static enum IndexType { + STANDARD, UNIQUE, HASH, UNIQUE_HASH; + } + + /** + * An index annotation. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface IQIndex { + + /** + * Standard indexes may be optionally specified. + *
      + *
    • standard = "id, name"
    • + *
    • standard = "id name"
    • + *
    • standard = { "id name", "date" }
    • + *
    + * Standard indexes may still be added in the define() method if the + * model class is not annotated with IQTable. Default: unspecified. + */ + String[] standard() default {}; + + /** + * Unique indexes may be optionally specified. + *
      + *
    • unique = "id, name"
    • + *
    • unique = "id name"
    • + *
    • unique = { "id name", "date" }
    • + *
    + * Unique indexes may still be added in the define() method if the model + * class is not annotated with IQTable. Default: unspecified. + */ + String[] unique() default {}; + + /** + * Hash indexes may be optionally specified. + *
      + *
    • hash = "name" + *
    • hash = { "name", "date" } + *
    + * Hash indexes may still be added in the define() method if the model + * class is not annotated with IQTable. Default: unspecified. + */ + String[] hash() default {}; + + /** + * Unique hash indexes may be optionally specified. + *
      + *
    • uniqueHash = "id" + *
    • uniqueHash = "name" + *
    • uniqueHash = { "id", "name" } + *
    + * Unique hash indexes may still be added in the define() method if the + * model class is not annotated with IQTable. Default: unspecified. + */ + String[] uniqueHash() default {}; + + } + + /** + * Annotation to define a table. + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface IQTable { + + /** + * The table name. If not specified the class name is used as the table + * name. + *

    + * The table name may still be overridden in the define() method if the + * model class is not annotated with IQTable. Default: unspecified. + */ + String name() default ""; + + /** + * The primary key may be optionally specified. If it is not specified, + * then no primary key is set by the IQTable annotation. You may specify + * a composite primary key. + *

      + *
    • primaryKey = "id, name" + *
    • primaryKey = "id name" + *
    + * The primary key may still be overridden in the define() method if the + * model class is not annotated with IQTable. Default: unspecified. + */ + String primaryKey() default ""; + + /** + * The inherit columns allows this model class to inherit columns from + * its super class. Any IQTable annotation present on the super class is + * ignored. Default: false. + */ + boolean inheritColumns() default false; + + /** + * Whether or not iciql tries to create the table and indexes. Default: + * true. + */ + boolean createIfRequired() default true; + + /** + * Whether only supported types are mapped. If true, unsupported mapped + * types will throw an IciqlException. If false, unsupported mapped + * types will default to VARCHAR. Default: true. + */ + boolean strictTypeMapping() default true; + + /** + * If true, only fields that are explicitly annotated as IQColumn are + * mapped. Default: true. + */ + boolean annotationsOnly() default true; + + /** + * If true, this table is created as a memory table where data is + * persistent, but index data is kept in main memory. Valid only for H2 + * databases. Default: false. + */ + boolean memoryTable() default false; + + /** + * If non-zero, iciql will maintain a "_iq_versions" table within your + * database. The version number is used to call to a registered + * DbUpgrader implementation to perform relevant ALTER statements. + * Default: 0. You must specify a DbUpgrader on your Db object to use + * this parameter. + */ + int version() default 0; + } + + /** + * Annotation to define a column. Annotated fields may have any scope + * (however, the JVM may raise a SecurityException if the SecurityManager + * doesn't allow iciql to access the field.) + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + public @interface IQColumn { + + /** + * If not specified, the field name is used as the column name. Default: + * the field name. + */ + String name() default ""; + + /** + * This column is the primary key. Default: false. + */ + boolean primaryKey() default false; + + /** + * The column is created with a sequence as the default value. Default: + * false. + */ + boolean autoIncrement() default false; + + /** + * If larger than zero, it is used during the CREATE TABLE phase. It may + * also be used to prevent database exceptions on INSERT and UPDATE + * statements (see trimString). + *

    + * Any maxLength set in define() may override this annotation setting if + * the model class is not annotated with IQTable. Default: 0. + */ + int maxLength() default 0; + + /** + * If true, iciql will automatically trim the string if it exceeds + * maxLength (value.substring(0, maxLength)). Default: false. + */ + boolean trimString() default false; + + /** + * If false, iciql will set the column NOT NULL during the CREATE TABLE + * phase. Default: false. + */ + boolean allowNull() default false; + + /** + * The default value assigned to the column during the CREATE TABLE + * phase. This field could contain a literal single-quoted value, or a + * function call. Empty strings are considered NULL. Examples: + *

      + *
    • defaultValue="" (null) + *
    • defaultValue="CURRENT_TIMESTAMP" + *
    • defaultValue="''" (empty string) + *
    • defaultValue="'0'" + *
    • defaultValue="'1970-01-01 00:00:01'" + *
    + * if the default value is specified, and auto increment is disabled, + * and primary key is disabled, then this value is included in the + * "DEFAULT ..." phrase of a column during the CREATE TABLE process. + * Default: unspecified (null). + */ + String defaultValue() default ""; + + } + + /** + * This method is called to let the table define the primary key, indexes, + * and the table name. + */ + @Deprecated + void defineIQ(); +} diff --git a/src/com/iciql/IciqlException.java b/src/com/iciql/IciqlException.java new file mode 100644 index 0000000..2aee36d --- /dev/null +++ b/src/com/iciql/IciqlException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +/** + * Iciql wraps all exceptions with this class. + */ +public class IciqlException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public IciqlException(String message) { + super(message); + } + + public IciqlException(Throwable t) { + super(t); + } + + public IciqlException(String message, Throwable t) { + super(message, t); + } +} diff --git a/src/com/iciql/ModelUtils.java b/src/com/iciql/ModelUtils.java new file mode 100644 index 0000000..6b28f0e --- /dev/null +++ b/src/com/iciql/ModelUtils.java @@ -0,0 +1,324 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +import static com.iciql.util.StringUtils.isNullOrEmpty; + +import java.lang.reflect.Method; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import com.iciql.TableDefinition.FieldDefinition; +import com.iciql.util.StringUtils; + +/** + * Utility methods for models related to type mapping, default value validation, + * and class or field name creation. + */ +class ModelUtils { + + /** + * The list of supported data types. It is used by the runtime mapping for + * CREATE statements. + */ + private static final Map, String> SUPPORTED_TYPES = new HashMap, String>(); + + static { + Map, String> m = SUPPORTED_TYPES; + m.put(String.class, "VARCHAR"); + m.put(Boolean.class, "BIT"); + m.put(Byte.class, "TINYINT"); + m.put(Short.class, "SMALLINT"); + m.put(Integer.class, "INT"); + m.put(Long.class, "BIGINT"); + m.put(Float.class, "REAL"); + m.put(Double.class, "DOUBLE"); + m.put(BigDecimal.class, "DECIMAL"); + m.put(java.sql.Timestamp.class, "TIMESTAMP"); + m.put(java.util.Date.class, "TIMESTAMP"); + m.put(java.sql.Date.class, "DATE"); + m.put(java.sql.Time.class, "TIME"); + // TODO add blobs, binary types, custom types? + } + + /** + * Convert SQL type aliases to the list of supported types. This map is used + * by generation and validation. + */ + private static final Map SQL_TYPES = new HashMap(); + + static { + Map m = SQL_TYPES; + m.put("CHAR", "VARCHAR"); + m.put("CHARACTER", "VARCHAR"); + m.put("NCHAR", "VARCHAR"); + m.put("VARCHAR_CASESENSITIVE", "VARCHAR"); + m.put("VARCHAR_IGNORECASE", "VARCHAR"); + m.put("LONGVARCHAR", "VARCHAR"); + m.put("VARCHAR2", "VARCHAR"); + m.put("NVARCHAR", "VARCHAR"); + m.put("NVARCHAR2", "VARCHAR"); + m.put("TEXT", "VARCHAR"); + m.put("NTEXT", "VARCHAR"); + m.put("TINYTEXT", "VARCHAR"); + m.put("MEDIUMTEXT", "VARCHAR"); + m.put("LONGTEXT", "VARCHAR"); + m.put("CLOB", "VARCHAR"); + m.put("NCLOB", "VARCHAR"); + + // logic + m.put("BOOL", "BIT"); + m.put("BOOLEAN", "BIT"); + + // numeric + m.put("BYTE", "TINYINT"); + m.put("INT2", "SMALLINT"); + m.put("YEAR", "SMALLINT"); + m.put("INTEGER", "INT"); + m.put("MEDIUMINT", "INT"); + m.put("INT4", "INT"); + m.put("SIGNED", "INT"); + m.put("INT8", "BIGINT"); + m.put("IDENTITY", "BIGINT"); + + // decimal + m.put("NUMBER", "DECIMAL"); + m.put("DEC", "DECIMAL"); + m.put("NUMERIC", "DECIMAL"); + m.put("FLOAT", "DOUBLE"); + m.put("FLOAT4", "DOUBLE"); + m.put("FLOAT8", "DOUBLE"); + + // date + m.put("DATETIME", "TIMESTAMP"); + m.put("SMALLDATETIME", "TIMESTAMP"); + } + + private static final List KEYWORDS = Arrays.asList("abstract", "assert", "boolean", "break", "byte", + "case", "catch", "char", "class", "const", "continue", "default", "do", "double", "else", "enum", + "extends", "final", "finally", "float", "for", "goto", "if", "implements", "import", "instanceof", "int", + "interface", "long", "native", "new", "package", "private", "protected", "public", "return", "short", + "static", "strictfp", "super", "switch", "synchronized", "this", "throw", "throws", "transient", "try", + "void", "volatile", "while", "false", "null", "true"); + + /** + * Returns a SQL type mapping for a Java class. + * + * @param fieldDef + * the field to map + * @param strictTypeMapping + * throws a IciqlException if type is unsupported + * @return + */ + static String getDataType(FieldDefinition fieldDef, boolean strictTypeMapping) { + Class fieldClass = fieldDef.field.getType(); + if (SUPPORTED_TYPES.containsKey(fieldClass)) { + String type = SUPPORTED_TYPES.get(fieldClass); + if (type.equals("VARCHAR") && fieldDef.maxLength <= 0) { + // unspecified length strings are TEXT, not VARCHAR + return "TEXT"; + } + return type; + } + if (!strictTypeMapping) { + return "VARCHAR"; + } + throw new IciqlException("Unsupported type " + fieldClass.getName()); + } + + /** + * Returns the Java class for a given SQL type. + * + * @param sqlType + * @param dateTimeClass + * the preferred date class (java.util.Date or + * java.sql.Timestamp) + * @return + */ + static Class getClassForSqlType(String sqlType, Class dateTimeClass) { + sqlType = sqlType.toUpperCase(); + // XXX dropping "UNSIGNED" or parts like that could be trouble + sqlType = sqlType.split(" ")[0].trim(); + + if (SQL_TYPES.containsKey(sqlType)) { + // convert the sqlType to a standard type + sqlType = SQL_TYPES.get(sqlType); + } + Class mappedClass = null; + for (Class clazz : SUPPORTED_TYPES.keySet()) { + if (SUPPORTED_TYPES.get(clazz).equalsIgnoreCase(sqlType)) { + mappedClass = clazz; + break; + } + } + if (mappedClass != null) { + if (mappedClass.equals(java.util.Date.class) || mappedClass.equals(java.sql.Timestamp.class)) { + return dateTimeClass; + } + return mappedClass; + } + return null; + } + + /** + * Tries to create a convert a SQL table name to a camel case class name. + * + * @param tableName + * the SQL table name + * @return the class name + */ + static String convertTableToClassName(String tableName) { + String[] chunks = StringUtils.arraySplit(tableName, '_', false); + StringBuilder className = new StringBuilder(); + for (String chunk : chunks) { + if (chunk.length() == 0) { + // leading or trailing _ + continue; + } + className.append(Character.toUpperCase(chunk.charAt(0))); + className.append(chunk.substring(1).toLowerCase()); + } + return className.toString(); + } + + /** + * Ensures that SQL column names don't collide with Java keywords. + * + * @param columnName + * the column name + * @return the Java field name + */ + static String convertColumnToFieldName(String columnName) { + String lower = columnName.toLowerCase(); + if (KEYWORDS.contains(lower)) { + lower += "Value"; + } + return lower; + } + + /** + * Checks the formatting of IQColumn.defaultValue(). + * + * @param defaultValue + * the default value + * @return true if it is + */ + static boolean isProperlyFormattedDefaultValue(String defaultValue) { + if (isNullOrEmpty(defaultValue)) { + return true; + } + Pattern literalDefault = Pattern.compile("'.*'"); + Pattern functionDefault = Pattern.compile("[^'].*[^']"); + return literalDefault.matcher(defaultValue).matches() || functionDefault.matcher(defaultValue).matches(); + } + + /** + * Checks to see if the default value matches the class. + * + * @param modelClass + * the class + * @param defaultValue + * the value + * @return true if it does + */ + static boolean isValidDefaultValue(Class modelClass, String defaultValue) { + + if (defaultValue == null) { + // NULL + return true; + } + if (defaultValue.trim().length() == 0) { + // NULL (effectively) + return true; + } + + // TODO H2 single-quotes literal values, which is useful. + // MySQL does not single-quote literal values so its hard to + // differentiate a FUNCTION/VARIABLE from a literal value. + + // function / variable + Pattern functionDefault = Pattern.compile("[^'].*[^']"); + if (functionDefault.matcher(defaultValue).matches()) { + // hard to validate this since its in the database + // assume it is good + return true; + } + + // STRING + if (modelClass == String.class) { + Pattern stringDefault = Pattern.compile("'(.|\\n)*'"); + return stringDefault.matcher(defaultValue).matches(); + } + + String dateRegex = "[0-9]{1,4}[-/\\.][0-9]{1,2}[-/\\.][0-9]{1,2}"; + String timeRegex = "[0-2]{1}[0-9]{1}:[0-5]{1}[0-9]{1}:[0-5]{1}[0-9]{1}"; + + // TIMESTAMP + if (modelClass == java.util.Date.class || modelClass == java.sql.Timestamp.class) { + // this may be a little loose.... + // 00-00-00 00:00:00 + // 00/00/00T00:00:00 + // 00.00.00T00:00:00 + Pattern pattern = Pattern.compile("'" + dateRegex + "." + timeRegex + "'"); + return pattern.matcher(defaultValue).matches(); + } + + // DATE + if (modelClass == java.sql.Date.class) { + // this may be a little loose.... + // 00-00-00 + // 00/00/00 + // 00.00.00 + Pattern pattern = Pattern.compile("'" + dateRegex + "'"); + return pattern.matcher(defaultValue).matches(); + } + + // TIME + if (modelClass == java.sql.Time.class) { + // 00:00:00 + Pattern pattern = Pattern.compile("'" + timeRegex + "'"); + return pattern.matcher(defaultValue).matches(); + } + + // NUMBER + if (Number.class.isAssignableFrom(modelClass)) { + // strip single quotes + String unquoted = defaultValue; + if (unquoted.charAt(0) == '\'') { + unquoted = unquoted.substring(1); + } + if (unquoted.charAt(unquoted.length() - 1) == '\'') { + unquoted = unquoted.substring(0, unquoted.length() - 1); + } + + try { + // delegate to static valueOf() method to parse string + Method m = modelClass.getMethod("valueOf", String.class); + m.invoke(null, unquoted); + } catch (NumberFormatException ex) { + return false; + } catch (Throwable t) { + } + } + return true; + } +} diff --git a/src/com/iciql/OrderExpression.java b/src/com/iciql/OrderExpression.java new file mode 100644 index 0000000..36acf16 --- /dev/null +++ b/src/com/iciql/OrderExpression.java @@ -0,0 +1,55 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +/** + * An expression to order by in a query. + * + * @param + * the query data type + */ + +class OrderExpression { + private Query query; + private Object expression; + private boolean desc; + private boolean nullsFirst; + private boolean nullsLast; + + OrderExpression(Query query, Object expression, boolean desc, boolean nullsFirst, boolean nullsLast) { + this.query = query; + this.expression = expression; + this.desc = desc; + this.nullsFirst = nullsFirst; + this.nullsLast = nullsLast; + } + + void appendSQL(SQLStatement stat) { + query.appendSQL(stat, expression); + if (desc) { + stat.appendSQL(" DESC"); + } + if (nullsLast) { + stat.appendSQL(" NULLS LAST"); + } + if (nullsFirst) { + stat.appendSQL(" NULLS FIRST"); + } + } + +} diff --git a/src/com/iciql/Query.java b/src/com/iciql/Query.java new file mode 100644 index 0000000..97e143b --- /dev/null +++ b/src/com/iciql/Query.java @@ -0,0 +1,451 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +import java.lang.reflect.Field; +import java.sql.Clob; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; + +import com.iciql.bytecode.ClassReader; +import com.iciql.util.JdbcUtils; +import com.iciql.util.StatementLogger; +import com.iciql.util.Utils; + +/** + * This class represents a query. + * + * @param + * the return type + */ + +public class Query { + + private Db db; + private SelectTable from; + private ArrayList conditions = Utils.newArrayList(); + private ArrayList updateColumnDeclarations = Utils.newArrayList(); + private ArrayList> joins = Utils.newArrayList(); + private final IdentityHashMap> aliasMap = Utils.newIdentityHashMap(); + private ArrayList> orderByList = Utils.newArrayList(); + private Object[] groupByExpressions; + private long limit; + private long offset; + + private Query(Db db) { + this.db = db; + } + + /** + * from() is a static factory method to build a Query object. + * + * @param db + * @param alias + * @return a query object + */ + @SuppressWarnings("unchecked") + static Query from(Db db, T alias) { + Query query = new Query(db); + TableDefinition def = (TableDefinition) db.define(alias.getClass()); + query.from = new SelectTable(db, query, alias, false); + def.initSelectObject(query.from, alias, query.aliasMap); + return query; + } + + public long selectCount() { + SQLStatement stat = getSelectStatement(false); + stat.appendSQL("COUNT(*) "); + appendFromWhere(stat); + ResultSet rs = stat.executeQuery(); + try { + rs.next(); + long value = rs.getLong(1); + return value; + } catch (SQLException e) { + throw new IciqlException(e); + } finally { + JdbcUtils.closeSilently(rs, true); + } + } + + public List select() { + return select(false); + } + + public T selectFirst() { + return select(false).get(0); + } + + public List selectDistinct() { + return select(true); + } + + @SuppressWarnings("unchecked") + public X selectFirst(Z x) { + List list = (List) select(x); + return list.isEmpty() ? null : list.get(0); + } + + public String getSQL() { + SQLStatement stat = getSelectStatement(false); + stat.appendSQL("*"); + appendFromWhere(stat); + return stat.getSQL().trim(); + } + + private List select(boolean distinct) { + List result = Utils.newArrayList(); + TableDefinition def = from.getAliasDefinition(); + SQLStatement stat = getSelectStatement(distinct); + def.appendSelectList(stat); + appendFromWhere(stat); + ResultSet rs = stat.executeQuery(); + try { + while (rs.next()) { + T item = from.newObject(); + from.getAliasDefinition().readRow(item, rs); + result.add(item); + } + } catch (SQLException e) { + throw new IciqlException(e); + } finally { + JdbcUtils.closeSilently(rs, true); + } + return result; + } + + public int delete() { + SQLStatement stat = new SQLStatement(db); + stat.appendSQL("DELETE FROM "); + from.appendSQL(stat); + appendWhere(stat); + StatementLogger.delete(stat.getSQL()); + return stat.executeUpdate(); + } + + public
    UpdateColumnSet set(A field) { + return new UpdateColumnSet(this, field); + } + + public UpdateColumnIncrement increment(A field) { + return new UpdateColumnIncrement(this, field); + } + + public int update() { + if (updateColumnDeclarations.size() == 0) { + throw new IciqlException("Missing set or increment call."); + } + SQLStatement stat = new SQLStatement(db); + stat.appendSQL("UPDATE "); + from.appendSQL(stat); + stat.appendSQL(" SET "); + int i = 0; + for (UpdateColumn declaration : updateColumnDeclarations) { + if (i++ > 0) { + stat.appendSQL(", "); + } + declaration.appendSQL(stat); + } + appendWhere(stat); + StatementLogger.update(stat.getSQL()); + return stat.executeUpdate(); + } + + public List selectDistinct(Z x) { + return select(x, true); + } + + public List select(Z x) { + return select(x, false); + } + + @SuppressWarnings("unchecked") + private List select(Z x, boolean distinct) { + Class clazz = x.getClass(); + if (Utils.isSimpleType(clazz)) { + return selectSimple((X) x, distinct); + } + clazz = clazz.getSuperclass(); + return select((Class) clazz, (X) x, distinct); + } + + private List select(Class clazz, X x, boolean distinct) { + List result = Utils.newArrayList(); + TableDefinition def = db.define(clazz); + SQLStatement stat = getSelectStatement(distinct); + def.appendSelectList(stat, this, x); + appendFromWhere(stat); + ResultSet rs = stat.executeQuery(); + try { + while (rs.next()) { + X row = Utils.newObject(clazz); + def.readRow(row, rs); + result.add(row); + } + } catch (SQLException e) { + throw new IciqlException(e); + } finally { + JdbcUtils.closeSilently(rs, true); + } + return result; + } + + @SuppressWarnings("unchecked") + private List selectSimple(X x, boolean distinct) { + SQLStatement stat = getSelectStatement(distinct); + appendSQL(stat, x); + appendFromWhere(stat); + ResultSet rs = stat.executeQuery(); + List result = Utils.newArrayList(); + try { + while (rs.next()) { + try { + X value; + Object o = rs.getObject(1); + if (Clob.class.isAssignableFrom(o.getClass())) { + value = (X) Utils.convert(o, String.class); + } else { + value = (X) o; + } + result.add(value); + } catch (Exception e) { + throw new IciqlException(e); + } + } + } catch (SQLException e) { + throw new IciqlException(e); + } finally { + JdbcUtils.closeSilently(rs, true); + } + return result; + } + + private SQLStatement getSelectStatement(boolean distinct) { + SQLStatement stat = new SQLStatement(db); + stat.appendSQL("SELECT "); + if (distinct) { + stat.appendSQL("DISTINCT "); + } + return stat; + } + + public QueryCondition where(A x) { + return new QueryCondition(this, x); + } + + public QueryWhere where(Filter filter) { + HashMap fieldMap = Utils.newHashMap(); + for (Field f : filter.getClass().getDeclaredFields()) { + f.setAccessible(true); + try { + Object obj = f.get(filter); + if (obj == from.getAlias()) { + List fields = from.getAliasDefinition().getFields(); + String name = f.getName(); + for (TableDefinition.FieldDefinition field : fields) { + String n = name + "." + field.field.getName(); + Object o = field.field.get(obj); + fieldMap.put(n, o); + } + } + fieldMap.put(f.getName(), f.get(filter)); + } catch (Exception e) { + throw new IciqlException(e); + } + } + Token filterCode = new ClassReader().decompile(filter, fieldMap, "where"); + // String filterQuery = filterCode.toString(); + conditions.add(filterCode); + return new QueryWhere(this); + } + + public QueryWhere where(String fragment, Object... args) { + conditions.add(new RuntimeToken(fragment, args)); + return new QueryWhere(this); + } + + public QueryWhere whereTrue(Boolean condition) { + Token token = new Function("", condition); + addConditionToken(token); + return new QueryWhere(this); + } + + /** + * Sets the Limit and Offset of a query. + * + * @return the query + */ + + public Query limit(long limit) { + this.limit = limit; + return this; + } + + public Query offset(long offset) { + this.offset = offset; + return this; + } + + /** + * Order by a number of columns. + * + * @param expressions + * the columns + * @return the query + */ + + public Query orderBy(Object... expressions) { + for (Object expr : expressions) { + OrderExpression e = new OrderExpression(this, expr, false, false, false); + addOrderBy(e); + } + return this; + } + + public Query orderByDesc(Object expr) { + OrderExpression e = new OrderExpression(this, expr, true, false, false); + addOrderBy(e); + return this; + } + + public Query groupBy(Object... groupBy) { + this.groupByExpressions = groupBy; + return this; + } + + /** + * INTERNAL + * + * @param stat + * the statement + * @param x + * the alias object + */ + public void appendSQL(SQLStatement stat, Object x) { + if (x == Function.count()) { + stat.appendSQL("COUNT(*)"); + return; + } + Token token = Db.getToken(x); + if (token != null) { + token.appendSQL(stat, this); + return; + } + SelectColumn col = aliasMap.get(x); + if (col != null) { + col.appendSQL(stat); + return; + } + stat.appendSQL("?"); + stat.addParameter(x); + } + + void addConditionToken(Token condition) { + conditions.add(condition); + } + + void addUpdateColumnDeclaration(UpdateColumn declaration) { + updateColumnDeclarations.add(declaration); + } + + void appendWhere(SQLStatement stat) { + if (!conditions.isEmpty()) { + stat.appendSQL(" WHERE "); + for (Token token : conditions) { + token.appendSQL(stat, this); + stat.appendSQL(" "); + } + } + } + + void appendFromWhere(SQLStatement stat) { + stat.appendSQL(" FROM "); + from.appendSQL(stat); + for (SelectTable join : joins) { + join.appendSQLAsJoin(stat, this); + } + appendWhere(stat); + if (groupByExpressions != null) { + stat.appendSQL(" GROUP BY "); + int i = 0; + for (Object obj : groupByExpressions) { + if (i++ > 0) { + stat.appendSQL(", "); + } + appendSQL(stat, obj); + stat.appendSQL(" "); + } + } + if (!orderByList.isEmpty()) { + stat.appendSQL(" ORDER BY "); + int i = 0; + for (OrderExpression o : orderByList) { + if (i++ > 0) { + stat.appendSQL(", "); + } + o.appendSQL(stat); + stat.appendSQL(" "); + } + } + if (limit > 0) { + db.getDialect().appendLimit(stat, limit); + } + if (offset > 0) { + db.getDialect().appendOffset(stat, offset); + } + StatementLogger.select(stat.getSQL()); + } + + /** + * Join another table. + * + * @param alias + * an alias for the table to join + * @return the joined query + */ + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public QueryJoin innerJoin(U alias) { + TableDefinition def = (TableDefinition) db.define(alias.getClass()); + SelectTable join = new SelectTable(db, this, alias, false); + def.initSelectObject(join, alias, aliasMap); + joins.add(join); + return new QueryJoin(this, join); + } + + Db getDb() { + return db; + } + + boolean isJoin() { + return !joins.isEmpty(); + } + + SelectColumn getSelectColumn(Object obj) { + return aliasMap.get(obj); + } + + void addOrderBy(OrderExpression expr) { + orderByList.add(expr); + } + +} diff --git a/src/com/iciql/QueryCondition.java b/src/com/iciql/QueryCondition.java new file mode 100644 index 0000000..8e7cd42 --- /dev/null +++ b/src/com/iciql/QueryCondition.java @@ -0,0 +1,69 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +/** + * This class represents a query with an incomplete condition. + * + * @param + * the return type of the query + * @param + * the incomplete condition data type + */ + +public class QueryCondition { + + private Query query; + private A x; + + QueryCondition(Query query, A x) { + this.query = query; + this.x = x; + } + + public QueryWhere is(A y) { + query.addConditionToken(new Condition(x, y, CompareType.EQUAL)); + return new QueryWhere(query); + } + + public QueryWhere exceeds(A y) { + query.addConditionToken(new Condition(x, y, CompareType.EXCEEDS)); + return new QueryWhere(query); + } + + public QueryWhere atLeast(A y) { + query.addConditionToken(new Condition(x, y, CompareType.AT_LEAST)); + return new QueryWhere(query); + } + + public QueryWhere lessThan(A y) { + query.addConditionToken(new Condition(x, y, CompareType.LESS_THAN)); + return new QueryWhere(query); + } + + public QueryWhere atMost(A y) { + query.addConditionToken(new Condition(x, y, CompareType.AT_MOST)); + return new QueryWhere(query); + } + + public QueryWhere like(A pattern) { + query.addConditionToken(new Condition(x, pattern, CompareType.LIKE)); + return new QueryWhere(query); + } + +} diff --git a/src/com/iciql/QueryJoin.java b/src/com/iciql/QueryJoin.java new file mode 100644 index 0000000..bb614eb --- /dev/null +++ b/src/com/iciql/QueryJoin.java @@ -0,0 +1,37 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +/** + * This class represents a query with a join. + */ + +public class QueryJoin { + + private Query query; + private SelectTable join; + + QueryJoin(Query query, SelectTable join) { + this.query = query; + this.join = join; + } + + public QueryJoinCondition on(A x) { + return new QueryJoinCondition(query, join, x); + } +} diff --git a/src/com/iciql/QueryJoinCondition.java b/src/com/iciql/QueryJoinCondition.java new file mode 100644 index 0000000..e5620d5 --- /dev/null +++ b/src/com/iciql/QueryJoinCondition.java @@ -0,0 +1,43 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +/** + * This class represents a query with join and an incomplete condition. + * + * @param + * the incomplete condition data type + */ + +public class QueryJoinCondition { + + private Query query; + private SelectTable join; + private A x; + + QueryJoinCondition(Query query, SelectTable join, A x) { + this.query = query; + this.join = join; + this.x = x; + } + + public Query is(A y) { + join.addConditionToken(new Condition(x, y, CompareType.EQUAL)); + return query; + } +} diff --git a/src/com/iciql/QueryWhere.java b/src/com/iciql/QueryWhere.java new file mode 100644 index 0000000..9071b52 --- /dev/null +++ b/src/com/iciql/QueryWhere.java @@ -0,0 +1,148 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +import java.util.List; + +/** + * This class represents a query with a condition. + * + * @param + * the return type + */ + +public class QueryWhere { + + Query query; + + QueryWhere(Query query) { + this.query = query; + } + + public QueryCondition and(A x) { + query.addConditionToken(ConditionAndOr.AND); + return new QueryCondition(query, x); + } + + public QueryCondition or(A x) { + query.addConditionToken(ConditionAndOr.OR); + return new QueryCondition(query, x); + } + + public QueryWhere limit(long limit) { + query.limit(limit); + return this; + } + + public QueryWhere offset(long offset) { + query.offset(offset); + return this; + } + + public List select(Z x) { + return query.select(x); + } + + public String getSQL() { + SQLStatement stat = new SQLStatement(query.getDb()); + stat.appendSQL("SELECT *"); + query.appendFromWhere(stat); + return stat.getSQL().trim(); + } + + public List selectDistinct(Z x) { + return query.selectDistinct(x); + } + + public X selectFirst(Z x) { + List list = query.select(x); + return list.isEmpty() ? null : list.get(0); + } + + public List select() { + return query.select(); + } + + public T selectFirst() { + List list = select(); + return list.isEmpty() ? null : list.get(0); + } + + public List selectDistinct() { + return query.selectDistinct(); + } + + /** + * Order by a number of columns. + * + * @param expressions + * the order by expressions + * @return the query + */ + + public QueryWhere orderBy(Object... expressions) { + for (Object expr : expressions) { + OrderExpression e = new OrderExpression(query, expr, false, false, false); + query.addOrderBy(e); + } + return this; + } + + public QueryWhere orderByNullsFirst(Object expr) { + OrderExpression e = new OrderExpression(query, expr, false, true, false); + query.addOrderBy(e); + return this; + } + + public QueryWhere orderByNullsLast(Object expr) { + OrderExpression e = new OrderExpression(query, expr, false, false, true); + query.addOrderBy(e); + return this; + } + + public QueryWhere orderByDesc(Object expr) { + OrderExpression e = new OrderExpression(query, expr, true, false, false); + query.addOrderBy(e); + return this; + } + + public QueryWhere orderByDescNullsFirst(Object expr) { + OrderExpression e = new OrderExpression(query, expr, true, true, false); + query.addOrderBy(e); + return this; + } + + public QueryWhere orderByDescNullsLast(Object expr) { + OrderExpression e = new OrderExpression(query, expr, true, false, true); + query.addOrderBy(e); + return this; + } + + public int delete() { + return query.delete(); + } + + public int update() { + return query.update(); + } + + public long selectCount() { + return query.selectCount(); + } + +} diff --git a/src/com/iciql/RuntimeToken.java b/src/com/iciql/RuntimeToken.java new file mode 100644 index 0000000..cbfd882 --- /dev/null +++ b/src/com/iciql/RuntimeToken.java @@ -0,0 +1,57 @@ +/* + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.iciql; + +import java.text.MessageFormat; + +import com.iciql.util.StringUtils; + +/** + * Represents a traditional PreparedStatment fragment like "id=?, name=?". + * + */ +public class RuntimeToken implements Token { + + final String fragment; + final Object[] args; + + public RuntimeToken(String fragment, Object... args) { + this.fragment = fragment; + this.args = args == null ? new Object[0] : args; + } + + /** + * Append the SQL to the given statement using the given query. + * + * @param stat + * the statement to append the SQL to + * @param query + * the query to use + */ + @Override + public void appendSQL(SQLStatement stat, Query query) { + int tokenCount = StringUtils.count('?', fragment); + if (tokenCount != args.length) { + throw new IciqlException(MessageFormat.format( + "Fragment \"{0}\" specifies {1} tokens but you supplied {2} args", fragment, tokenCount, + args.length)); + } + stat.appendSQL(fragment); + for (Object arg : args) { + stat.addParameter(arg); + } + } +} diff --git a/src/com/iciql/SQLDialect.java b/src/com/iciql/SQLDialect.java new file mode 100644 index 0000000..3cb9339 --- /dev/null +++ b/src/com/iciql/SQLDialect.java @@ -0,0 +1,236 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +import com.iciql.TableDefinition.IndexDefinition; +import com.iciql.util.StatementBuilder; +import com.iciql.util.StringUtils; + +/** + * This interface defines points where iciql can build different statements + * depending on the database used. + */ +public interface SQLDialect { + + /** + * Configure the dialect from the database connection. + * + * @param conn + */ + void configureDialect(Connection conn); + + /** + * Returns a properly formatted table name for the dialect. + * + * @param schema + * the schema name, or null for no schema + * @param table + * the properly formatted table name + * @return the SQL snippet + */ + String prepareTableName(String schema, String table); + + /** + * Returns a properly formatted column name for the dialect. + * + * @param name + * the column name + * @return the properly formatted column name + */ + String prepareColumnName(String name); + + /** + * Get the CREATE INDEX statement. + * + * @param schema + * the schema name + * @param table + * the table name + * @param index + * the index definition + * @return the SQL statement + */ + String prepareCreateIndex(String schema, String table, IndexDefinition index); + + /** + * Append "LIMIT limit" to the SQL statement. + * + * @param stat + * the statement + * @param limit + * the limit + */ + void appendLimit(SQLStatement stat, long limit); + + /** + * Append "OFFSET offset" to the SQL statement. + * + * @param stat + * the statement + * @param offset + * the offset + */ + void appendOffset(SQLStatement stat, long offset); + + /** + * Whether memory tables are supported. + * + * @return true if they are + */ + boolean supportsMemoryTables(); + + /** + * Whether merge is a supported function. + * + * @return true if they are + */ + boolean supportsMerge(); + + /** + * Whether LIMIT/OFFSET notation is supported. + * + * @return true if they are + */ + boolean supportsLimitOffset(); + + /** + * Default implementation of an SQL dialect. + * Does not support merge nor index creation. + */ + public static class DefaultSQLDialect implements SQLDialect { + float databaseVersion; + String productName; + String productVersion; + + @Override + public String toString() { + return getClass().getName() + ": " + productName + " " + productVersion; + } + + @Override + public void configureDialect(Connection conn) { + loadIdentity(conn); + } + + protected void loadIdentity(Connection conn) { + try { + DatabaseMetaData data = conn.getMetaData(); + databaseVersion = Float.parseFloat(data.getDatabaseMajorVersion() + "." + + data.getDatabaseMinorVersion()); + productName = data.getDatabaseProductName(); + productVersion = data.getDatabaseProductVersion(); + } catch (SQLException e) { + throw new IciqlException(e); + } + } + + @Override + public boolean supportsMemoryTables() { + return false; + } + + @Override + public boolean supportsMerge() { + return false; + } + + @Override + public boolean supportsLimitOffset() { + return true; + } + + @Override + public String prepareTableName(String schema, String table) { + if (StringUtils.isNullOrEmpty(schema)) { + return table; + } + return schema + "." + table; + } + + @Override + public String prepareColumnName(String name) { + return name; + } + + @Override + public String prepareCreateIndex(String schema, String table, IndexDefinition index) { + throw new IciqlException("Dialect does not support index creation!"); + } + + @Override + public void appendLimit(SQLStatement stat, long limit) { + stat.appendSQL(" LIMIT " + limit); + } + + @Override + public void appendOffset(SQLStatement stat, long offset) { + stat.appendSQL(" OFFSET " + offset); + } + } + + + /** + * H2 database dialect. + */ + public static class H2Dialect extends DefaultSQLDialect { + + @Override + public boolean supportsMemoryTables() { + return true; + } + + @Override + public boolean supportsMerge() { + return true; + } + + @Override + public String prepareCreateIndex(String schema, String table, IndexDefinition index) { + StatementBuilder buff = new StatementBuilder(); + buff.append("CREATE "); + switch (index.type) { + case STANDARD: + break; + case UNIQUE: + buff.append("UNIQUE "); + break; + case HASH: + buff.append("HASH "); + break; + case UNIQUE_HASH: + buff.append("UNIQUE HASH "); + break; + } + buff.append("INDEX IF NOT EXISTS "); + buff.append(index.indexName); + buff.append(" ON "); + buff.append(table); + buff.append("("); + for (String col : index.columnNames) { + buff.appendExceptFirst(", "); + buff.append(col); + } + buff.append(")"); + return buff.toString(); + } + } +} diff --git a/src/com/iciql/SQLStatement.java b/src/com/iciql/SQLStatement.java new file mode 100644 index 0000000..b6c5e18 --- /dev/null +++ b/src/com/iciql/SQLStatement.java @@ -0,0 +1,128 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; + +import com.iciql.util.JdbcUtils; + +/** + * This class represents a parameterized SQL statement. + */ + +public class SQLStatement { + private Db db; + private StringBuilder buff = new StringBuilder(); + private String sql; + private ArrayList params = new ArrayList(); + + SQLStatement(Db db) { + this.db = db; + } + + void setSQL(String sql) { + this.sql = sql; + buff = new StringBuilder(sql); + } + + public SQLStatement appendSQL(String s) { + buff.append(s); + sql = null; + return this; + } + + public SQLStatement appendTable(String schema, String table) { + return appendSQL(db.getDialect().prepareTableName(schema, table)); + } + + public SQLStatement appendColumn(String column) { + return appendSQL(db.getDialect().prepareColumnName(column)); + } + + String getSQL() { + if (sql == null) { + sql = buff.toString(); + } + return sql; + } + + SQLStatement addParameter(Object o) { + params.add(o); + return this; + } + + ResultSet executeQuery() { + try { + return prepare(false).executeQuery(); + } catch (SQLException e) { + throw new IciqlException(e); + } + } + + int executeUpdate() { + PreparedStatement ps = null; + try { + ps = prepare(false); + return ps.executeUpdate(); + } catch (SQLException e) { + throw new IciqlException(e); + } finally { + JdbcUtils.closeSilently(ps); + } + } + + long executeInsert() { + PreparedStatement ps = null; + try { + ps = prepare(true); + ps.executeUpdate(); + long identity = -1; + ResultSet rs = ps.getGeneratedKeys(); + if (rs != null && rs.next()) { + identity = rs.getLong(1); + } + JdbcUtils.closeSilently(rs); + return identity; + } catch (SQLException e) { + throw new IciqlException(e); + } finally { + JdbcUtils.closeSilently(ps); + } + } + + private static void setValue(PreparedStatement prep, int parameterIndex, Object x) { + try { + prep.setObject(parameterIndex, x); + } catch (SQLException e) { + throw new IciqlException(e); + } + } + + private PreparedStatement prepare(boolean returnGeneratedKeys) { + PreparedStatement prep = db.prepare(getSQL(), returnGeneratedKeys); + for (int i = 0; i < params.size(); i++) { + Object o = params.get(i); + setValue(prep, i + 1, o); + } + return prep; + } + +} diff --git a/src/com/iciql/SelectColumn.java b/src/com/iciql/SelectColumn.java new file mode 100644 index 0000000..43a1a93 --- /dev/null +++ b/src/com/iciql/SelectColumn.java @@ -0,0 +1,57 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +import com.iciql.TableDefinition.FieldDefinition; + +/** + * This class represents a column of a table in a query. + * + * @param + * the table data type + */ + +class SelectColumn { + private SelectTable selectTable; + private FieldDefinition fieldDef; + + SelectColumn(SelectTable table, FieldDefinition fieldDef) { + this.selectTable = table; + this.fieldDef = fieldDef; + } + + void appendSQL(SQLStatement stat) { + if (selectTable.getQuery().isJoin()) { + stat.appendSQL(selectTable.getAs() + "." + fieldDef.columnName); + } else { + stat.appendColumn(fieldDef.columnName); + } + } + + FieldDefinition getFieldDefinition() { + return fieldDef; + } + + SelectTable getSelectTable() { + return selectTable; + } + + Object getCurrentValue() { + return fieldDef.getValue(selectTable.getCurrent()); + } +} diff --git a/src/com/iciql/SelectTable.java b/src/com/iciql/SelectTable.java new file mode 100644 index 0000000..7c5017c --- /dev/null +++ b/src/com/iciql/SelectTable.java @@ -0,0 +1,113 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +import java.util.ArrayList; + +import com.iciql.util.Utils; + +/** + * This class represents a table in a query. + * + * @param + * the table class + */ + +class SelectTable { + + private static int asCounter; + private Query query; + private Class clazz; + private T current; + private String as; + private TableDefinition aliasDef; + private boolean outerJoin; + private ArrayList joinConditions = Utils.newArrayList(); + private T alias; + + @SuppressWarnings("unchecked") + SelectTable(Db db, Query query, T alias, boolean outerJoin) { + this.alias = alias; + this.query = query; + this.outerJoin = outerJoin; + aliasDef = (TableDefinition) db.getTableDefinition(alias.getClass()); + clazz = Utils.getClass(alias); + as = "T" + asCounter++; + } + + T getAlias() { + return alias; + } + + T newObject() { + return Utils.newObject(clazz); + } + + TableDefinition getAliasDefinition() { + return aliasDef; + } + + void appendSQL(SQLStatement stat) { + if (query.isJoin()) { + stat.appendTable(aliasDef.schemaName, aliasDef.tableName).appendSQL(" AS " + as); + } else { + stat.appendTable(aliasDef.schemaName, aliasDef.tableName); + } + } + + void appendSQLAsJoin(SQLStatement stat, Query q) { + if (outerJoin) { + stat.appendSQL(" LEFT OUTER JOIN "); + } else { + stat.appendSQL(" INNER JOIN "); + } + appendSQL(stat); + if (!joinConditions.isEmpty()) { + stat.appendSQL(" ON "); + for (Token token : joinConditions) { + token.appendSQL(stat, q); + stat.appendSQL(" "); + } + } + } + + boolean getOuterJoin() { + return outerJoin; + } + + Query getQuery() { + return query; + } + + String getAs() { + return as; + } + + void addConditionToken(Token condition) { + joinConditions.add(condition); + } + + T getCurrent() { + return current; + } + + void setCurrent(T current) { + this.current = current; + } + +} diff --git a/src/com/iciql/TableDefinition.java b/src/com/iciql/TableDefinition.java new file mode 100644 index 0000000..883ce33 --- /dev/null +++ b/src/com/iciql/TableDefinition.java @@ -0,0 +1,669 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +import com.iciql.Iciql.IndexType; +import com.iciql.Iciql.IQColumn; +import com.iciql.Iciql.IQIndex; +import com.iciql.Iciql.IQSchema; +import com.iciql.Iciql.IQTable; +import com.iciql.util.StatementBuilder; +import com.iciql.util.StatementLogger; +import com.iciql.util.StringUtils; +import com.iciql.util.Utils; + +/** + * A table definition contains the index definitions of a table, the field + * definitions, the table name, and other meta data. + * + * @param + * the table type + */ + +class TableDefinition { + + /** + * The meta data of an index. + */ + + static class IndexDefinition { + IndexType type; + String indexName; + + List columnNames; + } + + /** + * The meta data of a field. + */ + + static class FieldDefinition { + String columnName; + Field field; + String dataType; + int maxLength; + boolean isPrimaryKey; + boolean isAutoIncrement; + boolean trimString; + boolean allowNull; + String defaultValue; + + Object getValue(Object obj) { + try { + return field.get(obj); + } catch (Exception e) { + throw new IciqlException(e); + } + } + + Object initWithNewObject(Object obj) { + Object o = Utils.newObject(field.getType()); + setValue(obj, o); + return o; + } + + void setValue(Object obj, Object o) { + try { + if (!field.isAccessible()) { + field.setAccessible(true); + } + o = Utils.convert(o, field.getType()); + field.set(obj, o); + } catch (Exception e) { + throw new IciqlException(e); + } + } + + Object read(ResultSet rs, int columnIndex) { + try { + return rs.getObject(columnIndex); + } catch (SQLException e) { + throw new IciqlException(e); + } + } + } + + String schemaName; + String tableName; + int tableVersion; + private boolean createTableIfRequired = true; + private Class clazz; + private ArrayList fields = Utils.newArrayList(); + private IdentityHashMap fieldMap = Utils.newIdentityHashMap(); + + private List primaryKeyColumnNames; + private ArrayList indexes = Utils.newArrayList(); + private boolean memoryTable; + + TableDefinition(Class clazz) { + this.clazz = clazz; + schemaName = null; + tableName = clazz.getSimpleName(); + } + + Class getModelClass() { + return clazz; + } + + List getFields() { + return fields; + } + + FieldDefinition getField(String name) { + for (FieldDefinition field:fields) { + if (field.columnName.equalsIgnoreCase(name)) { + return field; + } + } + return null; + } + + void setSchemaName(String schemaName) { + this.schemaName = schemaName; + } + + void setTableName(String tableName) { + this.tableName = tableName; + } + + /** + * Define a primary key by the specified model fields. + * + * @param modelFields + * the ordered list of model fields + */ + void setPrimaryKey(Object[] modelFields) { + List columnNames = mapColumnNames(modelFields); + setPrimaryKey(columnNames); + } + + /** + * Define a primary key by the specified column names. + * + * @param columnNames + * the ordered list of column names + */ + void setPrimaryKey(List columnNames) { + primaryKeyColumnNames = Utils.newArrayList(columnNames); + // set isPrimaryKey flag for all field definitions + for (FieldDefinition fieldDefinition : fieldMap.values()) { + fieldDefinition.isPrimaryKey = this.primaryKeyColumnNames.contains(fieldDefinition.columnName); + } + } + + String getColumnName(A fieldObject) { + FieldDefinition def = fieldMap.get(fieldObject); + return def == null ? null : def.columnName; + } + + private ArrayList mapColumnNames(Object[] columns) { + ArrayList columnNames = Utils.newArrayList(); + for (Object column : columns) { + columnNames.add(getColumnName(column)); + } + return columnNames; + } + + /** + * Defines an index with the specified model fields. + * + * @param type + * the index type (STANDARD, HASH, UNIQUE, UNIQUE_HASH) + * @param modelFields + * the ordered list of model fields + */ + void addIndex(IndexType type, Object[] modelFields) { + List columnNames = mapColumnNames(modelFields); + addIndex(type, columnNames); + } + + /** + * Defines an index with the specified column names. + * + * @param type + * the index type (STANDARD, HASH, UNIQUE, UNIQUE_HASH) + * @param columnNames + * the ordered list of column names + */ + void addIndex(IndexType type, List columnNames) { + IndexDefinition index = new IndexDefinition(); + index.indexName = tableName + "_" + indexes.size(); + index.columnNames = Utils.newArrayList(columnNames); + index.type = type; + indexes.add(index); + } + + public void setColumnName(Object column, String columnName) { + FieldDefinition def = fieldMap.get(column); + if (def != null) { + def.columnName = columnName; + } + } + + public void setMaxLength(Object column, int maxLength) { + FieldDefinition def = fieldMap.get(column); + if (def != null) { + def.maxLength = maxLength; + } + } + + void mapFields() { + boolean byAnnotationsOnly = false; + boolean inheritColumns = false; + boolean strictTypeMapping = false; + if (clazz.isAnnotationPresent(IQTable.class)) { + IQTable tableAnnotation = clazz.getAnnotation(IQTable.class); + byAnnotationsOnly = tableAnnotation.annotationsOnly(); + inheritColumns = tableAnnotation.inheritColumns(); + strictTypeMapping = tableAnnotation.strictTypeMapping(); + } + + List classFields = Utils.newArrayList(); + classFields.addAll(Arrays.asList(clazz.getDeclaredFields())); + if (inheritColumns) { + Class superClass = clazz.getSuperclass(); + classFields.addAll(Arrays.asList(superClass.getDeclaredFields())); + } + + for (Field f : classFields) { + // default to field name + String columnName = f.getName(); + boolean isAutoIncrement = false; + boolean isPrimaryKey = false; + int maxLength = 0; + boolean trimString = false; + boolean allowNull = true; + String defaultValue = ""; + boolean hasAnnotation = f.isAnnotationPresent(IQColumn.class); + if (hasAnnotation) { + IQColumn col = f.getAnnotation(IQColumn.class); + if (!StringUtils.isNullOrEmpty(col.name())) { + columnName = col.name(); + } + isAutoIncrement = col.autoIncrement(); + isPrimaryKey = col.primaryKey(); + maxLength = col.maxLength(); + trimString = col.trimString(); + allowNull = col.allowNull(); + defaultValue = col.defaultValue(); + } + boolean isPublic = Modifier.isPublic(f.getModifiers()); + boolean reflectiveMatch = isPublic && !byAnnotationsOnly; + if (reflectiveMatch || hasAnnotation) { + FieldDefinition fieldDef = new FieldDefinition(); + fieldDef.field = f; + fieldDef.columnName = columnName; + fieldDef.isAutoIncrement = isAutoIncrement; + fieldDef.isPrimaryKey = isPrimaryKey; + fieldDef.maxLength = maxLength; + fieldDef.trimString = trimString; + fieldDef.allowNull = allowNull; + fieldDef.defaultValue = defaultValue; + fieldDef.dataType = ModelUtils.getDataType(fieldDef, strictTypeMapping); + fields.add(fieldDef); + } + } + List primaryKey = Utils.newArrayList(); + for (FieldDefinition fieldDef : fields) { + if (fieldDef.isPrimaryKey) { + primaryKey.add(fieldDef.columnName); + } + } + if (primaryKey.size() > 0) { + setPrimaryKey(primaryKey); + } + } + + /** + * Optionally truncates strings to the maximum length + */ + private Object getValue(Object obj, FieldDefinition field) { + Object value = field.getValue(obj); + if (field.trimString && field.maxLength > 0) { + if (value instanceof String) { + // clip strings + String s = (String) value; + if (s.length() > field.maxLength) { + return s.substring(0, field.maxLength); + } + return s; + } + return value; + } + // standard behavior + return value; + } + + long insert(Db db, Object obj, boolean returnKey) { + SQLStatement stat = new SQLStatement(db); + StatementBuilder buff = new StatementBuilder("INSERT INTO "); + buff.append(db.getDialect().prepareTableName(schemaName, tableName)).append('('); + for (FieldDefinition field : fields) { + buff.appendExceptFirst(", "); + buff.append(db.getDialect().prepareColumnName(field.columnName)); + } + buff.append(") VALUES("); + buff.resetCount(); + for (FieldDefinition field : fields) { + buff.appendExceptFirst(", "); + buff.append('?'); + Object value = getValue(obj, field); + stat.addParameter(value); + } + buff.append(')'); + stat.setSQL(buff.toString()); + StatementLogger.insert(stat.getSQL()); + if (returnKey) { + return stat.executeInsert(); + } + return stat.executeUpdate(); + } + + void merge(Db db, Object obj) { + if (primaryKeyColumnNames == null || primaryKeyColumnNames.size() == 0) { + throw new IllegalStateException("No primary key columns defined " + "for table " + obj.getClass() + + " - no update possible"); + } + SQLStatement stat = new SQLStatement(db); + StatementBuilder buff = new StatementBuilder("MERGE INTO "); + buff.append(db.getDialect().prepareTableName(schemaName, tableName)).append(" ("); + buff.resetCount(); + for (FieldDefinition field : fields) { + buff.appendExceptFirst(", "); + buff.append(db.getDialect().prepareColumnName(field.columnName)); + } + buff.append(") KEY("); + buff.resetCount(); + for (FieldDefinition field : fields) { + if (field.isPrimaryKey) { + buff.appendExceptFirst(", "); + buff.append(db.getDialect().prepareColumnName(field.columnName)); + } + } + buff.append(") "); + buff.resetCount(); + buff.append("VALUES ("); + for (FieldDefinition field : fields) { + buff.appendExceptFirst(", "); + buff.append('?'); + Object value = getValue(obj, field); + stat.addParameter(value); + } + buff.append(')'); + stat.setSQL(buff.toString()); + + StatementLogger.merge(stat.getSQL()); + stat.executeUpdate(); + } + + void update(Db db, Object obj) { + if (primaryKeyColumnNames == null || primaryKeyColumnNames.size() == 0) { + throw new IllegalStateException("No primary key columns defined " + "for table " + obj.getClass() + + " - no update possible"); + } + SQLStatement stat = new SQLStatement(db); + StatementBuilder buff = new StatementBuilder("UPDATE "); + buff.append(db.getDialect().prepareTableName(schemaName, tableName)).append(" SET "); + buff.resetCount(); + + for (FieldDefinition field : fields) { + if (!field.isPrimaryKey) { + buff.appendExceptFirst(", "); + buff.append(db.getDialect().prepareColumnName(field.columnName)); + buff.append(" = ?"); + Object value = getValue(obj, field); + stat.addParameter(value); + } + } + Object alias = Utils.newObject(obj.getClass()); + Query query = Query.from(db, alias); + boolean firstCondition = true; + for (FieldDefinition field : fields) { + if (field.isPrimaryKey) { + Object aliasValue = field.getValue(alias); + Object value = field.getValue(obj); + if (!firstCondition) { + query.addConditionToken(ConditionAndOr.AND); + } + firstCondition = false; + query.addConditionToken(new Condition(aliasValue, value, CompareType.EQUAL)); + } + } + stat.setSQL(buff.toString()); + query.appendWhere(stat); + StatementLogger.update(stat.getSQL()); + stat.executeUpdate(); + } + + void delete(Db db, Object obj) { + if (primaryKeyColumnNames == null || primaryKeyColumnNames.size() == 0) { + throw new IllegalStateException("No primary key columns defined " + "for table " + obj.getClass() + + " - no update possible"); + } + SQLStatement stat = new SQLStatement(db); + StatementBuilder buff = new StatementBuilder("DELETE FROM "); + buff.append(db.getDialect().prepareTableName(schemaName, tableName)); + buff.resetCount(); + Object alias = Utils.newObject(obj.getClass()); + Query query = Query.from(db, alias); + boolean firstCondition = true; + for (FieldDefinition field : fields) { + if (field.isPrimaryKey) { + Object aliasValue = field.getValue(alias); + Object value = field.getValue(obj); + if (!firstCondition) { + query.addConditionToken(ConditionAndOr.AND); + } + firstCondition = false; + query.addConditionToken(new Condition(aliasValue, value, CompareType.EQUAL)); + } + } + stat.setSQL(buff.toString()); + query.appendWhere(stat); + StatementLogger.delete(stat.getSQL()); + stat.executeUpdate(); + } + + TableDefinition createTableIfRequired(Db db) { + if (!createTableIfRequired) { + // skip table and index creation + // but still check for upgrades + db.upgradeTable(this); + return this; + } + SQLDialect dialect = db.getDialect(); + SQLStatement stat = new SQLStatement(db); + StatementBuilder buff; + if (memoryTable && dialect.supportsMemoryTables()) { + buff = new StatementBuilder("CREATE MEMORY TABLE IF NOT EXISTS "); + } else { + buff = new StatementBuilder("CREATE TABLE IF NOT EXISTS "); + } + + buff.append(dialect.prepareTableName(schemaName, tableName)).append('('); + + for (FieldDefinition field : fields) { + buff.appendExceptFirst(", "); + buff.append(dialect.prepareColumnName(field.columnName)).append(' ').append(field.dataType); + if (field.maxLength > 0) { + buff.append('(').append(field.maxLength).append(')'); + } + + if (field.isAutoIncrement) { + buff.append(" AUTO_INCREMENT"); + } + + if (!field.allowNull) { + buff.append(" NOT NULL"); + } + + // default values + if (!field.isAutoIncrement && !field.isPrimaryKey) { + String dv = field.defaultValue; + if (!StringUtils.isNullOrEmpty(dv)) { + if (ModelUtils.isProperlyFormattedDefaultValue(dv) + && ModelUtils.isValidDefaultValue(field.field.getType(), dv)) { + buff.append(" DEFAULT " + dv); + } + } + } + } + + // primary key + if (primaryKeyColumnNames != null && primaryKeyColumnNames.size() > 0) { + buff.append(", PRIMARY KEY("); + buff.resetCount(); + for (String n : primaryKeyColumnNames) { + buff.appendExceptFirst(", "); + buff.append(n); + } + buff.append(')'); + } + buff.append(')'); + stat.setSQL(buff.toString()); + StatementLogger.create(stat.getSQL()); + stat.executeUpdate(); + + // create indexes + for (IndexDefinition index : indexes) { + String sql = db.getDialect().prepareCreateIndex(schemaName, tableName, index); + stat.setSQL(sql); + StatementLogger.create(stat.getSQL()); + stat.executeUpdate(); + } + + // tables are created using IF NOT EXISTS + // but we may still need to upgrade + db.upgradeTable(this); + return this; + } + + /** + * Retrieve list of columns from index definition. + * + * @param index + * the index columns, separated by space + * @return the column list + */ + private List getColumns(String index) { + List cols = Utils.newArrayList(); + if (index == null || index.length() == 0) { + return null; + } + String[] cs = index.split("(,|\\s)"); + for (String c : cs) { + if (c != null && c.trim().length() > 0) { + cols.add(c.trim()); + } + } + if (cols.size() == 0) { + return null; + } + return cols; + } + + void mapObject(Object obj) { + fieldMap.clear(); + initObject(obj, fieldMap); + + if (clazz.isAnnotationPresent(IQSchema.class)) { + IQSchema schemaAnnotation = clazz.getAnnotation(IQSchema.class); + // setup schema name mapping, if properly annotated + if (!StringUtils.isNullOrEmpty(schemaAnnotation.name())) { + schemaName = schemaAnnotation.name(); + } + } + + if (clazz.isAnnotationPresent(IQTable.class)) { + IQTable tableAnnotation = clazz.getAnnotation(IQTable.class); + + // setup table name mapping, if properly annotated + if (!StringUtils.isNullOrEmpty(tableAnnotation.name())) { + tableName = tableAnnotation.name(); + } + + // allow control over createTableIfRequired() + createTableIfRequired = tableAnnotation.createIfRequired(); + + // model version + if (tableAnnotation.version() > 0) { + tableVersion = tableAnnotation.version(); + } + + // setup the primary index, if properly annotated + List primaryKey = getColumns(tableAnnotation.primaryKey()); + if (primaryKey != null) { + setPrimaryKey(primaryKey); + } + } + + if (clazz.isAnnotationPresent(IQIndex.class)) { + IQIndex indexAnnotation = clazz.getAnnotation(IQIndex.class); + + // setup the indexes, if properly annotated + addIndexes(IndexType.STANDARD, indexAnnotation.standard()); + addIndexes(IndexType.UNIQUE, indexAnnotation.unique()); + addIndexes(IndexType.HASH, indexAnnotation.hash()); + addIndexes(IndexType.UNIQUE_HASH, indexAnnotation.uniqueHash()); + } + } + + void addIndexes(IndexType type, String[] indexes) { + for (String index : indexes) { + List validatedColumns = getColumns(index); + if (validatedColumns == null) { + return; + } + addIndex(type, validatedColumns); + } + } + + List getIndexes(IndexType type) { + List list = Utils.newArrayList(); + for (IndexDefinition def : indexes) { + if (def.type.equals(type)) { + list.add(def); + } + } + return list; + } + + void initObject(Object obj, Map map) { + for (FieldDefinition def : fields) { + Object newValue = def.initWithNewObject(obj); + map.put(newValue, def); + } + } + + void initSelectObject(SelectTable table, Object obj, Map> map) { + for (FieldDefinition def : fields) { + Object newValue = def.initWithNewObject(obj); + SelectColumn column = new SelectColumn(table, def); + map.put(newValue, column); + } + } + + void readRow(Object item, ResultSet rs) { + for (int i = 0; i < fields.size(); i++) { + FieldDefinition def = fields.get(i); + Object o = def.read(rs, i + 1); + def.setValue(item, o); + } + } + + void appendSelectList(SQLStatement stat) { + for (int i = 0; i < fields.size(); i++) { + if (i > 0) { + stat.appendSQL(", "); + } + FieldDefinition def = fields.get(i); + stat.appendColumn(def.columnName); + } + } + + void appendSelectList(SQLStatement stat, Query query, X x) { + for (int i = 0; i < fields.size(); i++) { + if (i > 0) { + stat.appendSQL(", "); + } + FieldDefinition def = fields.get(i); + Object obj = def.getValue(x); + query.appendSQL(stat, obj); + } + } + + void copyAttributeValues(Query query, X to, X map) { + for (FieldDefinition def : fields) { + Object obj = def.getValue(map); + SelectColumn col = query.getSelectColumn(obj); + Object value = col.getCurrentValue(); + def.setValue(to, value); + } + } + +} diff --git a/src/com/iciql/TableInspector.java b/src/com/iciql/TableInspector.java new file mode 100644 index 0000000..f920315 --- /dev/null +++ b/src/com/iciql/TableInspector.java @@ -0,0 +1,674 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +import static com.iciql.ValidationRemark.consider; +import static com.iciql.ValidationRemark.error; +import static com.iciql.ValidationRemark.warn; +import static com.iciql.util.JdbcUtils.closeSilently; +import static com.iciql.util.StringUtils.isNullOrEmpty; +import static java.text.MessageFormat.format; +import java.lang.reflect.Modifier; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.iciql.Iciql.IndexType; +import com.iciql.Iciql.IQColumn; +import com.iciql.Iciql.IQIndex; +import com.iciql.Iciql.IQSchema; +import com.iciql.Iciql.IQTable; +import com.iciql.TableDefinition.FieldDefinition; +import com.iciql.TableDefinition.IndexDefinition; +import com.iciql.util.StatementBuilder; +import com.iciql.util.Utils; + +/** + * Class to inspect the contents of a particular table including its indexes. + * This class does the bulk of the work in terms of model generation and model + * validation. + */ +public class TableInspector { + + private String schema; + private String table; + private boolean forceUpperCase; + private Class dateTimeClass; + private List primaryKeys = Utils.newArrayList(); + private Map indexes; + private Map columns; + private final String eol = "\n"; + + TableInspector(String schema, String table, boolean forceUpperCase, + Class dateTimeClass) { + this.schema = schema; + this.table = table; + this.forceUpperCase = forceUpperCase; + this.dateTimeClass = dateTimeClass; + } + + /** + * Tests to see if this TableInspector represents schema.table. + *

    + * + * @param schema + * the schema name + * @param table + * the table name + * @return true if the table matches + */ + boolean matches(String schema, String table) { + if (isNullOrEmpty(schema)) { + // table name matching + return this.table.equalsIgnoreCase(table); + } else if (isNullOrEmpty(table)) { + // schema name matching + return this.schema.equalsIgnoreCase(schema); + } else { + // exact table matching + return this.schema.equalsIgnoreCase(schema) && this.table.equalsIgnoreCase(table); + } + } + + /** + * Reads the DatabaseMetaData for the details of this table including + * primary keys and indexes. + * + * @param metaData + * the database meta data + */ + void read(DatabaseMetaData metaData) throws SQLException { + ResultSet rs = null; + + // primary keys + try { + rs = metaData.getPrimaryKeys(null, schema, table); + while (rs.next()) { + String c = rs.getString("COLUMN_NAME"); + primaryKeys.add(c); + } + closeSilently(rs); + + // indexes + rs = metaData.getIndexInfo(null, schema, table, false, true); + indexes = Utils.newHashMap(); + while (rs.next()) { + IndexInspector info = new IndexInspector(rs); + if (info.type.equals(IndexType.UNIQUE) && info.name.toLowerCase().startsWith("primary")) { + // skip primary key indexes + continue; + } + if (indexes.containsKey(info.name)) { + indexes.get(info.name).addColumn(rs); + } else { + indexes.put(info.name, info); + } + } + closeSilently(rs); + + // columns + rs = metaData.getColumns(null, schema, table, null); + columns = Utils.newHashMap(); + while (rs.next()) { + ColumnInspector col = new ColumnInspector(); + col.name = rs.getString("COLUMN_NAME"); + col.type = rs.getString("TYPE_NAME"); + col.clazz = ModelUtils.getClassForSqlType(col.type, dateTimeClass); + col.size = rs.getInt("COLUMN_SIZE"); + col.allowNull = rs.getInt("NULLABLE") == DatabaseMetaData.columnNullable; + col.isAutoIncrement = rs.getBoolean("IS_AUTOINCREMENT"); + if (primaryKeys.size() == 1) { + if (col.name.equalsIgnoreCase(primaryKeys.get(0))) { + col.isPrimaryKey = true; + } + } + if (!col.isAutoIncrement) { + col.defaultValue = rs.getString("COLUMN_DEF"); + } + columns.put(col.name, col); + } + } finally { + closeSilently(rs); + } + } + + /** + * Generates a model (class definition) from this table. The model includes + * indexes, primary keys, default values, maxLengths, and allowNull + * information. + *

    + * The caller may optionally set a destination package name, whether or not + * to include the schema name (setting schema can be a problem when using + * the model between databases), and if to automatically trim strings for + * those that have a maximum length. + *

    + * + * @param packageName + * @param annotateSchema + * @param trimStrings + * @return a complete model (class definition) for this table as a string + */ + String generateModel(String packageName, boolean annotateSchema, boolean trimStrings) { + + // import statements + Set imports = Utils.newHashSet(); + imports.add(IQSchema.class.getCanonicalName()); + imports.add(IQTable.class.getCanonicalName()); + imports.add(IQIndex.class.getCanonicalName()); + imports.add(IQColumn.class.getCanonicalName()); + + // fields + StringBuilder fields = new StringBuilder(); + List sortedColumns = Utils.newArrayList(columns.values()); + Collections.sort(sortedColumns); + for (ColumnInspector col : sortedColumns) { + fields.append(generateColumn(imports, col, trimStrings)); + } + + // build complete class definition + StringBuilder model = new StringBuilder(); + if (!isNullOrEmpty(packageName)) { + // package + model.append("package " + packageName + ";"); + model.append(eol).append(eol); + } + + // imports + List sortedImports = new ArrayList(imports); + Collections.sort(sortedImports); + for (String imp : sortedImports) { + model.append("import ").append(imp).append(';').append(eol); + } + model.append(eol); + + // @IQSchema + if (annotateSchema && !isNullOrEmpty(schema)) { + model.append('@').append(IQSchema.class.getSimpleName()); + model.append('('); + AnnotationBuilder ap = new AnnotationBuilder(); + ap.addParameter("name", schema); + model.append(ap); + model.append(')').append(eol); + } + + // @IQTable + model.append('@').append(IQTable.class.getSimpleName()); + model.append('('); + + // IQTable annotation parameters + AnnotationBuilder ap = new AnnotationBuilder(); + ap.addParameter("name", table); + + if (primaryKeys.size() > 1) { + StringBuilder pk = new StringBuilder(); + for (String key : primaryKeys) { + pk.append(key).append(' '); + } + pk.trimToSize(); + ap.addParameter("primaryKey", pk.toString()); + } + + // finish @IQTable annotation + model.append(ap); + model.append(')').append(eol); + + // @IQIndex + ap = new AnnotationBuilder(); + generateIndexAnnotations(ap, "standard", IndexType.STANDARD); + generateIndexAnnotations(ap, "unique", IndexType.UNIQUE); + generateIndexAnnotations(ap, "hash", IndexType.HASH); + generateIndexAnnotations(ap, "uniqueHash", IndexType.UNIQUE_HASH); + if (ap.length() > 0) { + model.append('@').append(IQIndex.class.getSimpleName()); + model.append('('); + model.append(ap); + model.append(')').append(eol); + } + + // class declaration + String clazzName = ModelUtils.convertTableToClassName(table); + model.append(format("public class {0} '{'", clazzName)).append(eol); + model.append(eol); + + // field declarations + model.append(fields); + + // default constructor + model.append("\t" + "public ").append(clazzName).append("() {").append(eol); + model.append("\t}").append(eol); + + // end of class body + model.append('}'); + model.trimToSize(); + return model.toString(); + } + + /** + * Generates the specified index annotation. + * + * @param ap + */ + void generateIndexAnnotations(AnnotationBuilder ap, String parameter, IndexType type) { + List list = getIndexes(type); + if (list.size() == 0) { + // no matching indexes + return; + } + if (list.size() == 1) { + ap.addParameter(parameter, list.get(0).getColumnsString()); + } else { + List parameters = Utils.newArrayList(); + for (IndexInspector index : list) { + parameters.add(index.getColumnsString()); + } + ap.addParameter(parameter, parameters); + } + + } + + private List getIndexes(IndexType type) { + List list = Utils.newArrayList(); + for (IndexInspector index : indexes.values()) { + if (index.type.equals(type)) { + list.add(index); + } + } + return list; + } + + private StatementBuilder generateColumn(Set imports, ColumnInspector col, boolean trimStrings) { + StatementBuilder sb = new StatementBuilder(); + Class clazz = col.clazz; + String column = ModelUtils.convertColumnToFieldName(col.name.toLowerCase()); + sb.append('\t'); + if (clazz == null) { + // unsupported type + clazz = Object.class; + sb.append("// unsupported type " + col.type); + } else { + // @IQColumn + imports.add(clazz.getCanonicalName()); + sb.append('@').append(IQColumn.class.getSimpleName()); + + // IQColumn annotation parameters + AnnotationBuilder ap = new AnnotationBuilder(); + + // IQColumn.name + if (!col.name.equalsIgnoreCase(column)) { + ap.addParameter("name", col.name); + } + + // IQColumn.primaryKey + // composite primary keys are annotated on the table + if (col.isPrimaryKey && primaryKeys.size() == 1) { + ap.addParameter("primaryKey=true"); + } + + // IQColumn.maxLength + if ((clazz == String.class) && (col.size > 0) && (col.size < Integer.MAX_VALUE)) { + ap.addParameter("maxLength", col.size); + + // IQColumn.trimStrings + if (trimStrings) { + ap.addParameter("trimString=true"); + } + } else { + // IQColumn.AutoIncrement + if (col.isAutoIncrement) { + ap.addParameter("autoIncrement=true"); + } + } + + // IQColumn.allowNull + if (!col.allowNull) { + ap.addParameter("allowNull=false"); + } + + // IQColumn.defaultValue + if (!isNullOrEmpty(col.defaultValue)) { + ap.addParameter("defaultValue=\"" + col.defaultValue + "\""); + } + + // add leading and trailing () + if (ap.length() > 0) { + ap.insert(0, '('); + ap.append(')'); + } + sb.append(ap); + } + sb.append(eol); + + // variable declaration + sb.append("\t" + "public "); + sb.append(clazz.getSimpleName()); + sb.append(' '); + sb.append(column); + sb.append(';'); + sb.append(eol).append(eol); + return sb; + } + + /** + * Validates that a table definition (annotated, interface, or both) matches + * the current state of the table and indexes in the database. Results are + * returned as a list of validation remarks which includes recommendations, + * warnings, and errors about the model. The caller may choose to have + * validate throw an exception on any validation ERROR. + * + * @param def + * the table definition + * @param throwError + * whether or not to throw an exception if an error was found + * @return a list if validation remarks + */ + List validate(TableDefinition def, boolean throwError) { + List remarks = Utils.newArrayList(); + + // model class definition validation + if (!Modifier.isPublic(def.getModelClass().getModifiers())) { + remarks.add(error(table, "SCHEMA", + format("Class {0} MUST BE PUBLIC!", def.getModelClass().getCanonicalName())).throwError( + throwError)); + } + + // Schema Validation + if (!isNullOrEmpty(schema)) { + if (isNullOrEmpty(def.schemaName)) { + remarks.add(consider(table, "SCHEMA", + format("@{0}(name={1})", IQSchema.class.getSimpleName(), schema))); + } else if (!schema.equalsIgnoreCase(def.schemaName)) { + remarks.add(error( + table, + "SCHEMA", + format("@{0}(name={1}) != {2}", IQSchema.class.getSimpleName(), def.schemaName, + schema)).throwError(throwError)); + } + } + + // index validation + for (IndexInspector index : indexes.values()) { + validate(remarks, def, index, throwError); + } + + // field column validation + for (FieldDefinition fieldDef : def.getFields()) { + validate(remarks, fieldDef, throwError); + } + return remarks; + } + + /** + * Validates an inspected index from the database against the + * IndexDefinition within the TableDefinition. + */ + private void validate(List remarks, TableDefinition def, IndexInspector index, + boolean throwError) { + List defIndexes = def.getIndexes(IndexType.STANDARD); + List dbIndexes = getIndexes(IndexType.STANDARD); + if (defIndexes.size() > dbIndexes.size()) { + remarks.add(warn(table, IndexType.STANDARD.name(), "More model indexes than database indexes")); + } else if (defIndexes.size() < dbIndexes.size()) { + remarks.add(warn(table, IndexType.STANDARD.name(), "Model class is missing indexes")); + } + // TODO complete index validation. + // need to actually compare index types and columns within each index. + } + + /** + * Validates a column against the model's field definition. Checks for + * existence, supported type, type mapping, default value, defined lengths, + * primary key, autoincrement. + */ + private void validate(List remarks, FieldDefinition fieldDef, boolean throwError) { + // unknown field + String field = forceUpperCase ? fieldDef.columnName.toUpperCase() : fieldDef.columnName; + if (!columns.containsKey(field)) { + // unknown column mapping + remarks.add(error(table, fieldDef, "Does not exist in database!").throwError(throwError)); + return; + } + ColumnInspector col = columns.get(field); + Class fieldClass = fieldDef.field.getType(); + Class jdbcClass = ModelUtils.getClassForSqlType(col.type, dateTimeClass); + + // supported type check + // iciql maps to VARCHAR for unsupported types. + if (fieldDef.dataType.equals("VARCHAR") && (fieldClass != String.class)) { + remarks.add(error(table, fieldDef, + "iciql does not currently implement support for " + fieldClass.getName()).throwError( + throwError)); + } + // number types + if (!fieldClass.equals(jdbcClass)) { + if (Number.class.isAssignableFrom(fieldClass)) { + remarks.add(warn( + table, + col, + format("Precision mismatch: ModelObject={0}, ColumnObject={1}", + fieldClass.getSimpleName(), jdbcClass.getSimpleName()))); + } else { + if (!Date.class.isAssignableFrom(jdbcClass)) { + remarks.add(warn( + table, + col, + format("Object Mismatch: ModelObject={0}, ColumnObject={1}", + fieldClass.getSimpleName(), jdbcClass.getSimpleName()))); + } + } + } + + // string types + if (fieldClass == String.class) { + if ((fieldDef.maxLength != col.size) && (col.size < Integer.MAX_VALUE)) { + remarks.add(warn( + table, + col, + format("{0}.maxLength={1}, ColumnMaxLength={2}", IQColumn.class.getSimpleName(), + fieldDef.maxLength, col.size))); + } + if (fieldDef.maxLength > 0 && !fieldDef.trimString) { + remarks.add(consider(table, col, + format("{0}.truncateToMaxLength=true" + " will prevent IciqlExceptions on" + + " INSERT or UPDATE, but will clip data!", IQColumn.class.getSimpleName()))); + } + } + + // numeric autoIncrement + if (fieldDef.isAutoIncrement != col.isAutoIncrement) { + remarks.add(warn( + table, + col, + format("{0}.isAutoIncrement={1}" + " while Column autoIncrement={2}", + IQColumn.class.getSimpleName(), fieldDef.isAutoIncrement, col.isAutoIncrement))); + } + // default value + if (!col.isAutoIncrement && !col.isPrimaryKey) { + // check Model.defaultValue format + if (!ModelUtils.isProperlyFormattedDefaultValue(fieldDef.defaultValue)) { + remarks.add(error( + table, + col, + format("{0}.defaultValue=\"{1}\"" + " is improperly formatted!", + IQColumn.class.getSimpleName(), fieldDef.defaultValue)) + .throwError(throwError)); + // next field + return; + } + // compare Model.defaultValue to Column.defaultValue + if (isNullOrEmpty(fieldDef.defaultValue) && !isNullOrEmpty(col.defaultValue)) { + // Model.defaultValue is NULL, Column.defaultValue is NOT NULL + remarks.add(warn( + table, + col, + format("{0}.defaultValue=\"\"" + " while column default=\"{1}\"", + IQColumn.class.getSimpleName(), col.defaultValue))); + } else if (!isNullOrEmpty(fieldDef.defaultValue) && isNullOrEmpty(col.defaultValue)) { + // Column.defaultValue is NULL, Model.defaultValue is NOT NULL + remarks.add(warn( + table, + col, + format("{0}.defaultValue=\"{1}\"" + " while column default=\"\"", + IQColumn.class.getSimpleName(), fieldDef.defaultValue))); + } else if (!isNullOrEmpty(fieldDef.defaultValue) && !isNullOrEmpty(col.defaultValue)) { + if (!fieldDef.defaultValue.equals(col.defaultValue)) { + // Model.defaultValue != Column.defaultValue + remarks.add(warn( + table, + col, + format("{0}.defaultValue=\"{1}\"" + " while column default=\"{2}\"", + IQColumn.class.getSimpleName(), fieldDef.defaultValue, col.defaultValue))); + } + } + + // sanity check Model.defaultValue literal value + if (!ModelUtils.isValidDefaultValue(fieldDef.field.getType(), fieldDef.defaultValue)) { + remarks.add(error( + table, + col, + format("{0}.defaultValue=\"{1}\" is invalid!", IQColumn.class.getSimpleName(), + fieldDef.defaultValue))); + } + } + } + + /** + * Represents an index as it exists in the database. + */ + private static class IndexInspector { + + String name; + IndexType type; + private List columns = new ArrayList(); + + public IndexInspector(ResultSet rs) throws SQLException { + name = rs.getString("INDEX_NAME"); + + // determine index type + boolean hash = rs.getInt("TYPE") == DatabaseMetaData.tableIndexHashed; + boolean unique = !rs.getBoolean("NON_UNIQUE"); + + if (!hash && !unique) { + type = IndexType.STANDARD; + } else if (hash && unique) { + type = IndexType.UNIQUE_HASH; + } else if (unique) { + type = IndexType.UNIQUE; + } else if (hash) { + type = IndexType.HASH; + } + columns.add(rs.getString("COLUMN_NAME")); + } + + public void addColumn(ResultSet rs) throws SQLException { + columns.add(rs.getString("COLUMN_NAME")); + } + + public String getColumnsString() { + StatementBuilder sb = new StatementBuilder(); + for (String col : columns) { + sb.appendExceptFirst(", "); + sb.append(col); + } + return sb.toString().trim(); + } + } + + /** + * Represents a column as it exists in the database. + */ + static class ColumnInspector implements Comparable { + String name; + String type; + int size; + boolean allowNull; + Class clazz; + boolean isPrimaryKey; + boolean isAutoIncrement; + String defaultValue; + + public int compareTo(ColumnInspector o) { + if (isPrimaryKey && o.isPrimaryKey) { + // both primary sort by name + return name.compareTo(o.name); + } else if (isPrimaryKey && !o.isPrimaryKey) { + // primary first + return -1; + } else if (!isPrimaryKey && o.isPrimaryKey) { + // primary first + return 1; + } else { + // neither primary, sort by name + return name.compareTo(o.name); + } + } + } + + /** + * Convenience class based on StatementBuilder for creating the annotation + * parameter list. + */ + private static class AnnotationBuilder extends StatementBuilder { + AnnotationBuilder() { + super(); + } + + void addParameter(String parameter) { + appendExceptFirst(", "); + append(parameter); + } + + void addParameter(String parameter, T value) { + appendExceptFirst(", "); + append(parameter); + append('='); + if (value instanceof List) { + append("{ "); + List list = (List) value; + StatementBuilder flat = new StatementBuilder(); + for (Object o : list) { + flat.appendExceptFirst(", "); + if (o instanceof String) { + flat.append('\"'); + } + // TODO escape string + flat.append(o.toString().trim()); + if (o instanceof String) { + flat.append('\"'); + } + } + append(flat); + append(" }"); + } else { + if (value instanceof String) { + append('\"'); + } + // TODO escape + append(value.toString().trim()); + if (value instanceof String) { + append('\"'); + } + } + } + } +} \ No newline at end of file diff --git a/src/com/iciql/TestCondition.java b/src/com/iciql/TestCondition.java new file mode 100644 index 0000000..b4c1c15 --- /dev/null +++ b/src/com/iciql/TestCondition.java @@ -0,0 +1,115 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +import com.iciql.util.Utils; + +/** + * This class represents an incomplete condition. + * + * @param + * the incomplete condition data type + */ + +public class TestCondition { + + private A x; + + public TestCondition(A x) { + this.x = x; + } + + public Boolean is(A y) { + Boolean o = Utils.newObject(Boolean.class); + return Db.registerToken(o, new Function("=", x, y) { + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL("("); + query.appendSQL(stat, x[0]); + stat.appendSQL(" = "); + query.appendSQL(stat, x[1]); + stat.appendSQL(")"); + } + }); + } + + public Boolean bigger(A y) { + Boolean o = Utils.newObject(Boolean.class); + return Db.registerToken(o, new Function(">", x, y) { + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL("("); + query.appendSQL(stat, x[0]); + stat.appendSQL(" > "); + query.appendSQL(stat, x[1]); + stat.appendSQL(")"); + } + }); + } + + public Boolean biggerEqual(A y) { + Boolean o = Utils.newObject(Boolean.class); + return Db.registerToken(o, new Function(">=", x, y) { + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL("("); + query.appendSQL(stat, x[0]); + stat.appendSQL(" >= "); + query.appendSQL(stat, x[1]); + stat.appendSQL(")"); + } + }); + } + + public Boolean smaller(A y) { + Boolean o = Utils.newObject(Boolean.class); + return Db.registerToken(o, new Function("<", x, y) { + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL("("); + query.appendSQL(stat, x[0]); + stat.appendSQL(" < "); + query.appendSQL(stat, x[1]); + stat.appendSQL(")"); + } + }); + } + + public Boolean smallerEqual(A y) { + Boolean o = Utils.newObject(Boolean.class); + return Db.registerToken(o, new Function("<=", x, y) { + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL("("); + query.appendSQL(stat, x[0]); + stat.appendSQL(" <= "); + query.appendSQL(stat, x[1]); + stat.appendSQL(")"); + } + }); + } + + public Boolean like(A pattern) { + Boolean o = Utils.newObject(Boolean.class); + return Db.registerToken(o, new Function("LIKE", x, pattern) { + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL("("); + query.appendSQL(stat, x[0]); + stat.appendSQL(" LIKE "); + query.appendSQL(stat, x[1]); + stat.appendSQL(")"); + } + }); + } + +} diff --git a/src/com/iciql/Token.java b/src/com/iciql/Token.java new file mode 100644 index 0000000..cc2203c --- /dev/null +++ b/src/com/iciql/Token.java @@ -0,0 +1,35 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +/** + * Classes implementing this interface can be used as a token in a statement. + */ +public interface Token { + /** + * Append the SQL to the given statement using the given query. + * + * @param stat + * the statement to append the SQL to + * @param query + * the query to use + */ + + void appendSQL(SQLStatement stat, Query query); + +} diff --git a/src/com/iciql/UpdateColumn.java b/src/com/iciql/UpdateColumn.java new file mode 100644 index 0000000..1eaf14c --- /dev/null +++ b/src/com/iciql/UpdateColumn.java @@ -0,0 +1,35 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +/** + * Classes implementing this interface can be used as a declaration in an update + * statement. + */ +public interface UpdateColumn { + + /** + * Append the SQL to the given statement using the given query. + * + * @param stat + * the statement to append the SQL to + */ + + void appendSQL(SQLStatement stat); + +} diff --git a/src/com/iciql/UpdateColumnIncrement.java b/src/com/iciql/UpdateColumnIncrement.java new file mode 100644 index 0000000..43ef6d6 --- /dev/null +++ b/src/com/iciql/UpdateColumnIncrement.java @@ -0,0 +1,55 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +/** + * This class represents "SET column = (column + x)" in an UPDATE statement. + * + * @param + * the query type + * @param + * the new value data type + */ + +public class UpdateColumnIncrement implements UpdateColumn { + + private Query query; + private A x; + private A y; + + UpdateColumnIncrement(Query query, A x) { + this.query = query; + this.x = x; + } + + public Query by(A y) { + query.addUpdateColumnDeclaration(this); + this.y = y; + return query; + } + + public void appendSQL(SQLStatement stat) { + query.appendSQL(stat, x); + stat.appendSQL("=("); + query.appendSQL(stat, x); + stat.appendSQL("+"); + query.appendSQL(stat, y); + stat.appendSQL(")"); + } + +} diff --git a/src/com/iciql/UpdateColumnSet.java b/src/com/iciql/UpdateColumnSet.java new file mode 100644 index 0000000..55d3790 --- /dev/null +++ b/src/com/iciql/UpdateColumnSet.java @@ -0,0 +1,52 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +/** + * This class represents "SET column = value" in an UPDATE statement. + * + * @param + * the query type + * @param + * the new value data type + */ + +public class UpdateColumnSet implements UpdateColumn { + + private Query query; + private A x; + private A y; + + UpdateColumnSet(Query query, A x) { + this.query = query; + this.x = x; + } + + public Query to(A y) { + query.addUpdateColumnDeclaration(this); + this.y = y; + return query; + } + + public void appendSQL(SQLStatement stat) { + query.appendSQL(stat, x); + stat.appendSQL("=?"); + stat.addParameter(y); + } + +} diff --git a/src/com/iciql/ValidationRemark.java b/src/com/iciql/ValidationRemark.java new file mode 100644 index 0000000..a68bf21 --- /dev/null +++ b/src/com/iciql/ValidationRemark.java @@ -0,0 +1,131 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql; + +import com.iciql.TableDefinition.FieldDefinition; +import com.iciql.TableInspector.ColumnInspector; +import com.iciql.util.StringUtils; + +/** + * A validation remark is a result of running a model validation. Each remark + * has a level, associated component (schema, table, column, index), and a + * message. + */ +public class ValidationRemark { + + /** + * The validation message level. + */ + public static enum Level { + CONSIDER, WARN, ERROR; + } + + private Level level; + private String table; + private String fieldType; + private String fieldName; + private String message; + + private ValidationRemark(Level level, String table, String type, String message) { + this.level = level; + this.table = table; + this.fieldType = type; + this.fieldName = ""; + this.message = message; + } + + private ValidationRemark(Level level, String table, FieldDefinition field, String message) { + this.level = level; + this.table = table; + this.fieldType = field.dataType; + this.fieldName = field.columnName; + this.message = message; + } + + private ValidationRemark(Level level, String table, ColumnInspector col, String message) { + this.level = level; + this.table = table; + this.fieldType = col.type; + this.fieldName = col.name; + this.message = message; + } + + public static ValidationRemark consider(String table, String type, String message) { + return new ValidationRemark(Level.CONSIDER, table, type, message); + } + + public static ValidationRemark consider(String table, ColumnInspector col, String message) { + return new ValidationRemark(Level.CONSIDER, table, col, message); + } + + public static ValidationRemark warn(String table, ColumnInspector col, String message) { + return new ValidationRemark(Level.WARN, table, col, message); + } + + public static ValidationRemark warn(String table, String type, String message) { + return new ValidationRemark(Level.WARN, table, type, message); + } + + public static ValidationRemark error(String table, ColumnInspector col, String message) { + return new ValidationRemark(Level.ERROR, table, col, message); + } + + public static ValidationRemark error(String table, String type, String message) { + return new ValidationRemark(Level.ERROR, table, type, message); + } + + public static ValidationRemark error(String table, FieldDefinition field, String message) { + return new ValidationRemark(Level.ERROR, table, field, message); + } + + public ValidationRemark throwError(boolean throwOnError) { + if (throwOnError && isError()) { + throw new IciqlException(toString()); + } + return this; + } + + public boolean isError() { + return level.equals(Level.ERROR); + } + + public Level getLevel() { + return level; + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(StringUtils.pad(level.name(), 9, " ", true)); + sb.append(StringUtils.pad(table, 25, " ", true)); + sb.append(StringUtils.pad(fieldName, 20, " ", true)); + sb.append(' '); + sb.append(message); + return sb.toString(); + } + + public String toCSVString() { + StringBuilder sb = new StringBuilder(); + sb.append(level.name()).append(','); + sb.append(table).append(','); + sb.append(fieldType).append(','); + sb.append(fieldName).append(','); + sb.append(message); + return sb.toString(); + } + +} diff --git a/src/com/iciql/build/Build.java b/src/com/iciql/build/Build.java new file mode 100644 index 0000000..5963d16 --- /dev/null +++ b/src/com/iciql/build/Build.java @@ -0,0 +1,234 @@ +/* + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.build; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import com.iciql.util.StringUtils; + +/** + * The Build class downloads runtime and compile-time jar files from the Apache + * Maven repositories. + * + * Its important that this class have minimal compile dependencies since its + * called very early in the build script. + * + */ +public class Build { + + /** + * BuildTypes + */ + public static enum BuildType { + RUNTIME, COMPILETIME; + } + + public static void main(String... args) { + runtime(); + compiletime(); + } + + public static void runtime() { + } + + public static void compiletime() { + downloadFromApache(MavenObject.H2, BuildType.RUNTIME); + downloadFromApache(MavenObject.H2, BuildType.COMPILETIME); + downloadFromApache(MavenObject.JCOMMANDER, BuildType.RUNTIME); + downloadFromApache(MavenObject.JCOMMANDER, BuildType.COMPILETIME); + downloadFromApache(MavenObject.MARKDOWNPAPERS, BuildType.RUNTIME); + downloadFromApache(MavenObject.MARKDOWNPAPERS, BuildType.COMPILETIME); + downloadFromApache(MavenObject.JUNIT, BuildType.RUNTIME); + downloadFromApache(MavenObject.DOCLAVA, BuildType.RUNTIME); + downloadFromApache(MavenObject.DOCLAVA, BuildType.COMPILETIME); + downloadFromApache(MavenObject.SLF4JAPI, BuildType.RUNTIME); + downloadFromApache(MavenObject.SLF4JAPI, BuildType.COMPILETIME); + + // needed for site publishing + downloadFromApache(MavenObject.COMMONSNET, BuildType.RUNTIME); + } + + /** + * Download a file from the official Apache Maven repository. + * + * @param mo + * the maven object to download. + * @return + */ + private static List downloadFromApache(MavenObject mo, BuildType type) { + return downloadFromMaven("http://repo1.maven.org/maven2/", mo, type); + } + + /** + * Download a file from a Maven repository. + * + * @param mo + * the maven object to download. + * @return + */ + private static List downloadFromMaven(String mavenRoot, MavenObject mo, BuildType type) { + List downloads = new ArrayList(); + String[] jars = { "" }; + if (BuildType.RUNTIME.equals(type)) { + jars = new String[] { "" }; + } else if (BuildType.COMPILETIME.equals(type)) { + jars = new String[] { "-sources", "-javadoc" }; + } + for (String jar : jars) { + File targetFile = mo.getLocalFile("ext", jar); + if (targetFile.exists()) { + downloads.add(targetFile); + continue; + } + String expectedSHA1 = mo.getSHA1(jar); + if (expectedSHA1 == null) { + // skip this jar + continue; + } + String mavenURL = mavenRoot + mo.getRepositoryPath(jar); + if (!targetFile.getAbsoluteFile().getParentFile().exists()) { + boolean success = targetFile.getAbsoluteFile().getParentFile().mkdirs(); + if (!success) { + throw new RuntimeException("Failed to create destination folder structure!"); + } + } + ByteArrayOutputStream buff = new ByteArrayOutputStream(); + try { + URL url = new URL(mavenURL); + InputStream in = new BufferedInputStream(url.openStream()); + byte[] buffer = new byte[4096]; + + System.out.println("d/l: " + targetFile.getName()); + while (true) { + int len = in.read(buffer); + if (len < 0) { + break; + } + buff.write(buffer, 0, len); + } + in.close(); + + } catch (IOException e) { + throw new RuntimeException("Error downloading " + mavenURL + " to " + targetFile, e); + } + byte[] data = buff.toByteArray(); + String calculatedSHA1 = StringUtils.calculateSHA1(data); + + System.out.println(); + + if (expectedSHA1.length() == 0) { + System.out.println("sha: " + calculatedSHA1); + System.out.println(); + } else { + if (!calculatedSHA1.equals(expectedSHA1)) { + throw new RuntimeException("SHA1 checksum mismatch; got: " + calculatedSHA1); + } + } + try { + RandomAccessFile ra = new RandomAccessFile(targetFile, "rw"); + ra.write(data); + ra.setLength(data.length); + ra.close(); + } catch (IOException e) { + throw new RuntimeException("Error writing to file " + targetFile, e); + } + downloads.add(targetFile); + } + return downloads; + } + + /** + * Class that describes a retrievable Maven object. + */ + private static class MavenObject { + + public static final MavenObject JCOMMANDER = new MavenObject("com/beust", "jcommander", "1.17", + "219a3540f3b27d7cc3b1d91d6ea046cd8723290e", "0bb50eec177acf0e94d58e0cf07262fe5164331d", + "c7adc475ca40c288c93054e0f4fe58f3a98c0cb5"); + + public static final MavenObject H2 = new MavenObject("com/h2database", "h2", "1.3.158", + "4bac13427caeb32ef6e93b70101e61f370c7b5e2", "6bb165156a0831879fa7797df6e18bdcd4421f2d", + "446d3f58c44992534cb54f67134532d95961904a"); + + public static final MavenObject JUNIT = new MavenObject("junit", "junit", "4.8.2", + "c94f54227b08100974c36170dcb53329435fe5ad", "", ""); + + public static final MavenObject MARKDOWNPAPERS = new MavenObject("org/tautua/markdownpapers", + "markdownpapers-core", "1.1.0", "b879b4720fa642d3c490ab559af132daaa16dbb4", + "d98c53939815be2777d5a56dcdc3bbc9ddb468fa", "4c09d2d3073e85b973572292af00bd69681df76b"); + + public static final MavenObject COMMONSNET = new MavenObject("commons-net", "commons-net", "1.4.0", + "eb47e8cad2dd7f92fd7e77df1d1529cae87361f7", "", ""); + + public static final MavenObject DOCLAVA = new MavenObject("com/google/doclava", "doclava", "1.0.3", + "5a1e05977fd36480b0cf314410440f88e3a0049e", "6e314df1733455d66b98b56014363172773d0905", + "1c1aa631b235439356e6e5803319caca80aaaa88"); + + public static final MavenObject SLF4JAPI = new MavenObject("org/slf4j", "slf4j-api", "1.6.1", + "6f3b8a24bf970f17289b234284c94f43eb42f0e4", "46a386136c901748e6a3af67ebde6c22bc6b4524", + "e223571d77769cdafde59040da235842f3326453"); + + public final String group; + public final String artifact; + public final String version; + public final String librarySHA1; + public final String sourcesSHA1; + public final String javadocSHA1; + + private MavenObject(String group, String artifact, String version, String librarySHA1, + String sourcesSHA1, String javadocSHA1) { + this.group = group; + this.artifact = artifact; + this.version = version; + this.librarySHA1 = librarySHA1; + this.sourcesSHA1 = sourcesSHA1; + this.javadocSHA1 = javadocSHA1; + } + + private String getRepositoryPath(String jar) { + return group + "/" + artifact + "/" + version + "/" + artifact + "-" + version + jar + ".jar"; + } + + private File getLocalFile(String basePath, String jar) { + return new File(basePath, artifact + "-" + version + jar + ".jar"); + } + + private String getSHA1(String jar) { + if (jar.equals("")) { + return librarySHA1; + } else if (jar.equals("-sources")) { + return sourcesSHA1; + } else if (jar.equals("-javadoc")) { + return javadocSHA1; + } + return librarySHA1; + } + + @Override + public String toString() { + return group; + } + } +} diff --git a/src/com/iciql/build/BuildSite.java b/src/com/iciql/build/BuildSite.java new file mode 100644 index 0000000..0686def --- /dev/null +++ b/src/com/iciql/build/BuildSite.java @@ -0,0 +1,320 @@ +/* + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.build; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.nio.charset.Charset; +import java.text.MessageFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Vector; + +import org.tautua.markdownpapers.Markdown; + +import com.beust.jcommander.JCommander; +import com.beust.jcommander.Parameter; +import com.beust.jcommander.ParameterException; +import com.beust.jcommander.Parameters; +import com.iciql.Constants; +import com.iciql.util.StringUtils; + +/** + * Builds the web site or deployment documentation from Markdown source files. + * + * All Markdown source files must have the .mkd extension. + * + * Natural string sort order of the Markdown source filenames is the order of + * page links. "##_" prefixes are used to control the sort order. + * + * @author James Moger + * + */ +public class BuildSite { + + public static void main(String... args) { + Params params = new Params(); + JCommander jc = new JCommander(params); + try { + jc.parse(args); + } catch (ParameterException t) { + usage(jc, t); + } + + File sourceFolder = new File(params.sourceFolder); + File destinationFolder = new File(params.outputFolder); + File[] markdownFiles = sourceFolder.listFiles(new FilenameFilter() { + + @Override + public boolean accept(File dir, String name) { + return name.toLowerCase().endsWith(".mkd"); + } + }); + Arrays.sort(markdownFiles); + + Map aliasMap = new HashMap(); + for (String alias : params.aliases) { + String[] values = alias.split("="); + aliasMap.put(values[0], values[1]); + } + + System.out.println(MessageFormat.format("Generating site from {0} Markdown Docs in {1} ", markdownFiles.length, + sourceFolder.getAbsolutePath())); + String linkPattern = "{1}"; + StringBuilder sb = new StringBuilder(); + for (File file : markdownFiles) { + String documentName = getDocumentName(file); + if (!params.skips.contains(documentName)) { + String displayName = documentName; + if (aliasMap.containsKey(documentName)) { + displayName = aliasMap.get(documentName); + } else { + displayName = displayName.replace('_', ' '); + } + String fileName = documentName + ".html"; + sb.append(MessageFormat.format(linkPattern, fileName, displayName)); + sb.append(" | "); + } + } + sb.setLength(sb.length() - 3); + sb.trimToSize(); + + String htmlHeader = readContent(new File(params.pageHeader), "\n"); + + String htmlAdSnippet = null; + if (!StringUtils.isNullOrEmpty(params.adSnippet)) { + File snippet = new File(params.adSnippet); + if (snippet.exists()) { + htmlAdSnippet = readContent(snippet, "\n"); + } + } + String htmlFooter = readContent(new File(params.pageFooter), "\n"); + String links = sb.toString(); + String header = MessageFormat.format(htmlHeader, Constants.NAME, links); + if (!StringUtils.isNullOrEmpty(params.analyticsSnippet)) { + File snippet = new File(params.analyticsSnippet); + if (snippet.exists()) { + String htmlSnippet = readContent(snippet, "\n"); + header = header.replace("", htmlSnippet); + } + } + final String date = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); + final String footer = MessageFormat.format(htmlFooter, "generated " + date); + for (File file : markdownFiles) { + try { + String documentName = getDocumentName(file); + if (!params.skips.contains(documentName)) { + String fileName = documentName + ".html"; + System.out.println(MessageFormat.format(" {0} => {1}", file.getName(), fileName)); + String rawContent = readContent(file, "\n"); + String markdownContent = rawContent; + + Map> nomarkdownMap = new HashMap>(); + + // extract sections marked as no-markdown + int nmd = 0; + for (String token : params.nomarkdown) { + StringBuilder strippedContent = new StringBuilder(); + + String nomarkdownKey = "%NOMARKDOWN" + nmd + "%"; + String[] kv = token.split(":", 2); + String beginToken = kv[0]; + String endToken = kv[1]; + + // strip nomarkdown chunks from markdown and cache them + List chunks = new Vector(); + int beginCode = 0; + int endCode = 0; + while ((beginCode = markdownContent.indexOf(beginToken, endCode)) > -1) { + if (endCode == 0) { + strippedContent.append(markdownContent.substring(0, beginCode)); + } else { + strippedContent.append(markdownContent.substring(endCode, beginCode)); + } + strippedContent.append(nomarkdownKey); + endCode = markdownContent.indexOf(endToken, beginCode); + chunks.add(markdownContent.substring(beginCode, endCode)); + nomarkdownMap.put(nomarkdownKey, chunks); + } + + // get remainder of text + if (endCode < markdownContent.length()) { + strippedContent.append(markdownContent.substring(endCode, markdownContent.length())); + } + markdownContent = strippedContent.toString(); + nmd++; + } + + // transform markdown to html + String content = transformMarkdown(new StringReader(markdownContent.toString())); + + // reinsert nomarkdown chunks + for (Map.Entry> nomarkdown : nomarkdownMap.entrySet()) { + for (String chunk : nomarkdown.getValue()) { + content = content.replaceFirst(nomarkdown.getKey(), chunk); + } + } + + // perform specified substitutions + for (String token : params.substitutions) { + String[] kv = token.split("=", 2); + content = content.replace(kv[0], kv[1]); + } + + OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(new File(destinationFolder, + fileName)), Charset.forName("UTF-8")); + writer.write(header); + if (!StringUtils.isNullOrEmpty(htmlAdSnippet)) { + writer.write(htmlAdSnippet); + } + writer.write(content); + writer.write(footer); + writer.close(); + } + } catch (Throwable t) { + System.err.println("Failed to transform " + file.getName()); + t.printStackTrace(); + } + } + } + + private static String getDocumentName(File file) { + String displayName = file.getName().substring(0, file.getName().lastIndexOf('.')).toLowerCase(); + int underscore = displayName.indexOf('_') + 1; + if (underscore > -1) { + // trim leading ##_ which is to control display order + return displayName.substring(underscore); + } + return displayName; + } + + /** + * Returns the string content of the specified file. + * + * @param file + * @param lineEnding + * @return the string content of the file + */ + private static String readContent(File file, String lineEnding) { + StringBuilder sb = new StringBuilder(); + try { + InputStreamReader is = new InputStreamReader(new FileInputStream(file), Charset.forName("UTF-8")); + BufferedReader reader = new BufferedReader(is); + String line = null; + while ((line = reader.readLine()) != null) { + sb.append(line); + if (lineEnding != null) { + sb.append(lineEnding); + } + } + reader.close(); + } catch (Throwable t) { + System.err.println("Failed to read content of " + file.getAbsolutePath()); + t.printStackTrace(); + } + return sb.toString(); + } + + private static String transformMarkdown(Reader markdownReader) throws ParseException { + // Read raw markdown content and transform it to html + StringWriter writer = new StringWriter(); + try { + Markdown md = new Markdown(); + md.transform(markdownReader, writer); + return writer.toString().trim(); + } catch (org.tautua.markdownpapers.parser.ParseException p) { + throw new java.text.ParseException(p.getMessage(), 0); + } finally { + try { + markdownReader.close(); + } catch (IOException e) { + // IGNORE + } + try { + writer.close(); + } catch (IOException e) { + // IGNORE + } + } + } + + private static void usage(JCommander jc, ParameterException t) { + System.out.println(Constants.NAME + " v" + Constants.VERSION); + System.out.println(); + if (t != null) { + System.out.println(t.getMessage()); + System.out.println(); + } + if (jc != null) { + jc.usage(); + } + System.exit(0); + } + + /** + * Command-line parameters for BuildSite utility. + */ + @Parameters(separators = " ") + private static class Params { + + @Parameter(names = { "--sourceFolder" }, description = "Markdown Source Folder", required = true) + public String sourceFolder; + + @Parameter(names = { "--outputFolder" }, description = "HTML Ouptut Folder", required = true) + public String outputFolder; + + @Parameter(names = { "--pageHeader" }, description = "Page Header HTML Snippet", required = true) + public String pageHeader; + + @Parameter(names = { "--pageFooter" }, description = "Page Footer HTML Snippet", required = true) + public String pageFooter; + + @Parameter(names = { "--adSnippet" }, description = "Ad HTML Snippet", required = false) + public String adSnippet; + + @Parameter(names = { "--analyticsSnippet" }, description = "Analytics HTML Snippet", required = false) + public String analyticsSnippet; + + @Parameter(names = { "--skip" }, description = "Filename to skip", required = false) + public List skips = new ArrayList(); + + @Parameter(names = { "--alias" }, description = "Filename=Linkname aliases", required = false) + public List aliases = new ArrayList(); + + @Parameter(names = { "--substitute" }, description = "%TOKEN%=value", required = false) + public List substitutions = new ArrayList(); + + @Parameter(names = { "--nomarkdown" }, description = "%STARTTOKEN%:%ENDTOKEN%", required = false) + public List nomarkdown = new ArrayList(); + + } +} diff --git a/src/com/iciql/bytecode/And.java b/src/com/iciql/bytecode/And.java new file mode 100644 index 0000000..808a9c1 --- /dev/null +++ b/src/com/iciql/bytecode/And.java @@ -0,0 +1,46 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; +import com.iciql.Token; + +/** + * An AND expression. + */ +public class And implements Token { + + private final Token left, right; + + private And(Token left, Token right) { + this.left = left; + this.right = right; + } + + static And get(Token left, Token right) { + return new And(left, right); + } + + public void appendSQL(SQLStatement stat, Query query) { + left.appendSQL(stat, query); + stat.appendSQL(" AND "); + right.appendSQL(stat, query); + } + +} diff --git a/src/com/iciql/bytecode/ArrayGet.java b/src/com/iciql/bytecode/ArrayGet.java new file mode 100644 index 0000000..29516c2 --- /dev/null +++ b/src/com/iciql/bytecode/ArrayGet.java @@ -0,0 +1,49 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; +import com.iciql.Token; + +/** + * An array access operation. + */ +public class ArrayGet implements Token { + + private final Token variable; + private final Token index; + + private ArrayGet(Token variable, Token index) { + this.variable = variable; + this.index = index; + } + + static ArrayGet get(Token variable, Token index) { + return new ArrayGet(variable, index); + } + + public void appendSQL(SQLStatement stat, Query query) { + // untested + variable.appendSQL(stat, query); + stat.appendSQL("["); + index.appendSQL(stat, query); + stat.appendSQL("]"); + } + +} diff --git a/src/com/iciql/bytecode/CaseWhen.java b/src/com/iciql/bytecode/CaseWhen.java new file mode 100644 index 0000000..2a1d69e --- /dev/null +++ b/src/com/iciql/bytecode/CaseWhen.java @@ -0,0 +1,62 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; +import com.iciql.Token; + +/** + * A conditional expression. + */ +public class CaseWhen implements Token { + + private final Token condition, ifTrue, ifFalse; + + private CaseWhen(Token condition, Token ifTrue, Token ifFalse) { + this.condition = condition; + this.ifTrue = ifTrue; + this.ifFalse = ifFalse; + } + + static Token get(Token condition, Token ifTrue, Token ifFalse) { + if ("0".equals(ifTrue.toString()) && "1".equals(ifFalse.toString())) { + return Not.get(condition); + } else if ("1".equals(ifTrue.toString()) && "0".equals(ifFalse.toString())) { + return condition; + } else if ("0".equals(ifTrue.toString())) { + return And.get(Not.get(condition), ifFalse); + } + return new CaseWhen(condition, ifTrue, ifFalse); + } + + public String toString() { + return "CASEWHEN(" + condition + ", " + ifTrue + ", " + ifFalse + ")"; + } + + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL("CASEWHEN "); + condition.appendSQL(stat, query); + stat.appendSQL(" THEN "); + ifTrue.appendSQL(stat, query); + stat.appendSQL(" ELSE "); + ifFalse.appendSQL(stat, query); + stat.appendSQL(" END"); + } + +} diff --git a/src/com/iciql/bytecode/ClassReader.java b/src/com/iciql/bytecode/ClassReader.java new file mode 100644 index 0000000..35e94e9 --- /dev/null +++ b/src/com/iciql/bytecode/ClassReader.java @@ -0,0 +1,1454 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.bytecode; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Stack; + +import com.iciql.IciqlException; +import com.iciql.Token; + +/** + * This class converts a method to a SQL Token by interpreting (decompiling) the + * bytecode of the class. + */ +public class ClassReader { + + private static final boolean DEBUG = false; + + private byte[] data; + private int pos; + private Constant[] constantPool; + private int startByteCode; + private String methodName; + + private String convertMethodName; + private Token result; + private Stack stack = new Stack(); + private ArrayList variables = new ArrayList(); + private boolean endOfMethod; + private boolean condition; + private int nextPc; + private Map fieldMap = new HashMap(); + + private static void debug(String s) { + if (DEBUG) { + System.out.println(s); + } + } + + public Token decompile(Object instance, Map fields, String method) { + this.fieldMap = fields; + this.convertMethodName = method; + Class clazz = instance.getClass(); + String className = clazz.getName(); + debug("class name " + className); + ByteArrayOutputStream buff = new ByteArrayOutputStream(); + try { + InputStream in = clazz.getClassLoader().getResource(className.replace('.', '/') + ".class").openStream(); + while (true) { + int x = in.read(); + if (x < 0) { + break; + } + buff.write(x); + } + } catch (IOException e) { + throw new IciqlException("Could not read class bytecode", e); + } + data = buff.toByteArray(); + int header = readInt(); + debug("header: " + Integer.toHexString(header)); + int minorVersion = readShort(); + int majorVersion = readShort(); + debug("version: " + majorVersion + "." + minorVersion); + int constantPoolCount = readShort(); + constantPool = new Constant[constantPoolCount]; + for (int i = 1; i < constantPoolCount; i++) { + int type = readByte(); + switch (type) { + case 1: + constantPool[i] = ConstantString.get(readString()); + break; + case 3: { + int x = readInt(); + constantPool[i] = ConstantNumber.get(x); + break; + } + case 4: { + int x = readInt(); + constantPool[i] = ConstantNumber.get("" + Float.intBitsToFloat(x), x, Constant.Type.FLOAT); + break; + } + case 5: { + long x = readLong(); + constantPool[i] = ConstantNumber.get(x); + i++; + break; + } + case 6: { + long x = readLong(); + constantPool[i] = ConstantNumber.get("" + Double.longBitsToDouble(x), x, Constant.Type.DOUBLE); + i++; + break; + } + case 7: { + int x = readShort(); + constantPool[i] = ConstantNumber.get(null, x, ConstantNumber.Type.CLASS_REF); + break; + } + case 8: { + int x = readShort(); + constantPool[i] = ConstantNumber.get(null, x, ConstantNumber.Type.STRING_REF); + break; + } + case 9: { + int x = readInt(); + constantPool[i] = ConstantNumber.get(null, x, ConstantNumber.Type.FIELD_REF); + break; + } + case 10: { + int x = readInt(); + constantPool[i] = ConstantNumber.get(null, x, ConstantNumber.Type.METHOD_REF); + break; + } + case 11: { + int x = readInt(); + constantPool[i] = ConstantNumber.get(null, x, ConstantNumber.Type.INTERFACE_METHOD_REF); + break; + } + case 12: { + int x = readInt(); + constantPool[i] = ConstantNumber.get(null, x, ConstantNumber.Type.NAME_AND_TYPE); + break; + } + default: + throw new IciqlException("Unsupported constant pool tag: " + type); + } + } + int accessFlags = readShort(); + debug("access flags: " + accessFlags); + int classRef = readShort(); + debug("class: " + constantPool[constantPool[classRef].intValue()]); + int superClassRef = readShort(); + debug(" extends " + constantPool[constantPool[superClassRef].intValue()]); + int interfaceCount = readShort(); + for (int i = 0; i < interfaceCount; i++) { + int interfaceRef = readShort(); + debug(" implements " + constantPool[constantPool[interfaceRef].intValue()]); + } + int fieldCount = readShort(); + for (int i = 0; i < fieldCount; i++) { + readField(); + } + int methodCount = readShort(); + for (int i = 0; i < methodCount; i++) { + readMethod(); + } + readAttributes(); + return result; + } + + private void readField() { + int accessFlags = readShort(); + int nameIndex = readShort(); + int descIndex = readShort(); + debug(" " + constantPool[descIndex] + " " + constantPool[nameIndex] + " " + accessFlags); + readAttributes(); + } + + private void readMethod() { + int accessFlags = readShort(); + int nameIndex = readShort(); + int descIndex = readShort(); + String desc = constantPool[descIndex].toString(); + methodName = constantPool[nameIndex].toString(); + debug(" " + desc + " " + methodName + " " + accessFlags); + readAttributes(); + } + + private void readAttributes() { + int attributeCount = readShort(); + for (int i = 0; i < attributeCount; i++) { + int attributeNameIndex = readShort(); + String attributeName = constantPool[attributeNameIndex].toString(); + debug(" attribute " + attributeName); + int attributeLength = readInt(); + int end = pos + attributeLength; + if ("Code".equals(attributeName)) { + readCode(); + } + pos = end; + } + } + + void decompile() { + int maxStack = readShort(); + int maxLocals = readShort(); + debug("stack: " + maxStack + " locals: " + maxLocals); + int codeLength = readInt(); + startByteCode = pos; + int end = pos + codeLength; + while (pos < end) { + readByteCode(); + } + debug(""); + pos = startByteCode + codeLength; + int exceptionTableLength = readShort(); + pos += 2 * exceptionTableLength; + readAttributes(); + } + + private void readCode() { + variables.clear(); + stack.clear(); + int maxStack = readShort(); + int maxLocals = readShort(); + debug("stack: " + maxStack + " locals: " + maxLocals); + int codeLength = readInt(); + startByteCode = pos; + if (methodName.startsWith(convertMethodName)) { + result = getResult(); + } + pos = startByteCode + codeLength; + int exceptionTableLength = readShort(); + pos += 2 * exceptionTableLength; + readAttributes(); + } + + private Token getResult() { + while (true) { + readByteCode(); + if (endOfMethod) { + return stack.pop(); + } + if (condition) { + Token c = stack.pop(); + Stack currentStack = new Stack(); + currentStack.addAll(stack); + ArrayList currentVariables = new ArrayList(); + currentVariables.addAll(variables); + int branch = nextPc; + Token a = getResult(); + stack = currentStack; + variables = currentVariables; + pos = branch + startByteCode; + Token b = getResult(); + if (a.equals("0") && b.equals("1")) { + return c; + } else if (a.equals("1") && b.equals("0")) { + return Not.get(c); + } else if (b.equals("0")) { + return And.get(Not.get(c), a); + } else if (a.equals("0")) { + return And.get(c, b); + } else if (b.equals("1")) { + return Or.get(c, a); + } else if (a.equals("1")) { + return And.get(Not.get(c), b); + } + return CaseWhen.get(c, b, a); + } + if (nextPc != 0) { + pos = nextPc + startByteCode; + } + } + } + + private void readByteCode() { + int startPos = pos - startByteCode; + int opCode = readByte(); + String op; + endOfMethod = false; + condition = false; + nextPc = 0; + switch (opCode) { + case 0: + op = "nop"; + break; + case 1: + op = "aconst_null"; + stack.push(Null.INSTANCE); + break; + case 2: + op = "iconst_m1"; + stack.push(ConstantNumber.get("-1")); + break; + case 3: + op = "iconst_0"; + stack.push(ConstantNumber.get("0")); + break; + case 4: + op = "iconst_1"; + stack.push(ConstantNumber.get("1")); + break; + case 5: + op = "iconst_2"; + stack.push(ConstantNumber.get("2")); + break; + case 6: + op = "iconst_3"; + stack.push(ConstantNumber.get("3")); + break; + case 7: + op = "iconst_4"; + stack.push(ConstantNumber.get("4")); + break; + case 8: + op = "iconst_5"; + stack.push(ConstantNumber.get("5")); + break; + case 9: + op = "lconst_0"; + stack.push(ConstantNumber.get("0")); + break; + case 10: + op = "lconst_1"; + stack.push(ConstantNumber.get("1")); + break; + case 11: + op = "fconst_0"; + stack.push(ConstantNumber.get("0.0")); + break; + case 12: + op = "fconst_1"; + stack.push(ConstantNumber.get("1.0")); + break; + case 13: + op = "fconst_2"; + stack.push(ConstantNumber.get("2.0")); + break; + case 14: + op = "dconst_0"; + stack.push(ConstantNumber.get("0.0")); + break; + case 15: + op = "dconst_1"; + stack.push(ConstantNumber.get("1.0")); + break; + case 16: { + int x = (byte) readByte(); + op = "bipush " + x; + stack.push(ConstantNumber.get(x)); + break; + } + case 17: { + int x = (short) readShort(); + op = "sipush " + x; + stack.push(ConstantNumber.get(x)); + break; + } + case 18: { + Token s = getConstant(readByte()); + op = "ldc " + s; + stack.push(s); + break; + } + case 19: { + Token s = getConstant(readShort()); + op = "ldc_w " + s; + stack.push(s); + break; + } + case 20: { + Token s = getConstant(readShort()); + op = "ldc2_w " + s; + stack.push(s); + break; + } + case 21: { + int x = readByte(); + op = "iload " + x; + stack.push(getVariable(x)); + break; + } + case 22: { + int x = readByte(); + op = "lload " + x; + stack.push(getVariable(x)); + break; + } + case 23: { + int x = readByte(); + op = "fload " + x; + stack.push(getVariable(x)); + break; + } + case 24: { + int x = readByte(); + op = "dload " + x; + stack.push(getVariable(x)); + break; + } + case 25: { + int x = readByte(); + op = "aload " + x; + stack.push(getVariable(x)); + break; + } + case 26: + op = "iload_0"; + stack.push(getVariable(0)); + break; + case 27: + op = "iload_1"; + stack.push(getVariable(1)); + break; + case 28: + op = "iload_2"; + stack.push(getVariable(2)); + break; + case 29: + op = "iload_3"; + stack.push(getVariable(3)); + break; + case 30: + op = "lload_0"; + stack.push(getVariable(0)); + break; + case 31: + op = "lload_1"; + stack.push(getVariable(1)); + break; + case 32: + op = "lload_2"; + stack.push(getVariable(2)); + break; + case 33: + op = "lload_3"; + stack.push(getVariable(3)); + break; + case 34: + op = "fload_0"; + stack.push(getVariable(0)); + break; + case 35: + op = "fload_1"; + stack.push(getVariable(1)); + break; + case 36: + op = "fload_2"; + stack.push(getVariable(2)); + break; + case 37: + op = "fload_3"; + stack.push(getVariable(3)); + break; + case 38: + op = "dload_0"; + stack.push(getVariable(0)); + break; + case 39: + op = "dload_1"; + stack.push(getVariable(1)); + break; + case 40: + op = "dload_2"; + stack.push(getVariable(2)); + break; + case 41: + op = "dload_3"; + stack.push(getVariable(3)); + break; + case 42: + op = "aload_0"; + stack.push(getVariable(0)); + break; + case 43: + op = "aload_1"; + stack.push(getVariable(1)); + break; + case 44: + op = "aload_2"; + stack.push(getVariable(2)); + break; + case 45: + op = "aload_3"; + stack.push(getVariable(3)); + break; + case 46: { + Token index = stack.pop(); + Token ref = stack.pop(); + op = "iaload"; + stack.push(ArrayGet.get(ref, index)); + break; + } + case 47: { + Token index = stack.pop(); + Token ref = stack.pop(); + op = "laload"; + stack.push(ArrayGet.get(ref, index)); + break; + } + case 48: { + Token index = stack.pop(); + Token ref = stack.pop(); + op = "faload"; + stack.push(ArrayGet.get(ref, index)); + break; + } + case 49: { + Token index = stack.pop(); + Token ref = stack.pop(); + op = "daload"; + stack.push(ArrayGet.get(ref, index)); + break; + } + case 50: { + Token index = stack.pop(); + Token ref = stack.pop(); + op = "aaload"; + stack.push(ArrayGet.get(ref, index)); + break; + } + case 51: { + Token index = stack.pop(); + Token ref = stack.pop(); + op = "baload"; + stack.push(ArrayGet.get(ref, index)); + break; + } + case 52: { + Token index = stack.pop(); + Token ref = stack.pop(); + op = "caload"; + stack.push(ArrayGet.get(ref, index)); + break; + } + case 53: { + Token index = stack.pop(); + Token ref = stack.pop(); + op = "saload"; + stack.push(ArrayGet.get(ref, index)); + break; + } + case 54: { + int var = readByte(); + op = "istore " + var; + setVariable(var, stack.pop()); + break; + } + case 55: { + int var = readByte(); + op = "lstore " + var; + setVariable(var, stack.pop()); + break; + } + case 56: { + int var = readByte(); + op = "fstore " + var; + setVariable(var, stack.pop()); + break; + } + case 57: { + int var = readByte(); + op = "dstore " + var; + setVariable(var, stack.pop()); + break; + } + case 58: { + int var = readByte(); + op = "astore " + var; + setVariable(var, stack.pop()); + break; + } + case 59: + op = "istore_0"; + setVariable(0, stack.pop()); + break; + case 60: + op = "istore_1"; + setVariable(1, stack.pop()); + break; + case 61: + op = "istore_2"; + setVariable(2, stack.pop()); + break; + case 62: + op = "istore_3"; + setVariable(3, stack.pop()); + break; + case 63: + op = "lstore_0"; + setVariable(0, stack.pop()); + break; + case 64: + op = "lstore_1"; + setVariable(1, stack.pop()); + break; + case 65: + op = "lstore_2"; + setVariable(2, stack.pop()); + break; + case 66: + op = "lstore_3"; + setVariable(3, stack.pop()); + break; + case 67: + op = "fstore_0"; + setVariable(0, stack.pop()); + break; + case 68: + op = "fstore_1"; + setVariable(1, stack.pop()); + break; + case 69: + op = "fstore_2"; + setVariable(2, stack.pop()); + break; + case 70: + op = "fstore_3"; + setVariable(3, stack.pop()); + break; + case 71: + op = "dstore_0"; + setVariable(0, stack.pop()); + break; + case 72: + op = "dstore_1"; + setVariable(1, stack.pop()); + break; + case 73: + op = "dstore_2"; + setVariable(2, stack.pop()); + break; + case 74: + op = "dstore_3"; + setVariable(3, stack.pop()); + break; + case 75: + op = "astore_0"; + setVariable(0, stack.pop()); + break; + case 76: + op = "astore_1"; + setVariable(1, stack.pop()); + break; + case 77: + op = "astore_2"; + setVariable(2, stack.pop()); + break; + case 78: + op = "astore_3"; + setVariable(3, stack.pop()); + break; + case 79: { + // String value = stack.pop(); + // String index = stack.pop(); + // String ref = stack.pop(); + op = "iastore"; + // TODO side effect - not supported + break; + } + case 80: + op = "lastore"; + // TODO side effect - not supported + break; + case 81: + op = "fastore"; + // TODO side effect - not supported + break; + case 82: + op = "dastore"; + // TODO side effect - not supported + break; + case 83: + op = "aastore"; + // TODO side effect - not supported + break; + case 84: + op = "bastore"; + // TODO side effect - not supported + break; + case 85: + op = "castore"; + // TODO side effect - not supported + break; + case 86: + op = "sastore"; + // TODO side effect - not supported + break; + case 87: + op = "pop"; + stack.pop(); + break; + case 88: + op = "pop2"; + // TODO currently we don't know the stack types + stack.pop(); + stack.pop(); + break; + case 89: { + op = "dup"; + Token x = stack.pop(); + stack.push(x); + stack.push(x); + break; + } + case 90: { + op = "dup_x1"; + Token a = stack.pop(); + Token b = stack.pop(); + stack.push(a); + stack.push(b); + stack.push(a); + break; + } + case 91: { + // TODO currently we don't know the stack types + op = "dup_x2"; + Token a = stack.pop(); + Token b = stack.pop(); + Token c = stack.pop(); + stack.push(a); + stack.push(c); + stack.push(b); + stack.push(a); + break; + } + case 92: { + // TODO currently we don't know the stack types + op = "dup2"; + Token a = stack.pop(); + Token b = stack.pop(); + stack.push(b); + stack.push(a); + stack.push(b); + stack.push(a); + break; + } + case 93: { + // TODO currently we don't know the stack types + op = "dup2_x1"; + Token a = stack.pop(); + Token b = stack.pop(); + Token c = stack.pop(); + stack.push(b); + stack.push(a); + stack.push(c); + stack.push(b); + stack.push(a); + break; + } + case 94: { + // TODO currently we don't know the stack types + op = "dup2_x2"; + Token a = stack.pop(); + Token b = stack.pop(); + Token c = stack.pop(); + Token d = stack.pop(); + stack.push(b); + stack.push(a); + stack.push(d); + stack.push(c); + stack.push(b); + stack.push(a); + break; + } + case 95: { + op = "swap"; + Token a = stack.pop(); + Token b = stack.pop(); + stack.push(a); + stack.push(b); + break; + } + case 96: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "iadd"; + stack.push(Operation.get(a, Operation.Type.ADD, b)); + break; + } + case 97: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "ladd"; + stack.push(Operation.get(a, Operation.Type.ADD, b)); + break; + } + case 98: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "fadd"; + stack.push(Operation.get(a, Operation.Type.ADD, b)); + break; + } + case 99: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "dadd"; + stack.push(Operation.get(a, Operation.Type.ADD, b)); + break; + } + case 100: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "isub"; + stack.push(Operation.get(a, Operation.Type.SUBTRACT, b)); + break; + } + case 101: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "lsub"; + stack.push(Operation.get(a, Operation.Type.SUBTRACT, b)); + break; + } + case 102: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "fsub"; + stack.push(Operation.get(a, Operation.Type.SUBTRACT, b)); + break; + } + case 103: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "dsub"; + stack.push(Operation.get(a, Operation.Type.SUBTRACT, b)); + break; + } + case 104: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "imul"; + stack.push(Operation.get(a, Operation.Type.MULTIPLY, b)); + break; + } + case 105: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "lmul"; + stack.push(Operation.get(a, Operation.Type.MULTIPLY, b)); + break; + } + case 106: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "fmul"; + stack.push(Operation.get(a, Operation.Type.MULTIPLY, b)); + break; + } + case 107: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "dmul"; + stack.push(Operation.get(a, Operation.Type.MULTIPLY, b)); + break; + } + case 108: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "idiv"; + stack.push(Operation.get(a, Operation.Type.DIVIDE, b)); + break; + } + case 109: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "ldiv"; + stack.push(Operation.get(a, Operation.Type.DIVIDE, b)); + break; + } + case 110: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "fdiv"; + stack.push(Operation.get(a, Operation.Type.DIVIDE, b)); + break; + } + case 111: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "ddiv"; + stack.push(Operation.get(a, Operation.Type.DIVIDE, b)); + break; + } + case 112: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "irem"; + stack.push(Operation.get(a, Operation.Type.MOD, b)); + break; + } + case 113: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "lrem"; + stack.push(Operation.get(a, Operation.Type.MOD, b)); + break; + } + case 114: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "frem"; + stack.push(Operation.get(a, Operation.Type.MOD, b)); + break; + } + case 115: { + Token b = stack.pop(); + Token a = stack.pop(); + op = "drem"; + stack.push(Operation.get(a, Operation.Type.MOD, b)); + break; + } + // case 116: + // op = "ineg"; + // break; + // case 117: + // op = "lneg"; + // break; + // case 118: + // op = "fneg"; + // break; + // case 119: + // op = "dneg"; + // break; + // case 120: + // op = "ishl"; + // break; + // case 121: + // op = "lshl"; + // break; + // case 122: + // op = "ishr"; + // break; + // case 123: + // op = "lshr"; + // break; + // case 124: + // op = "iushr"; + // break; + // case 125: + // op = "lushr"; + // break; + // case 126: + // op = "iand"; + // break; + // case 127: + // op = "land"; + // break; + // case 128: + // op = "ior"; + // break; + // case 129: + // op = "lor"; + // break; + // case 130: + // op = "ixor"; + // break; + // case 131: + // op = "lxor"; + // break; + // case 132: { + // int var = readByte(); + // int off = (byte) readByte(); + // op = "iinc " + var + " " + off; + // break; + // } + // case 133: + // op = "i2l"; + // break; + // case 134: + // op = "i2f"; + // break; + // case 135: + // op = "i2d"; + // break; + // case 136: + // op = "l2i"; + // break; + // case 137: + // op = "l2f"; + // break; + // case 138: + // op = "l2d"; + // break; + // case 139: + // op = "f2i"; + // break; + // case 140: + // op = "f2l"; + // break; + // case 141: + // op = "f2d"; + // break; + // case 142: + // op = "d2i"; + // break; + // case 143: + // op = "d2l"; + // break; + // case 144: + // op = "d2f"; + // break; + // case 145: + // op = "i2b"; + // break; + // case 146: + // op = "i2c"; + // break; + // case 147: + // op = "i2s"; + // break; + case 148: { + Token b = stack.pop(), a = stack.pop(); + stack.push(new Function("SIGN", Operation.get(a, Operation.Type.SUBTRACT, b))); + op = "lcmp"; + break; + } + // case 149: + // op = "fcmpl"; + // break; + // case 150: + // op = "fcmpg"; + // break; + // case 151: + // op = "dcmpl"; + // break; + // case 152: + // op = "dcmpg"; + // break; + case 153: + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + stack.push(Operation.get(stack.pop(), Operation.Type.EQUALS, ConstantNumber.get(0))); + op = "ifeq " + nextPc; + break; + case 154: + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + stack.push(Operation.get(stack.pop(), Operation.Type.NOT_EQUALS, ConstantNumber.get(0))); + op = "ifne " + nextPc; + break; + case 155: + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + stack.push(Operation.get(stack.pop(), Operation.Type.SMALLER, ConstantNumber.get(0))); + op = "iflt " + nextPc; + break; + case 156: + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + stack.push(Operation.get(stack.pop(), Operation.Type.BIGGER_EQUALS, ConstantNumber.get(0))); + op = "ifge " + nextPc; + break; + case 157: + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + stack.push(Operation.get(stack.pop(), Operation.Type.BIGGER, ConstantNumber.get(0))); + op = "ifgt " + nextPc; + break; + case 158: + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + stack.push(Operation.get(stack.pop(), Operation.Type.SMALLER_EQUALS, ConstantNumber.get(0))); + op = "ifle " + nextPc; + break; + case 159: { + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + Token b = stack.pop(), a = stack.pop(); + stack.push(Operation.get(a, Operation.Type.EQUALS, b)); + op = "if_icmpeq " + nextPc; + break; + } + case 160: { + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + Token b = stack.pop(), a = stack.pop(); + stack.push(Operation.get(a, Operation.Type.NOT_EQUALS, b)); + op = "if_icmpne " + nextPc; + break; + } + case 161: { + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + Token b = stack.pop(), a = stack.pop(); + stack.push(Operation.get(a, Operation.Type.SMALLER, b)); + op = "if_icmplt " + nextPc; + break; + } + case 162: { + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + Token b = stack.pop(), a = stack.pop(); + stack.push(Operation.get(a, Operation.Type.BIGGER_EQUALS, b)); + op = "if_icmpge " + nextPc; + break; + } + case 163: { + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + Token b = stack.pop(), a = stack.pop(); + stack.push(Operation.get(a, Operation.Type.BIGGER, b)); + op = "if_icmpgt " + nextPc; + break; + } + case 164: { + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + Token b = stack.pop(), a = stack.pop(); + stack.push(Operation.get(a, Operation.Type.SMALLER_EQUALS, b)); + op = "if_icmple " + nextPc; + break; + } + case 165: { + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + Token b = stack.pop(), a = stack.pop(); + stack.push(Operation.get(a, Operation.Type.EQUALS, b)); + op = "if_acmpeq " + nextPc; + break; + } + case 166: { + condition = true; + nextPc = getAbsolutePos(pos, readShort()); + Token b = stack.pop(), a = stack.pop(); + stack.push(Operation.get(a, Operation.Type.NOT_EQUALS, b)); + op = "if_acmpne " + nextPc; + break; + } + case 167: + nextPc = getAbsolutePos(pos, readShort()); + op = "goto " + nextPc; + break; + // case 168: + // // TODO not supported yet + // op = "jsr " + getAbsolutePos(pos, readShort()); + // break; + // case 169: + // // TODO not supported yet + // op = "ret " + readByte(); + // break; + // case 170: { + // int start = pos; + // pos += 4 - ((pos - startByteCode) & 3); + // int def = readInt(); + // int low = readInt(), high = readInt(); + // int n = high - low + 1; + // op = "tableswitch default:" + getAbsolutePos(start, def); + // StringBuilder buff = new StringBuilder(); + // for (int i = 0; i < n; i++) { + // buff.append(' ').append(low++). + // append(":"). + // append(getAbsolutePos(start, readInt())); + // } + // op += buff.toString(); + // // pos += n * 4; + // break; + // } + // case 171: { + // int start = pos; + // pos += 4 - ((pos - startByteCode) & 3); + // int def = readInt(); + // int n = readInt(); + // op = "lookupswitch default:" + getAbsolutePos(start, def); + // StringBuilder buff = new StringBuilder(); + // for (int i = 0; i < n; i++) { + // buff.append(' '). + // append(readInt()). + // append(":"). + // append(getAbsolutePos(start, readInt())); + // } + // op += buff.toString(); + // // pos += n * 8; + // break; + // } + case 172: + op = "ireturn"; + endOfMethod = true; + break; + case 173: + op = "lreturn"; + endOfMethod = true; + break; + case 174: + op = "freturn"; + endOfMethod = true; + break; + case 175: + op = "dreturn"; + endOfMethod = true; + break; + case 176: + op = "areturn"; + endOfMethod = true; + break; + case 177: + op = "return"; + // no value returned + stack.push(null); + endOfMethod = true; + break; + // case 178: + // op = "getstatic " + getField(readShort()); + // break; + // case 179: + // op = "putstatic " + getField(readShort()); + // break; + case 180: { + String field = getField(readShort()); + Token p = stack.pop(); + String s = p + "." + field.substring(field.lastIndexOf('.') + 1, field.indexOf(' ')); + if (s.startsWith("this.")) { + s = s.substring(5); + } + stack.push(Variable.get(s, fieldMap.get(s))); + op = "getfield " + field; + break; + } + // case 181: + // op = "putfield " + getField(readShort()); + // break; + case 182: { + String method = getMethod(readShort()); + op = "invokevirtual " + method; + if (method.equals("java/lang/String.equals (Ljava/lang/Object;)Z")) { + Token a = stack.pop(); + Token b = stack.pop(); + stack.push(Operation.get(a, Operation.Type.EQUALS, b)); + } else if (method.equals("java/lang/Integer.intValue ()I")) { + // ignore + } else if (method.equals("java/lang/Long.longValue ()J")) { + // ignore + } + break; + } + case 183: { + String method = getMethod(readShort()); + op = "invokespecial " + method; + break; + } + case 184: + op = "invokestatic " + getMethod(readShort()); + break; + // case 185: { + // int methodRef = readShort(); + // readByte(); + // readByte(); + // op = "invokeinterface " + getMethod(methodRef); + // break; + // } + case 187: { + String className = constantPool[constantPool[readShort()].intValue()].toString(); + op = "new " + className; + break; + } + // case 188: + // op = "newarray " + readByte(); + // break; + // case 189: + // op = "anewarray " + cpString[readShort()]; + // break; + // case 190: + // op = "arraylength"; + // break; + // case 191: + // op = "athrow"; + // break; + // case 192: + // op = "checkcast " + cpString[readShort()]; + // break; + // case 193: + // op = "instanceof " + cpString[readShort()]; + // break; + // case 194: + // op = "monitorenter"; + // break; + // case 195: + // op = "monitorexit"; + // break; + // case 196: { + // opCode = readByte(); + // switch (opCode) { + // case 21: + // op = "wide iload " + readShort(); + // break; + // case 22: + // op = "wide lload " + readShort(); + // break; + // case 23: + // op = "wide fload " + readShort(); + // break; + // case 24: + // op = "wide dload " + readShort(); + // break; + // case 25: + // op = "wide aload " + readShort(); + // break; + // case 54: + // op = "wide istore " + readShort(); + // break; + // case 55: + // op = "wide lstore " + readShort(); + // break; + // case 56: + // op = "wide fstore " + readShort(); + // break; + // case 57: + // op = "wide dstore " + readShort(); + // break; + // case 58: + // op = "wide astore " + readShort(); + // break; + // case 132: { + // int var = readShort(); + // int off = (short) readShort(); + // op = "wide iinc " + var + " " + off; + // break; + // } + // case 169: + // op = "wide ret " + readShort(); + // break; + // default: + // throw new IciqlException( + // "Unsupported wide opCode " + opCode); + // } + // break; + // } + // case 197: + // op = "multianewarray " + cpString[readShort()] + " " + readByte(); + // break; + // case 198: { + // condition = true; + // nextPc = getAbsolutePos(pos, readShort()); + // Token a = stack.pop(); + // stack.push("(" + a + " IS NULL)"); + // op = "ifnull " + nextPc; + // break; + // } + // case 199: { + // condition = true; + // nextPc = getAbsolutePos(pos, readShort()); + // Token a = stack.pop(); + // stack.push("(" + a + " IS NOT NULL)"); + // op = "ifnonnull " + nextPc; + // break; + // } + case 200: + op = "goto_w " + getAbsolutePos(pos, readInt()); + break; + case 201: + op = "jsr_w " + getAbsolutePos(pos, readInt()); + break; + default: + throw new IciqlException("Unsupported opCode " + opCode); + } + debug(" " + startPos + ": " + op); + } + + private void setVariable(int x, Token value) { + while (x >= variables.size()) { + variables.add(Variable.get("p" + variables.size(), null)); + } + variables.set(x, value); + } + + private Token getVariable(int x) { + if (x == 0) { + return Variable.THIS; + } + while (x >= variables.size()) { + variables.add(Variable.get("p" + variables.size(), null)); + } + return variables.get(x); + } + + private String getField(int fieldRef) { + int field = constantPool[fieldRef].intValue(); + int classIndex = field >>> 16; + int nameAndType = constantPool[field & 0xffff].intValue(); + String className = constantPool[constantPool[classIndex].intValue()] + "." + constantPool[nameAndType >>> 16] + + " " + constantPool[nameAndType & 0xffff]; + return className; + } + + private String getMethod(int methodRef) { + int method = constantPool[methodRef].intValue(); + int classIndex = method >>> 16; + int nameAndType = constantPool[method & 0xffff].intValue(); + String className = constantPool[constantPool[classIndex].intValue()] + "." + constantPool[nameAndType >>> 16] + + " " + constantPool[nameAndType & 0xffff]; + return className; + } + + private Constant getConstant(int constantRef) { + Constant c = constantPool[constantRef]; + switch (c.getType()) { + case INT: + case FLOAT: + case DOUBLE: + case LONG: + return c; + case STRING_REF: + return constantPool[c.intValue()]; + default: + throw new IciqlException("Not a constant: " + constantRef); + } + } + + private String readString() { + int size = readShort(); + byte[] buff = data; + int p = pos, end = p + size; + char[] chars = new char[size]; + int j = 0; + for (; p < end; j++) { + int x = buff[p++] & 0xff; + if (x < 0x80) { + chars[j] = (char) x; + } else if (x >= 0xe0) { + chars[j] = (char) (((x & 0xf) << 12) + ((buff[p++] & 0x3f) << 6) + (buff[p++] & 0x3f)); + } else { + chars[j] = (char) (((x & 0x1f) << 6) + (buff[p++] & 0x3f)); + } + } + pos = p; + return new String(chars, 0, j); + } + + private int getAbsolutePos(int start, int offset) { + return start - startByteCode - 1 + (short) offset; + } + + private int readByte() { + return data[pos++] & 0xff; + } + + private int readShort() { + byte[] buff = data; + return ((buff[pos++] & 0xff) << 8) + (buff[pos++] & 0xff); + } + + private int readInt() { + byte[] buff = data; + return (buff[pos++] << 24) + ((buff[pos++] & 0xff) << 16) + ((buff[pos++] & 0xff) << 8) + (buff[pos++] & 0xff); + } + + private long readLong() { + return ((long) (readInt()) << 32) + (readInt() & 0xffffffffL); + } + +} diff --git a/src/com/iciql/bytecode/Constant.java b/src/com/iciql/bytecode/Constant.java new file mode 100644 index 0000000..65cd66b --- /dev/null +++ b/src/com/iciql/bytecode/Constant.java @@ -0,0 +1,38 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.bytecode; + +import com.iciql.Token; + +/** + * An expression in the constant pool. + */ +public interface Constant extends Token { + + /** + * The constant pool type. + */ + enum Type { + STRING, INT, FLOAT, DOUBLE, LONG, CLASS_REF, STRING_REF, FIELD_REF, METHOD_REF, INTERFACE_METHOD_REF, NAME_AND_TYPE + } + + Constant.Type getType(); + + int intValue(); + +} diff --git a/src/com/iciql/bytecode/ConstantNumber.java b/src/com/iciql/bytecode/ConstantNumber.java new file mode 100644 index 0000000..934de3d --- /dev/null +++ b/src/com/iciql/bytecode/ConstantNumber.java @@ -0,0 +1,70 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; + +/** + * A literal number. + */ +public class ConstantNumber implements Constant { + + private final String value; + private final Type type; + private final long longValue; + + private ConstantNumber(String value, long longValue, Type type) { + this.value = value; + this.longValue = longValue; + this.type = type; + } + + static ConstantNumber get(String v) { + return new ConstantNumber(v, 0, Type.STRING); + } + + static ConstantNumber get(int v) { + return new ConstantNumber("" + v, v, Type.INT); + } + + static ConstantNumber get(long v) { + return new ConstantNumber("" + v, v, Type.LONG); + } + + static ConstantNumber get(String s, long x, Type type) { + return new ConstantNumber(s, x, type); + } + + public int intValue() { + return (int) longValue; + } + + public String toString() { + return value; + } + + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL(toString()); + } + + public Constant.Type getType() { + return type; + } + +} diff --git a/src/com/iciql/bytecode/ConstantString.java b/src/com/iciql/bytecode/ConstantString.java new file mode 100644 index 0000000..985f97d --- /dev/null +++ b/src/com/iciql/bytecode/ConstantString.java @@ -0,0 +1,55 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; +import com.iciql.util.StringUtils; + +/** + * A string constant. + */ +public class ConstantString implements Constant { + + private final String value; + + private ConstantString(String value) { + this.value = value; + } + + static ConstantString get(String v) { + return new ConstantString(v); + } + + public String toString() { + return value; + } + + public int intValue() { + return 0; + } + + public void appendSQL(SQLStatement stat, Query query) { + stat.appendSQL(StringUtils.quoteStringSQL(value)); + } + + public Constant.Type getType() { + return Constant.Type.STRING; + } + +} diff --git a/src/com/iciql/bytecode/Function.java b/src/com/iciql/bytecode/Function.java new file mode 100644 index 0000000..56a55ea --- /dev/null +++ b/src/com/iciql/bytecode/Function.java @@ -0,0 +1,47 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; +import com.iciql.Token; + +/** + * A method call. + */ +class Function implements Token { + + private final String name; + private final Token expr; + + Function(String name, Token expr) { + this.name = name; + this.expr = expr; + } + + public String toString() { + return name + "(" + expr + ")"; + } + + public void appendSQL(SQLStatement stat, Query query) { + // untested + stat.appendSQL(name + "("); + expr.appendSQL(stat, query); + stat.appendSQL(")"); + } +} diff --git a/src/com/iciql/bytecode/Not.java b/src/com/iciql/bytecode/Not.java new file mode 100644 index 0000000..ab5ab84 --- /dev/null +++ b/src/com/iciql/bytecode/Not.java @@ -0,0 +1,55 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; +import com.iciql.Token; + +/** + * A NOT condition. + */ +public class Not implements Token { + + private Token expr; + + private Not(Token expr) { + this.expr = expr; + } + + static Token get(Token expr) { + if (expr instanceof Not) { + return ((Not) expr).expr; + } else if (expr instanceof Operation) { + return ((Operation) expr).reverse(); + } + return new Not(expr); + } + + Token not() { + return expr; + } + + public void appendSQL(SQLStatement stat, Query query) { + // untested + stat.appendSQL("NOT("); + expr.appendSQL(stat, query); + stat.appendSQL(")"); + } + +} diff --git a/src/com/iciql/bytecode/Null.java b/src/com/iciql/bytecode/Null.java new file mode 100644 index 0000000..a28de56 --- /dev/null +++ b/src/com/iciql/bytecode/Null.java @@ -0,0 +1,44 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; +import com.iciql.Token; + +/** + * The Java 'null'. + */ +public class Null implements Token { + + static final Null INSTANCE = new Null(); + + private Null() { + // don't allow to create new instances + } + + public String toString() { + return "null"; + } + + public void appendSQL(SQLStatement stat, Query query) { + // untested + stat.appendSQL("NULL"); + } + +} diff --git a/src/com/iciql/bytecode/Operation.java b/src/com/iciql/bytecode/Operation.java new file mode 100644 index 0000000..7cd42d9 --- /dev/null +++ b/src/com/iciql/bytecode/Operation.java @@ -0,0 +1,111 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; +import com.iciql.Token; + +/** + * A mathematical or comparison operation. + */ +class Operation implements Token { + + /** + * The operation type. + */ + enum Type { + EQUALS("=") { + Type reverse() { + return NOT_EQUALS; + } + }, + NOT_EQUALS("<>") { + Type reverse() { + return EQUALS; + } + }, + BIGGER(">") { + Type reverse() { + return SMALLER_EQUALS; + } + }, + BIGGER_EQUALS(">=") { + Type reverse() { + return SMALLER; + } + }, + SMALLER_EQUALS("<=") { + Type reverse() { + return BIGGER; + } + }, + SMALLER("<") { + Type reverse() { + return BIGGER_EQUALS; + } + }, + ADD("+"), SUBTRACT("-"), MULTIPLY("*"), DIVIDE("/"), MOD("%"); + + private String name; + + Type(String name) { + this.name = name; + } + + public String toString() { + return name; + } + + Type reverse() { + return null; + } + + } + + private final Token left, right; + private final Type op; + + private Operation(Token left, Type op, Token right) { + this.left = left; + this.op = op; + this.right = right; + } + + static Token get(Token left, Type op, Token right) { + if (op == Type.NOT_EQUALS && "0".equals(right.toString())) { + return left; + } + return new Operation(left, op, right); + } + + public String toString() { + return left + " " + op + " " + right; + } + + public Token reverse() { + return get(left, op.reverse(), right); + } + + public void appendSQL(SQLStatement stat, Query query) { + left.appendSQL(stat, query); + stat.appendSQL(op.toString()); + right.appendSQL(stat, query); + } + +} diff --git a/src/com/iciql/bytecode/Or.java b/src/com/iciql/bytecode/Or.java new file mode 100644 index 0000000..37da2a6 --- /dev/null +++ b/src/com/iciql/bytecode/Or.java @@ -0,0 +1,47 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; +import com.iciql.Token; + +/** + * An OR expression. + */ +public class Or implements Token { + + private final Token left, right; + + private Or(Token left, Token right) { + this.left = left; + this.right = right; + } + + static Or get(Token left, Token right) { + return new Or(left, right); + } + + public void appendSQL(SQLStatement stat, Query query) { + // untested + left.appendSQL(stat, query); + stat.appendSQL(" OR "); + right.appendSQL(stat, query); + } + +} diff --git a/src/com/iciql/bytecode/Variable.java b/src/com/iciql/bytecode/Variable.java new file mode 100644 index 0000000..3412349 --- /dev/null +++ b/src/com/iciql/bytecode/Variable.java @@ -0,0 +1,51 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.bytecode; + +import com.iciql.Query; +import com.iciql.SQLStatement; +import com.iciql.Token; + +/** + * A variable. + */ +public class Variable implements Token { + + static final Variable THIS = new Variable("this", null); + + private final String name; + private final Object obj; + + private Variable(String name, Object obj) { + this.name = name; + this.obj = obj; + } + + static Variable get(String name, Object obj) { + return new Variable(name, obj); + } + + public String toString() { + return name; + } + + public void appendSQL(SQLStatement stat, Query query) { + query.appendSQL(stat, obj); + } + +} diff --git a/src/com/iciql/bytecode/package.html b/src/com/iciql/bytecode/package.html new file mode 100644 index 0000000..5107481 --- /dev/null +++ b/src/com/iciql/bytecode/package.html @@ -0,0 +1,25 @@ + + + + +Javadoc package documentation + + +The class decompiler for natural syntax iciql clauses. + + \ No newline at end of file diff --git a/src/com/iciql/package.html b/src/com/iciql/package.html new file mode 100644 index 0000000..769837b --- /dev/null +++ b/src/com/iciql/package.html @@ -0,0 +1,25 @@ + + + + +Javadoc package documentation + + +iciql (pronounced "icicle") is a Java JDBC SQL statement generator and simple object mapper + + \ No newline at end of file diff --git a/src/com/iciql/util/GenerateModels.java b/src/com/iciql/util/GenerateModels.java new file mode 100644 index 0000000..69ce852 --- /dev/null +++ b/src/com/iciql/util/GenerateModels.java @@ -0,0 +1,192 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.util; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.Writer; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.iciql.Db; +import com.iciql.DbInspector; + +/** + * Generates iciql models. + */ +public class GenerateModels { + + /** + * The output stream where this tool writes to. + */ + protected PrintStream out = System.out; + + public static void main(String... args) { + GenerateModels tool = new GenerateModels(); + try { + tool.runTool(args); + } catch (SQLException e) { + tool.out.print("Error: "); + tool.out.println(e.getMessage()); + tool.out.println(); + tool.showUsage(); + } + } + + public void runTool(String... args) throws SQLException { + String url = null; + String user = "sa"; + String password = ""; + String schema = null; + String table = null; + String packageName = ""; + String folder = null; + boolean annotateSchema = true; + boolean trimStrings = false; + for (int i = 0; args != null && i < args.length; i++) { + String arg = args[i]; + if (arg.equals("-url")) { + url = args[++i]; + } else if (arg.equals("-user")) { + user = args[++i]; + } else if (arg.equals("-password")) { + password = args[++i]; + } else if (arg.equals("-schema")) { + schema = args[++i]; + } else if (arg.equals("-table")) { + table = args[++i]; + } else if (arg.equals("-package")) { + packageName = args[++i]; + } else if (arg.equals("-folder")) { + folder = args[++i]; + } else if (arg.equals("-annotateSchema")) { + try { + annotateSchema = Boolean.parseBoolean(args[++i]); + } catch (Throwable t) { + throw new SQLException("Can not parse -annotateSchema value"); + } + } else if (arg.equals("-trimStrings")) { + try { + trimStrings = Boolean.parseBoolean(args[++i]); + } catch (Throwable t) { + throw new SQLException("Can not parse -trimStrings value"); + } + } else { + throwUnsupportedOption(arg); + } + } + if (url == null) { + throw new SQLException("URL not set"); + } + execute(url, user, password, schema, table, packageName, folder, annotateSchema, trimStrings); + } + + /** + * Generates models from the database. + * + * @param url + * the database URL + * @param user + * the user name + * @param password + * the password + * @param schema + * the schema to read from. null for all schemas. + * @param table + * the table to model. null for all tables within schema. + * @param packageName + * the package name of the model classes. + * @param folder + * destination folder for model classes (package path not + * included) + * @param annotateSchema + * includes the schema in the table model annotations + * @param trimStrings + * automatically trim strings that exceed maxLength + */ + public static void execute(String url, String user, String password, String schema, String table, + String packageName, String folder, boolean annotateSchema, boolean trimStrings) throws SQLException { + Connection conn = null; + try { + conn = DriverManager.getConnection(url, user, password); + Db db = Db.open(url, user, password.toCharArray()); + DbInspector inspector = new DbInspector(db); + List models = inspector.generateModel(schema, table, packageName, annotateSchema, trimStrings); + File parentFile; + if (StringUtils.isNullOrEmpty(folder)) { + parentFile = new File(System.getProperty("user.dir")); + } else { + parentFile = new File(folder); + } + parentFile.mkdirs(); + Pattern p = Pattern.compile("class ([a-zA-Z0-9]+)"); + for (String model : models) { + Matcher m = p.matcher(model); + if (m.find()) { + String className = m.group().substring("class".length()).trim(); + File classFile = new File(parentFile, className + ".java"); + Writer o = new FileWriter(classFile, false); + PrintWriter writer = new PrintWriter(new BufferedWriter(o)); + writer.write(model); + writer.close(); + System.out.println("Generated " + classFile.getAbsolutePath()); + } + } + } catch (IOException io) { + throw new SQLException("could not generate model", io); + } finally { + JdbcUtils.closeSilently(conn); + } + } + + /** + * Throw a SQLException saying this command line option is not supported. + * + * @param option + * the unsupported option + * @return this method never returns normally + */ + protected SQLException throwUnsupportedOption(String option) throws SQLException { + showUsage(); + throw new SQLException("Unsupported option: " + option); + } + + protected void showUsage() { + out.println("GenerateModels"); + out.println("Usage:"); + out.println(); + out.println("(*) -url jdbc:h2:~test"); + out.println(" -user "); + out.println(" -password "); + out.println(" -schema "); + out.println(" -table "); + out.println(" -package "); + out.println(" -folder "); + out.println(" -annotateSchema "); + out.println(" -trimStrings "); + } + +} diff --git a/src/com/iciql/util/JdbcUtils.java b/src/com/iciql/util/JdbcUtils.java new file mode 100644 index 0000000..edf21e8 --- /dev/null +++ b/src/com/iciql/util/JdbcUtils.java @@ -0,0 +1,253 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.util; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Properties; +import javax.naming.Context; +import javax.sql.DataSource; +import javax.sql.XAConnection; + +/** + * This is a utility class with JDBC helper functions. + */ +public class JdbcUtils { + + private static final String[] DRIVERS = { "h2:", "org.h2.Driver", "Cache:", + "com.intersys.jdbc.CacheDriver", "daffodilDB://", "in.co.daffodil.db.rmi.RmiDaffodilDBDriver", + "daffodil", "in.co.daffodil.db.jdbc.DaffodilDBDriver", "db2:", "COM.ibm.db2.jdbc.net.DB2Driver", + "derby:net:", "org.apache.derby.jdbc.ClientDriver", "derby://", + "org.apache.derby.jdbc.ClientDriver", "derby:", "org.apache.derby.jdbc.EmbeddedDriver", + "FrontBase:", "com.frontbase.jdbc.FBJDriver", "firebirdsql:", "org.firebirdsql.jdbc.FBDriver", + "hsqldb:", "org.hsqldb.jdbcDriver", "informix-sqli:", "com.informix.jdbc.IfxDriver", "jtds:", + "net.sourceforge.jtds.jdbc.Driver", "microsoft:", "com.microsoft.jdbc.sqlserver.SQLServerDriver", + "mimer:", "com.mimer.jdbc.Driver", "mysql:", "com.mysql.jdbc.Driver", "odbc:", + "sun.jdbc.odbc.JdbcOdbcDriver", "oracle:", "oracle.jdbc.driver.OracleDriver", "pervasive:", + "com.pervasive.jdbc.v2.Driver", "pointbase:micro:", "com.pointbase.me.jdbc.jdbcDriver", + "pointbase:", "com.pointbase.jdbc.jdbcUniversalDriver", "postgresql:", "org.postgresql.Driver", + "sybase:", "com.sybase.jdbc3.jdbc.SybDriver", "sqlserver:", + "com.microsoft.sqlserver.jdbc.SQLServerDriver", "teradata:", "com.ncr.teradata.TeraDriver", }; + + private JdbcUtils() { + // utility class + } + + /** + * Close a statement without throwing an exception. + * + * @param stat + * the statement or null + */ + public static void closeSilently(Statement stat) { + if (stat != null) { + try { + stat.close(); + } catch (SQLException e) { + // ignore + } + } + } + + /** + * Close a connection without throwing an exception. + * + * @param conn + * the connection or null + */ + public static void closeSilently(Connection conn) { + if (conn != null) { + try { + conn.close(); + } catch (SQLException e) { + // ignore + } + } + } + + /** + * Close a result set without throwing an exception. + * + * @param rs + * the result set or null + */ + public static void closeSilently(ResultSet rs) { + closeSilently(rs, false); + } + + /** + * Close a result set, and optionally its statement without throwing an + * exception. + * + * @param rs + * the result set or null + */ + public static void closeSilently(ResultSet rs, boolean closeStatement) { + if (rs != null) { + Statement stat = null; + if (closeStatement) { + try { + stat = rs.getStatement(); + } catch (SQLException e) { + // ignore + } + } + try { + rs.close(); + } catch (SQLException e) { + // ignore + } + closeSilently(stat); + } + } + + /** + * Close an XA connection set without throwing an exception. + * + * @param conn + * the XA connection or null + */ + public static void closeSilently(XAConnection conn) { + if (conn != null) { + try { + conn.close(); + } catch (SQLException e) { + // ignore + } + } + } + + /** + * Open a new database connection with the given settings. + * + * @param driver + * the driver class name + * @param url + * the database URL + * @param user + * the user name + * @param password + * the password + * @return the database connection + */ + public static Connection getConnection(String driver, String url, String user, String password) + throws SQLException { + Properties prop = new Properties(); + if (user != null) { + prop.setProperty("user", user); + } + if (password != null) { + prop.setProperty("password", password); + } + return getConnection(driver, url, prop); + } + + /** + * Escape table or schema patterns used for DatabaseMetaData functions. + * + * @param pattern + * the pattern + * @return the escaped pattern + */ + public static String escapeMetaDataPattern(String pattern) { + if (pattern == null || pattern.length() == 0) { + return pattern; + } + return StringUtils.replaceAll(pattern, "\\", "\\\\"); + } + + /** + * Open a new database connection with the given settings. + * + * @param driver + * the driver class name + * @param url + * the database URL + * @param prop + * the properties containing at least the user name and password + * @return the database connection + */ + public static Connection getConnection(String driver, String url, Properties prop) throws SQLException { + if (StringUtils.isNullOrEmpty(driver)) { + JdbcUtils.load(url); + } else { + Class d = Utils.loadClass(driver); + if (java.sql.Driver.class.isAssignableFrom(d)) { + return DriverManager.getConnection(url, prop); + } else if (javax.naming.Context.class.isAssignableFrom(d)) { + // JNDI context + try { + Context context = (Context) d.newInstance(); + DataSource ds = (DataSource) context.lookup(url); + String user = prop.getProperty("user"); + String password = prop.getProperty("password"); + if (StringUtils.isNullOrEmpty(user) && StringUtils.isNullOrEmpty(password)) { + return ds.getConnection(); + } + return ds.getConnection(user, password); + } catch (SQLException e) { + throw e; + } catch (Exception e) { + throw new SQLException("Failed to get connection for " + url, e); + } + } else { + // Don't know, but maybe it loaded a JDBC Driver + return DriverManager.getConnection(url, prop); + } + } + return DriverManager.getConnection(url, prop); + } + + /** + * Get the driver class name for the given URL, or null if the URL is + * unknown. + * + * @param url + * the database URL + * @return the driver class name + */ + public static String getDriver(String url) { + if (url.startsWith("jdbc:")) { + url = url.substring("jdbc:".length()); + for (int i = 0; i < DRIVERS.length; i += 2) { + String prefix = DRIVERS[i]; + if (url.startsWith(prefix)) { + return DRIVERS[i + 1]; + } + } + } + return null; + } + + /** + * Load the driver class for the given URL, if the database URL is known. + * + * @param url + * the database URL + */ + public static void load(String url) { + String driver = getDriver(url); + if (driver != null) { + Utils.loadClass(driver); + } + } + +} diff --git a/src/com/iciql/util/Slf4jStatementListener.java b/src/com/iciql/util/Slf4jStatementListener.java new file mode 100644 index 0000000..2398ade --- /dev/null +++ b/src/com/iciql/util/Slf4jStatementListener.java @@ -0,0 +1,94 @@ +/* + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.util; + +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.iciql.Iciql; +import com.iciql.util.StatementLogger.StatementListener; +import com.iciql.util.StatementLogger.StatementType; + +/** + * Slf4jStatementListener interfaces the iciql statement logger to the SLF4J + * logging architecture. + * + */ +public class Slf4jStatementListener implements StatementListener { + + private Logger logger = LoggerFactory.getLogger(Iciql.class); + + /** + * Enumeration representing the SLF4J log levels. + */ + public enum Level { + ERROR, WARN, INFO, DEBUG, TRACE, OFF; + } + + private final Level defaultLevel; + + private final Map levels; + + public Slf4jStatementListener() { + this(Level.TRACE); + } + + public Slf4jStatementListener(Level defaultLevel) { + this.defaultLevel = defaultLevel; + levels = new HashMap(); + for (StatementType type : StatementType.values()) { + levels.put(type, defaultLevel); + } + } + + /** + * Sets the logging level for a particular statement type. + * + * @param type + * @param level + */ + public void setLevel(StatementType type, Level level) { + levels.put(type, defaultLevel); + } + + @Override + public void logStatement(StatementType type, String statement) { + Level level = levels.get(type); + switch (level) { + case ERROR: + logger.error(statement); + break; + case WARN: + logger.warn(statement); + break; + case INFO: + logger.info(statement); + break; + case DEBUG: + logger.debug(statement); + break; + case TRACE: + logger.trace(statement); + break; + case OFF: + break; + } + } +} diff --git a/src/com/iciql/util/StatementBuilder.java b/src/com/iciql/util/StatementBuilder.java new file mode 100644 index 0000000..c2974ae --- /dev/null +++ b/src/com/iciql/util/StatementBuilder.java @@ -0,0 +1,157 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.util; + +/** + * A utility class to build a statement. In addition to the methods supported by + * StringBuilder, it allows to add a text only in the second iteration. This + * simplified constructs such as: + * + *

    + * StringBuilder buff = new StringBuilder();
    + * for (int i = 0; i < args.length; i++) {
    + * 	if (i > 0) {
    + * 		buff.append(", ");
    + * 	}
    + * 	buff.append(args[i]);
    + * }
    + * 
    + * + * to + * + *
    + * StatementBuilder buff = new StatementBuilder();
    + * for (String s : args) {
    + * 	buff.appendExceptFirst(", ");
    + * 	buff.append(a);
    + * }
    + * 
    + */ +public class StatementBuilder { + + private final StringBuilder builder = new StringBuilder(); + private int index; + + /** + * Create a new builder. + */ + public StatementBuilder() { + // nothing to do + } + + /** + * Create a new builder. + * + * @param string + * the initial string + */ + public StatementBuilder(String string) { + builder.append(string); + } + + /** + * Append a text. + * + * @param s + * the text to append + * @return itself + */ + public StatementBuilder append(String s) { + builder.append(s); + return this; + } + + /** + * Append a character. + * + * @param c + * the character to append + * @return itself + */ + public StatementBuilder append(char c) { + builder.append(c); + return this; + } + + /** + * Append a number. + * + * @param x + * the number to append + * @return itself + */ + public StatementBuilder append(long x) { + builder.append(x); + return this; + } + + /** + * Reset the loop counter. + * + * @return itself + */ + public StatementBuilder resetCount() { + index = 0; + return this; + } + + /** + * Append a text, but only if appendExceptFirst was never called. + * + * @param s + * the text to append + */ + public void appendOnlyFirst(String s) { + if (index == 0) { + builder.append(s); + } + } + + /** + * Append a text, except when this method is called the first time. + * + * @param s + * the text to append + */ + public void appendExceptFirst(String s) { + if (index++ > 0) { + builder.append(s); + } + } + + public void append(StatementBuilder sb) { + builder.append(sb); + } + + public void insert(int offset, char c) { + builder.insert(offset, c); + } + + public String toString() { + return builder.toString(); + } + + /** + * Get the length. + * + * @return the length + */ + public int length() { + return builder.length(); + } +} diff --git a/src/com/iciql/util/StatementLogger.java b/src/com/iciql/util/StatementLogger.java new file mode 100644 index 0000000..95d644a --- /dev/null +++ b/src/com/iciql/util/StatementLogger.java @@ -0,0 +1,182 @@ +/* + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.util; + +import java.text.DecimalFormat; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Utility class to optionally log generated statements to StatementListeners.
    + * Statement logging is disabled by default. + *

    + * This class also tracks the counts for generated statements by major type. + * + */ +public class StatementLogger { + + /** + * Enumeration of the different statement types that are logged. + */ + public enum StatementType { + STAT, TOTAL, CREATE, INSERT, UPDATE, MERGE, DELETE, SELECT; + } + + /** + * Interface that defines a statement listener. + */ + public interface StatementListener { + void logStatement(StatementType type, String statement); + } + + private static final Set LISTENERS = Utils.newHashSet(); + private static final StatementListener CONSOLE = new StatementListener() { + + @Override + public void logStatement(StatementType type, String message) { + System.out.println(message); + } + }; + + private static final AtomicLong SELECT_COUNT = new AtomicLong(); + private static final AtomicLong CREATE_COUNT = new AtomicLong(); + private static final AtomicLong INSERT_COUNT = new AtomicLong(); + private static final AtomicLong UPDATE_COUNT = new AtomicLong(); + private static final AtomicLong MERGE_COUNT = new AtomicLong(); + private static final AtomicLong DELETE_COUNT = new AtomicLong(); + + /** + * Activates the Console Logger. + */ + public static void activateConsoleLogger() { + LISTENERS.add(CONSOLE); + } + + /** + * Deactivates the Console Logger. + */ + public static void deactivateConsoleLogger() { + LISTENERS.remove(CONSOLE); + } + + /** + * Registers a listener with the relay. + * + * @param listener + */ + public static void registerListener(StatementListener listener) { + LISTENERS.add(listener); + } + + /** + * Unregisters a listener with the relay. + * + * @param listener + */ + public static void unregisterListener(StatementListener listener) { + LISTENERS.remove(listener); + } + + public static void create(String statement) { + CREATE_COUNT.incrementAndGet(); + logStatement(StatementType.CREATE, statement); + } + + public static void insert(String statement) { + INSERT_COUNT.incrementAndGet(); + logStatement(StatementType.INSERT, statement); + } + + public static void update(String statement) { + UPDATE_COUNT.incrementAndGet(); + logStatement(StatementType.UPDATE, statement); + } + + public static void merge(String statement) { + MERGE_COUNT.incrementAndGet(); + logStatement(StatementType.MERGE, statement); + } + + public static void delete(String statement) { + DELETE_COUNT.incrementAndGet(); + logStatement(StatementType.DELETE, statement); + } + + public static void select(String statement) { + SELECT_COUNT.incrementAndGet(); + logStatement(StatementType.SELECT, statement); + } + + private static void logStatement(StatementType type, String statement) { + for (StatementListener listener : LISTENERS) { + try { + listener.logStatement(type, statement); + } catch (Throwable t) { + } + } + } + + public static long getCreateCount() { + return CREATE_COUNT.longValue(); + } + + public static long getInsertCount() { + return INSERT_COUNT.longValue(); + } + + public static long getUpdateCount() { + return UPDATE_COUNT.longValue(); + } + + public static long getMergeCount() { + return MERGE_COUNT.longValue(); + } + + public static long getDeleteCount() { + return DELETE_COUNT.longValue(); + } + + public static long getSelectCount() { + return SELECT_COUNT.longValue(); + } + + public static long getTotalCount() { + return getCreateCount() + getInsertCount() + getUpdateCount() + getDeleteCount() + getMergeCount() + + getSelectCount(); + } + + public static void logStats() { + logStatement(StatementType.STAT, "iciql Runtime Statistics"); + logStatement(StatementType.STAT, "========================"); + logStat(StatementType.CREATE, getCreateCount()); + logStat(StatementType.INSERT, getInsertCount()); + logStat(StatementType.UPDATE, getUpdateCount()); + logStat(StatementType.MERGE, getMergeCount()); + logStat(StatementType.DELETE, getDeleteCount()); + logStat(StatementType.SELECT, getSelectCount()); + logStatement(StatementType.STAT, "========================"); + logStat(StatementType.TOTAL, getTotalCount()); + } + + private static void logStat(StatementType type, long value) { + if (value > 0) { + DecimalFormat df = new DecimalFormat("###,###,###,###"); + logStatement(StatementType.STAT, + StringUtils.pad(type.name(), 6, " ", true) + " = " + df.format(value)); + } + } +} \ No newline at end of file diff --git a/src/com/iciql/util/StringUtils.java b/src/com/iciql/util/StringUtils.java new file mode 100644 index 0000000..2a8c297 --- /dev/null +++ b/src/com/iciql/util/StringUtils.java @@ -0,0 +1,308 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.util; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; + +/** + * Common string utilities. + * + */ +public class StringUtils { + + /** + * Replace all occurrences of the before string with the after string. + * + * @param s + * the string + * @param before + * the old text + * @param after + * the new text + * @return the string with the before string replaced + */ + public static String replaceAll(String s, String before, String after) { + int next = s.indexOf(before); + if (next < 0) { + return s; + } + StringBuilder buff = new StringBuilder(s.length() - before.length() + after.length()); + int index = 0; + while (true) { + buff.append(s.substring(index, next)).append(after); + index = next + before.length(); + next = s.indexOf(before, index); + if (next < 0) { + buff.append(s.substring(index)); + break; + } + } + return buff.toString(); + } + + /** + * Check if a String is null or empty (the length is null). + * + * @param s + * the string to check + * @return true if it is null or empty + */ + public static boolean isNullOrEmpty(String s) { + return s == null || s.length() == 0; + } + + /** + * Convert a string to a Java literal using the correct escape sequences. + * The literal is not enclosed in double quotes. The result can be used in + * properties files or in Java source code. + * + * @param s + * the text to convert + * @return the Java representation + */ + public static String javaEncode(String s) { + int length = s.length(); + StringBuilder buff = new StringBuilder(length); + for (int i = 0; i < length; i++) { + char c = s.charAt(i); + switch (c) { + // case '\b': + // // BS backspace + // // not supported in properties files + // buff.append("\\b"); + // break; + case '\t': + // HT horizontal tab + buff.append("\\t"); + break; + case '\n': + // LF linefeed + buff.append("\\n"); + break; + case '\f': + // FF form feed + buff.append("\\f"); + break; + case '\r': + // CR carriage return + buff.append("\\r"); + break; + case '"': + // double quote + buff.append("\\\""); + break; + case '\\': + // backslash + buff.append("\\\\"); + break; + default: + int ch = c & 0xffff; + if (ch >= ' ' && (ch < 0x80)) { + buff.append(c); + // not supported in properties files + // } else if(ch < 0xff) { + // buff.append("\\"); + // // make sure it's three characters (0x200 is octal 1000) + // buff.append(Integer.toOctalString(0x200 | + // ch).substring(1)); + } else { + buff.append("\\u"); + // make sure it's four characters + buff.append(Integer.toHexString(0x10000 | ch).substring(1)); + } + } + } + return buff.toString(); + } + + /** + * Pad a string. This method is used for the SQL function RPAD and LPAD. + * + * @param string + * the original string + * @param n + * the target length + * @param padding + * the padding string + * @param right + * true if the padding should be appended at the end + * @return the padded string + */ + public static String pad(String string, int n, String padding, boolean right) { + if (n < 0) { + n = 0; + } + if (n < string.length()) { + return string.substring(0, n); + } else if (n == string.length()) { + return string; + } + char paddingChar; + if (padding == null || padding.length() == 0) { + paddingChar = ' '; + } else { + paddingChar = padding.charAt(0); + } + StringBuilder buff = new StringBuilder(n); + n -= string.length(); + if (right) { + buff.append(string); + } + for (int i = 0; i < n; i++) { + buff.append(paddingChar); + } + if (!right) { + buff.append(string); + } + return buff.toString(); + } + + /** + * Convert a string to a SQL literal. Null is converted to NULL. The text is + * enclosed in single quotes. If there are any special characters, the + * method STRINGDECODE is used. + * + * @param s + * the text to convert. + * @return the SQL literal + */ + public static String quoteStringSQL(String s) { + if (s == null) { + return "NULL"; + } + int length = s.length(); + StringBuilder buff = new StringBuilder(length + 2); + buff.append('\''); + for (int i = 0; i < length; i++) { + char c = s.charAt(i); + if (c == '\'') { + buff.append(c); + } else if (c < ' ' || c > 127) { + // need to start from the beginning because maybe there was a \ + // that was not quoted + return "STRINGDECODE(" + quoteStringSQL(javaEncode(s)) + ")"; + } + buff.append(c); + } + buff.append('\''); + return buff.toString(); + } + + /** + * Split a string into an array of strings using the given separator. A null + * string will result in a null array, and an empty string in a zero element + * array. + * + * @param s + * the string to split + * @param separatorChar + * the separator character + * @param trim + * whether each element should be trimmed + * @return the array list + */ + public static String[] arraySplit(String s, char separatorChar, boolean trim) { + if (s == null) { + return null; + } + int length = s.length(); + if (length == 0) { + return new String[0]; + } + ArrayList list = Utils.newArrayList(); + StringBuilder buff = new StringBuilder(length); + for (int i = 0; i < length; i++) { + char c = s.charAt(i); + if (c == separatorChar) { + String e = buff.toString(); + list.add(trim ? e.trim() : e); + buff.setLength(0); + } else if (c == '\\' && i < length - 1) { + buff.append(s.charAt(++i)); + } else { + buff.append(c); + } + } + String e = buff.toString(); + list.add(trim ? e.trim() : e); + String[] array = new String[list.size()]; + list.toArray(array); + return array; + } + + /** + * Calculates the SHA1 of the string. + * + * @param text + * @return sha1 of the string + */ + public static String calculateSHA1(String text) { + try { + byte[] bytes = text.getBytes("iso-8859-1"); + return calculateSHA1(bytes); + } catch (UnsupportedEncodingException u) { + throw new RuntimeException(u); + } + } + + /** + * Calculates the SHA1 of the byte array. + * + * @param bytes + * @return sha1 of the byte array + */ + public static String calculateSHA1(byte[] bytes) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + md.update(bytes, 0, bytes.length); + byte[] digest = md.digest(); + StringBuilder sb = new StringBuilder(digest.length * 2); + for (int i = 0; i < digest.length; i++) { + if (((int) digest[i] & 0xff) < 0x10) { + sb.append('0'); + } + sb.append(Integer.toHexString((int) digest[i] & 0xff)); + } + return sb.toString(); + } catch (NoSuchAlgorithmException t) { + throw new RuntimeException(t); + } + } + + /** + * Counts the occurrences of char c in the given string. + * + * @param c + * the character to count + * @param value + * the source string + * @return the count of c in value + */ + public static int count(char c, String value) { + int count = 0; + for (char cv : value.toCharArray()) { + if (cv == c) { + count++; + } + } + return count; + } +} diff --git a/src/com/iciql/util/Utils.java b/src/com/iciql/util/Utils.java new file mode 100644 index 0000000..3a600fa --- /dev/null +++ b/src/com/iciql/util/Utils.java @@ -0,0 +1,267 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.util; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringWriter; +import java.lang.reflect.Constructor; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.sql.Clob; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +import com.iciql.IciqlException; + +/** + * Generic utility methods. + */ +public class Utils { + + public static final AtomicLong COUNTER = new AtomicLong(0); + + private static final boolean MAKE_ACCESSIBLE = true; + + private static final int BUFFER_BLOCK_SIZE = 4 * 1024; + + @SuppressWarnings("unchecked") + public static Class getClass(X x) { + return (Class) x.getClass(); + } + + public static Class loadClass(String className) { + try { + return Class.forName(className); + } catch (Exception e) { + throw new IciqlException(e); + } + } + + public static ArrayList newArrayList() { + return new ArrayList(); + } + + public static ArrayList newArrayList(Collection c) { + return new ArrayList(c); + } + + public static HashSet newHashSet() { + return new HashSet(); + } + + public static HashSet newHashSet(Collection list) { + return new HashSet(list); + } + + public static HashMap newHashMap() { + return new HashMap(); + } + + public static Map newSynchronizedHashMap() { + HashMap map = newHashMap(); + return Collections.synchronizedMap(map); + } + + public static IdentityHashMap newIdentityHashMap() { + return new IdentityHashMap(); + } + + public static ThreadLocal newThreadLocal(final Class clazz) { + return new ThreadLocal() { + @SuppressWarnings("rawtypes") + @Override + protected T initialValue() { + try { + return clazz.newInstance(); + } catch (Exception e) { + if (MAKE_ACCESSIBLE) { + Constructor[] constructors = clazz.getDeclaredConstructors(); + // try 0 length constructors + for (Constructor c : constructors) { + if (c.getParameterTypes().length == 0) { + c.setAccessible(true); + try { + return clazz.newInstance(); + } catch (Exception e2) { + // ignore + } + } + } + } + throw new IciqlException("Exception trying to create " + clazz.getName() + ": " + e, e); + } + } + }; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static T newObject(Class clazz) { + // must create new instances + if (clazz == Integer.class) { + return (T) new Integer((int) COUNTER.getAndIncrement()); + } else if (clazz == String.class) { + return (T) ("" + COUNTER.getAndIncrement()); + } else if (clazz == Long.class) { + return (T) new Long(COUNTER.getAndIncrement()); + } else if (clazz == Short.class) { + return (T) new Short((short) COUNTER.getAndIncrement()); + } else if (clazz == Byte.class) { + return (T) new Byte((byte) COUNTER.getAndIncrement()); + } else if (clazz == Float.class) { + return (T) new Float(COUNTER.getAndIncrement()); + } else if (clazz == Double.class) { + return (T) new Double(COUNTER.getAndIncrement()); + } else if (clazz == Boolean.class) { + return (T) new Boolean(false); + } else if (clazz == BigDecimal.class) { + return (T) new BigDecimal(COUNTER.getAndIncrement()); + } else if (clazz == BigInteger.class) { + return (T) new BigInteger("" + COUNTER.getAndIncrement()); + } else if (clazz == java.sql.Date.class) { + return (T) new java.sql.Date(COUNTER.getAndIncrement()); + } else if (clazz == java.sql.Time.class) { + return (T) new java.sql.Time(COUNTER.getAndIncrement()); + } else if (clazz == java.sql.Timestamp.class) { + return (T) new java.sql.Timestamp(COUNTER.getAndIncrement()); + } else if (clazz == java.util.Date.class) { + return (T) new java.util.Date(COUNTER.getAndIncrement()); + } else if (clazz == List.class) { + return (T) new ArrayList(); + } + try { + return clazz.newInstance(); + } catch (Exception e) { + if (MAKE_ACCESSIBLE) { + Constructor[] constructors = clazz.getDeclaredConstructors(); + // try 0 length constructors + for (Constructor c : constructors) { + if (c.getParameterTypes().length == 0) { + c.setAccessible(true); + try { + return clazz.newInstance(); + } catch (Exception e2) { + // ignore + } + } + } + // try 1 length constructors + for (Constructor c : constructors) { + if (c.getParameterTypes().length == 1) { + c.setAccessible(true); + try { + return (T) c.newInstance(new Object[1]); + } catch (Exception e2) { + // ignore + } + } + } + } + throw new IciqlException("Exception trying to create " + clazz.getName() + ": " + e, e); + } + } + + public static boolean isSimpleType(Class clazz) { + if (Number.class.isAssignableFrom(clazz)) { + return true; + } else if (clazz == String.class) { + return true; + } + return false; + } + + public static Object convert(Object o, Class targetType) { + if (o == null) { + return null; + } + Class currentType = o.getClass(); + if (targetType.isAssignableFrom(currentType)) { + return o; + } + if (targetType == String.class) { + if (Clob.class.isAssignableFrom(currentType)) { + Clob c = (Clob) o; + try { + Reader r = c.getCharacterStream(); + return readStringAndClose(r, -1); + } catch (Exception e) { + throw new IciqlException("Error converting CLOB to String: " + e.toString(), e); + } + } + return o.toString(); + } + if (Number.class.isAssignableFrom(currentType)) { + Number n = (Number) o; + if (targetType == Byte.class) { + return n.byteValue(); + } else if (targetType == Short.class) { + return n.shortValue(); + } else if (targetType == Integer.class) { + return n.intValue(); + } else if (targetType == Long.class) { + return n.longValue(); + } else if (targetType == Double.class) { + return n.doubleValue(); + } else if (targetType == Float.class) { + return n.floatValue(); + } + } + throw new IciqlException("Can not convert the value " + o + " from " + currentType + " to " + + targetType); + } + + /** + * Read a number of characters from a reader and close it. + * + * @param in + * the reader + * @param length + * the maximum number of characters to read, or -1 to read until + * the end of file + * @return the string read + */ + public static String readStringAndClose(Reader in, int length) throws IOException { + try { + if (length <= 0) { + length = Integer.MAX_VALUE; + } + int block = Math.min(BUFFER_BLOCK_SIZE, length); + StringWriter out = new StringWriter(length == Integer.MAX_VALUE ? block : length); + char[] buff = new char[block]; + while (length > 0) { + int len = Math.min(block, length); + len = in.read(buff, 0, len); + if (len < 0) { + break; + } + out.write(buff, 0, len); + length -= len; + } + return out.toString(); + } finally { + in.close(); + } + } +} diff --git a/src/com/iciql/util/WeakIdentityHashMap.java b/src/com/iciql/util/WeakIdentityHashMap.java new file mode 100644 index 0000000..bc03cd0 --- /dev/null +++ b/src/com/iciql/util/WeakIdentityHashMap.java @@ -0,0 +1,243 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.util; + +import java.lang.ref.WeakReference; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +import com.iciql.IciqlException; + +/** + * This hash map uses weak references, so that elements that are no longer + * referenced elsewhere can be garbage collected. It also uses object identity + * to compare keys. The garbage collection happens when trying to add new data, + * or when resizing. + * + * @param + * the keys + * @param + * the value + */ + +public class WeakIdentityHashMap implements Map { + + private static final int MAX_LOAD = 90; + private static final WeakReference DELETED_KEY = new WeakReference(null); + private int mask, len, size, deletedCount, level; + private int maxSize, minSize, maxDeleted; + private WeakReference[] keys; + private V[] values; + + public WeakIdentityHashMap() { + reset(2); + } + + public int size() { + return size; + } + + private void checkSizePut() { + if (deletedCount > size) { + rehash(level); + } + if (size + deletedCount >= maxSize) { + rehash(level + 1); + } + } + + private void checkSizeRemove() { + if (size < minSize && level > 0) { + rehash(level - 1); + } else if (deletedCount > maxDeleted) { + rehash(level); + } + } + + private int getIndex(Object key) { + return System.identityHashCode(key) & mask; + } + + @SuppressWarnings("unchecked") + private void reset(int newLevel) { + minSize = size * 3 / 4; + size = 0; + level = newLevel; + len = 2 << level; + mask = len - 1; + maxSize = (int) (len * MAX_LOAD / 100L); + deletedCount = 0; + maxDeleted = 20 + len / 2; + keys = new WeakReference[len]; + values = (V[]) new Object[len]; + } + + public V put(K key, V value) { + checkSizePut(); + int index = getIndex(key); + int plus = 1; + int deleted = -1; + do { + WeakReference k = keys[index]; + if (k == null) { + // found an empty record + if (deleted >= 0) { + index = deleted; + deletedCount--; + } + size++; + keys[index] = new WeakReference(key); + values[index] = value; + return null; + } else if (k == DELETED_KEY) { + if (deleted < 0) { + // found the first deleted record + deleted = index; + } + } else { + Object r = k.get(); + if (r == null) { + delete(index); + } else if (r == key) { + // update existing + V old = values[index]; + values[index] = value; + return old; + } + } + index = (index + plus++) & mask; + } while (plus <= len); + throw new IciqlException("Hashmap is full"); + } + + public V remove(Object key) { + checkSizeRemove(); + int index = getIndex(key); + int plus = 1; + do { + WeakReference k = keys[index]; + if (k == null) { + // found an empty record + return null; + } else if (k == DELETED_KEY) { + // continue + } else { + Object r = k.get(); + if (r == null) { + delete(index); + } else if (r == key) { + // found the record + V old = values[index]; + delete(index); + return old; + } + } + index = (index + plus++) & mask; + k = keys[index]; + } while (plus <= len); + // not found + return null; + } + + @SuppressWarnings("unchecked") + private void delete(int index) { + keys[index] = (WeakReference) DELETED_KEY; + values[index] = null; + deletedCount++; + size--; + } + + private void rehash(int newLevel) { + WeakReference[] oldKeys = keys; + V[] oldValues = values; + reset(newLevel); + for (int i = 0; i < oldKeys.length; i++) { + WeakReference k = oldKeys[i]; + if (k != null && k != DELETED_KEY) { + K key = k.get(); + if (key != null) { + put(key, oldValues[i]); + } + } + } + } + + public V get(Object key) { + int index = getIndex(key); + int plus = 1; + do { + WeakReference k = keys[index]; + if (k == null) { + return null; + } else if (k == DELETED_KEY) { + // continue + } else { + Object r = k.get(); + if (r == null) { + delete(index); + } else if (r == key) { + return values[index]; + } + } + index = (index + plus++) & mask; + } while (plus <= len); + return null; + } + + public void clear() { + reset(2); + } + + public boolean containsKey(Object key) { + return get(key) != null; + } + + public boolean containsValue(Object value) { + if (value == null) { + return false; + } + for (V item : values) { + if (value.equals(item)) { + return true; + } + } + return false; + } + + public Set> entrySet() { + throw new UnsupportedOperationException(); + } + + public boolean isEmpty() { + return size == 0; + } + + public Set keySet() { + throw new UnsupportedOperationException(); + } + + public void putAll(Map m) { + throw new UnsupportedOperationException(); + } + + public Collection values() { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/com/iciql/util/package.html b/src/com/iciql/util/package.html new file mode 100644 index 0000000..3d24dee --- /dev/null +++ b/src/com/iciql/util/package.html @@ -0,0 +1,25 @@ + + + + +Javadoc package documentation + + +Utility classes for iciql. + + \ No newline at end of file diff --git a/tests/com/iciql/test/AliasMapTest.java b/tests/com/iciql/test/AliasMapTest.java new file mode 100644 index 0000000..4d19bd0 --- /dev/null +++ b/tests/com/iciql/test/AliasMapTest.java @@ -0,0 +1,50 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.test; + +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.junit.Test; + +import com.iciql.Db; +import com.iciql.test.models.Product; + +/** + * Tests that columns (p.unitsInStock) are not compared by value with the value + * (9), but by reference (using an identity hash map). See + * http://code.google.com/p/h2database/issues/detail?id=119 + * + * @author d moebius at scoop dash gmbh dot de + */ +public class AliasMapTest { + + @Test + public void testAliasMapping() throws Exception { + Db db = Db.open("jdbc:h2:mem:", "sa", "sa"); + db.insertAll(Product.getList()); + + Product p = new Product(); + List products = db.from(p).where(p.unitsInStock).is(9).orderBy(p.productId).select(); + + assertEquals("[]", products.toString()); + + db.close(); + } +} \ No newline at end of file diff --git a/tests/com/iciql/test/AnnotationsTest.java b/tests/com/iciql/test/AnnotationsTest.java new file mode 100644 index 0000000..367e6e1 --- /dev/null +++ b/tests/com/iciql/test/AnnotationsTest.java @@ -0,0 +1,177 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.test; + +import static com.iciql.test.IciqlSuite.assertStartsWith; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +import org.h2.constant.ErrorCode; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.iciql.Db; +import com.iciql.IciqlException; +import com.iciql.test.models.Product; +import com.iciql.test.models.ProductAnnotationOnly; +import com.iciql.test.models.ProductInheritedAnnotation; +import com.iciql.test.models.ProductMixedAnnotation; +import com.iciql.test.models.ProductNoCreateTable; + +/** + * Test annotation processing. + */ +public class AnnotationsTest { + + /** + * This object represents a database (actually a connection to the + * database). + */ + + private Db db; + + @Before + public void setUp() { + db = Db.open("jdbc:h2:mem:", "sa", "sa"); + db.insertAll(Product.getList()); + db.insertAll(ProductAnnotationOnly.getList()); + db.insertAll(ProductMixedAnnotation.getList()); + } + + @After + public void tearDown() { + db.close(); + } + + @Test + public void testIndexCreation() throws SQLException { + // test indexes are created, and columns are in the right order + DatabaseMetaData meta = db.getConnection().getMetaData(); + ResultSet rs = meta.getIndexInfo(null, "PUBLIC", "ANNOTATED" + "PRODUCT", false, true); + assertTrue(rs.next()); + assertStartsWith(rs.getString("INDEX_NAME"), "PRIMARY_KEY"); + assertTrue(rs.next()); + assertStartsWith(rs.getString("INDEX_NAME"), "ANNOTATED" + "PRODUCT_"); + assertStartsWith(rs.getString("COLUMN_NAME"), "NAME"); + assertTrue(rs.next()); + assertStartsWith(rs.getString("INDEX_NAME"), "ANNOTATED" + "PRODUCT_"); + assertStartsWith(rs.getString("COLUMN_NAME"), "CAT"); + assertFalse(rs.next()); + } + + @Test + public void testProductAnnotationOnly() { + ProductAnnotationOnly p = new ProductAnnotationOnly(); + assertEquals(10, db.from(p).selectCount()); + + // test IQColumn.name="cat" + assertEquals(2, db.from(p).where(p.category).is("Beverages").selectCount()); + + // test IQTable.annotationsOnly=true + // public String unmappedField is ignored by iciql + assertEquals(0, db.from(p).where(p.unmappedField).is("unmapped").selectCount()); + + // test IQColumn.autoIncrement=true + // 10 objects, 10 autoIncremented unique values + assertEquals(10, db.from(p).selectDistinct(p.autoIncrement).size()); + + // test IQTable.primaryKey=id + try { + db.insertAll(ProductAnnotationOnly.getList()); + } catch (IciqlException r) { + SQLException s = (SQLException) r.getCause(); + assertEquals(ErrorCode.DUPLICATE_KEY_1, s.getErrorCode()); + } + } + + @Test + public void testProductMixedAnnotation() { + ProductMixedAnnotation p = new ProductMixedAnnotation(); + + // test IQColumn.name="cat" + assertEquals(2, db.from(p).where(p.category).is("Beverages").selectCount()); + + // test IQTable.annotationsOnly=false + // public String mappedField is reflectively mapped by iciql + assertEquals(10, db.from(p).where(p.mappedField).is("mapped").selectCount()); + + // test IQColumn.primaryKey=true + try { + db.insertAll(ProductMixedAnnotation.getList()); + } catch (IciqlException r) { + SQLException s = (SQLException) r.getCause(); + assertEquals(ErrorCode.DUPLICATE_KEY_1, s.getErrorCode()); + } + } + + @Test + public void testTrimStringAnnotation() { + ProductAnnotationOnly p = new ProductAnnotationOnly(); + ProductAnnotationOnly prod = db.from(p).selectFirst(); + String oldValue = prod.category; + String newValue = "01234567890123456789"; + // 2 chars exceeds field max + prod.category = newValue; + db.update(prod); + + ProductAnnotationOnly newProd = db.from(p).where(p.productId).is(prod.productId).selectFirst(); + assertEquals(newValue.substring(0, 15), newProd.category); + + newProd.category = oldValue; + db.update(newProd); + } + + @Test + public void testColumnInheritanceAnnotation() { + ProductInheritedAnnotation table = new ProductInheritedAnnotation(); + List inserted = ProductInheritedAnnotation.getData(); + db.insertAll(inserted); + + List retrieved = db.from(table).select(); + + for (int j = 0; j < retrieved.size(); j++) { + ProductInheritedAnnotation i = inserted.get(j); + ProductInheritedAnnotation r = retrieved.get(j); + assertEquals(i.category, r.category); + assertEquals(i.mappedField, r.mappedField); + assertEquals(i.unitsInStock, r.unitsInStock); + assertEquals(i.unitPrice, r.unitPrice); + assertEquals(i.name(), r.name()); + assertEquals(i.id(), r.id()); + } + } + + @Test + public void testCreateTableIfRequiredAnnotation() { + // tests IQTable.createTableIfRequired=false + try { + db.insertAll(ProductNoCreateTable.getList()); + } catch (IciqlException r) { + SQLException s = (SQLException) r.getCause(); + assertEquals(ErrorCode.TABLE_OR_VIEW_NOT_FOUND_1, s.getErrorCode()); + } + } + +} diff --git a/tests/com/iciql/test/ClobTest.java b/tests/com/iciql/test/ClobTest.java new file mode 100644 index 0000000..1f2eff4 --- /dev/null +++ b/tests/com/iciql/test/ClobTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.test; + +import static org.junit.Assert.assertEquals; +import static com.iciql.Define.primaryKey; +import static com.iciql.Define.tableName; + +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; + +import com.iciql.Db; +import com.iciql.Iciql; + +/** + * Tests if converting a CLOB to a String works. + */ +public class ClobTest { + + @Test + public void testClob() throws Exception { + String create = "CREATE TABLE CLOB_TEST(ID INT PRIMARY KEY, WORDS {0})"; + Db db = Db.open("jdbc:h2:mem:", "sa", "sa"); + db.executeUpdate(MessageFormat.format(create, "VARCHAR(255)")); + db.insertAll(StringRecord.getList()); + testSimpleUpdate(db, "VARCHAR fail"); + db.close(); + + db = Db.open("jdbc:h2:mem:", "sa", "sa"); + db.executeUpdate(MessageFormat.format(create, "TEXT")); + db.insertAll(StringRecord.getList()); + testSimpleUpdate(db, "CLOB fail because of single quote artifacts"); + db.close(); + } + + private void testSimpleUpdate(Db db, String failureMsg) { + String newWords = "I changed the words"; + StringRecord r = new StringRecord(); + StringRecord originalRecord = db.from(r).where(r.id).is(2).selectFirst(); + String oldWords = originalRecord.words; + originalRecord.words = newWords; + db.update(originalRecord); + + StringRecord r2 = new StringRecord(); + StringRecord revisedRecord = db.from(r2).where(r2.id).is(2).selectFirst(); + assertEquals(failureMsg, newWords, revisedRecord.words); + + // undo update + originalRecord.words = oldWords; + db.update(originalRecord); + } + + /** + * A simple class used in this test. + */ + public static class StringRecord implements Iciql { + + public Integer id; + public String words; + + public StringRecord() { + // public constructor + } + + private StringRecord(int id, String words) { + this.id = id; + this.words = words; + } + + public void defineIQ() { + tableName("CLOB_TEST"); + primaryKey(id); + } + + private static StringRecord create(int id, String words) { + return new StringRecord(id, words); + } + + public static List getList() { + StringRecord[] list = { create(1, "Once upon a midnight dreary, while I pondered weak and weary,"), + create(2, "Over many a quaint and curious volume of forgotten lore,"), + create(3, "While I nodded, nearly napping, suddenly there came a tapping,"), + create(4, "As of some one gently rapping, rapping at my chamber door."), + create(5, "`'Tis some visitor,' I muttered, `tapping at my chamber door -"), + create(6, "Only this, and nothing more.'") }; + + return Arrays.asList(list); + } + + public String toString() { + return id + ": " + words; + } + } +} diff --git a/tests/com/iciql/test/ConcurrencyTest.java b/tests/com/iciql/test/ConcurrencyTest.java new file mode 100644 index 0000000..88b23f6 --- /dev/null +++ b/tests/com/iciql/test/ConcurrencyTest.java @@ -0,0 +1,178 @@ +/* + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.iciql.Db; +import com.iciql.Query; +import com.iciql.test.models.Product; +import com.iciql.util.Utils; + +/** + * Tests concurrency and alias instance sharing. + */ +public class ConcurrencyTest { + + private Db db; + private int numberOfTests = 200; + + @Before + public void setUp() { + db = Db.open("jdbc:h2:mem:", "sa", "sa"); + db.insertAll(Product.getList()); + } + + @After + public void tearDown() { + db.close(); + } + + @Test + public void testAliasSharing() throws Exception { + // Single-threaded example of why aliases can NOT be shared. + Product p = new Product(); + Query query1 = db.from(p); + Query query2 = db.from(p); + + // if you could share alias instances both counts should be equal + long count1 = query1.where(p.category).is("Beverages").selectCount(); + long count2 = query2.where(p.category).is("Beverages").selectCount(); + + // but they aren't + assertEquals(0, count1); + assertEquals(2, count2); + assertTrue(count1 != count2); + } + + @Test + public void testConcurrencyFinal() throws Exception { + // Multi-threaded example of why aliases can NOT be shared. + // + // This test looks like it _could_ work and you may find that it _can_ + // work, but you should also find that it _will_ fail. + + List threads = Utils.newArrayList(); + final AtomicInteger failures = new AtomicInteger(0); + final Product p = new Product(); + for (int i = 0; i < numberOfTests; i++) { + final int testNumber = i; + Thread t = new Thread(new Runnable() { + public void run() { + try { + int testCase = testNumber % 10; + test(testCase, p); + } catch (Throwable rex) { + failures.incrementAndGet(); + System.err.println("EXPECTED ERROR"); + rex.printStackTrace(); + } + } + }, "ICIQL-" + i); + t.start(); + threads.add(t); + } + + // wait till all threads complete + for (Thread t : threads) { + t.join(); + } + + assertTrue("This should fail. Try running a few more times.", failures.get() > 0); + } + + @Test + public void testConcurrencyThreadLocal() throws Exception { + List threads = Utils.newArrayList(); + final AtomicInteger failures = new AtomicInteger(0); + final ThreadLocal tl = Utils.newThreadLocal(Product.class); + for (int i = 0; i < numberOfTests; i++) { + final int testNumber = i; + Thread t = new Thread(new Runnable() { + public void run() { + try { + int testCase = testNumber % 10; + test(testCase, tl.get()); + } catch (Throwable rex) { + failures.incrementAndGet(); + rex.printStackTrace(); + } + } + }, "ICIQL-" + i); + t.start(); + threads.add(t); + } + + // wait till all threads complete + for (Thread t : threads) { + t.join(); + } + + assertEquals("ThreadLocal should never fail!", 0, failures.get()); + } + + private void test(int testCase, Product p) throws AssertionError { + List list; + switch (testCase) { + case 0: + list = db.from(p).where(p.productName).is("Chai").select(); + assertEquals(1, list.size()); + assertEquals("Chai", list.get(0).productName); + break; + case 1: + list = db.from(p).where(p.category).is("Condiments").select(); + assertEquals(5, list.size()); + break; + case 3: + list = db.from(p).where(p.productName).is("Aniseed Syrup").select(); + assertEquals(1, list.size()); + assertEquals("Aniseed Syrup", list.get(0).productName); + break; + case 4: + list = db.from(p).where(p.productName).like("Chef%").select(); + assertEquals(2, list.size()); + assertTrue(list.get(0).productName.startsWith("Chef")); + assertTrue(list.get(1).productName.startsWith("Chef")); + break; + case 6: + list = db.from(p).where(p.unitsInStock).exceeds(0).select(); + assertEquals(9, list.size()); + break; + case 7: + list = db.from(p).where(p.unitsInStock).is(0).select(); + assertEquals(1, list.size()); + assertEquals("Chef Anton's Gumbo Mix", list.get(0).productName); + break; + case 9: + list = db.from(p).where(p.productId).is(7).select(); + assertEquals(1, list.size()); + assertTrue(7 == list.get(0).productId); + break; + default: + list = db.from(p).select(); + assertEquals(10, list.size()); + } + } +} diff --git a/tests/com/iciql/test/IciqlSuite.java b/tests/com/iciql/test/IciqlSuite.java new file mode 100644 index 0000000..b235f6d --- /dev/null +++ b/tests/com/iciql/test/IciqlSuite.java @@ -0,0 +1,36 @@ +/* + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.iciql.test; + +import org.junit.Assert; +import org.junit.runner.RunWith; +import org.junit.runners.Suite; +import org.junit.runners.Suite.SuiteClasses; + +/** + * JUnit 4 iciql test suite. + * + */ +@RunWith(Suite.class) +@SuiteClasses({ AliasMapTest.class, AnnotationsTest.class, ClobTest.class, ConcurrencyTest.class, + ModelsTest.class, SamplesTest.class, UpdateTest.class, RuntimeQueryTest.class, + StatementLoggerTest.class }) +public class IciqlSuite { + + public static void assertStartsWith(String a, String b) { + Assert.assertTrue(a.startsWith(b)); + } +} diff --git a/tests/com/iciql/test/ModelsTest.java b/tests/com/iciql/test/ModelsTest.java new file mode 100644 index 0000000..b3974b8 --- /dev/null +++ b/tests/com/iciql/test/ModelsTest.java @@ -0,0 +1,181 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.sql.SQLException; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ErrorCollector; + +import com.iciql.Db; +import com.iciql.DbInspector; +import com.iciql.DbUpgrader; +import com.iciql.DbVersion; +import com.iciql.Iciql.IQDatabase; +import com.iciql.ValidationRemark; +import com.iciql.test.models.Product; +import com.iciql.test.models.ProductAnnotationOnly; +import com.iciql.test.models.ProductMixedAnnotation; +import com.iciql.test.models.SupportedTypes; +import com.iciql.test.models.SupportedTypes.SupportedTypes2; + +/** + * Test that the mapping between classes and tables is done correctly. + */ +public class ModelsTest { + + /* + * The ErrorCollector Rule allows execution of a test to continue after the + * first problem is found and report them all at once + */ + @Rule + public ErrorCollector errorCollector = new ErrorCollector(); + + private Db db; + + private void log(String text) { + System.out.println(text); + } + + @Before + public void setUp() { + db = Db.open("jdbc:h2:mem:", "sa", "sa"); + db.insertAll(Product.getList()); + db.insertAll(ProductAnnotationOnly.getList()); + db.insertAll(ProductMixedAnnotation.getList()); + } + + @After + public void tearDown() { + db.close(); + } + + @Test + public void testValidateModels() { + DbInspector inspector = new DbInspector(db); + validateModel(inspector, new Product()); + validateModel(inspector, new ProductAnnotationOnly()); + validateModel(inspector, new ProductMixedAnnotation()); + } + + private void validateModel(DbInspector inspector, Object o) { + List remarks = inspector.validateModel(o, false); + assertTrue("Validation remarks are null for " + o.getClass().getName(), remarks != null); + log("Validation remarks for " + o.getClass().getName()); + for (ValidationRemark remark : remarks) { + log(remark.toString()); + if (remark.isError()) { + errorCollector.addError(new SQLException(remark.toString())); + } + } + } + + @Test + public void testSupportedTypes() { + List original = SupportedTypes.createList(); + db.insertAll(original); + List retrieved = db.from(SupportedTypes.SAMPLE).select(); + assertEquals(original.size(), retrieved.size()); + for (int i = 0; i < original.size(); i++) { + SupportedTypes o = original.get(i); + SupportedTypes r = retrieved.get(i); + assertTrue(o.equivalentTo(r)); + } + } + + @Test + public void testModelGeneration() { + List original = SupportedTypes.createList(); + db.insertAll(original); + DbInspector inspector = new DbInspector(db); + List models = inspector.generateModel(null, "SupportedTypes", "com.iciql.test.models", true, + true); + assertEquals(1, models.size()); + // a poor test, but a start + assertEquals(1361, models.get(0).length()); + } + + @Test + public void testDatabaseUpgrade() { + // insert a database version record + db.insert(new DbVersion(1)); + + TestDbUpgrader dbUpgrader = new TestDbUpgrader(); + db.setDbUpgrader(dbUpgrader); + + List original = SupportedTypes.createList(); + db.insertAll(original); + + assertEquals(1, dbUpgrader.oldVersion.get()); + assertEquals(2, dbUpgrader.newVersion.get()); + } + + @Test + public void testTableUpgrade() { + Db db = Db.open("jdbc:h2:mem:", "sa", "sa"); + + // insert first, this will create version record automatically + List original = SupportedTypes.createList(); + db.insertAll(original); + + // reset the dbUpgrader (clears the update check cache) + TestDbUpgrader dbUpgrader = new TestDbUpgrader(); + db.setDbUpgrader(dbUpgrader); + + SupportedTypes2 s2 = new SupportedTypes2(); + + List types = db.from(s2).select(); + assertEquals(10, types.size()); + assertEquals(1, dbUpgrader.oldVersion.get()); + assertEquals(2, dbUpgrader.newVersion.get()); + db.close(); + } + + /** + * A sample database upgrader class. + */ + @IQDatabase(version = 2) + class TestDbUpgrader implements DbUpgrader { + final AtomicInteger oldVersion = new AtomicInteger(0); + final AtomicInteger newVersion = new AtomicInteger(0); + + public boolean upgradeTable(Db db, String schema, String table, int fromVersion, int toVersion) { + // just claims success on upgrade request + oldVersion.set(fromVersion); + newVersion.set(toVersion); + return true; + } + + public boolean upgradeDatabase(Db db, int fromVersion, int toVersion) { + // just claims success on upgrade request + oldVersion.set(fromVersion); + newVersion.set(toVersion); + return true; + } + + } + +} diff --git a/tests/com/iciql/test/RuntimeQueryTest.java b/tests/com/iciql/test/RuntimeQueryTest.java new file mode 100644 index 0000000..ff6fe2a --- /dev/null +++ b/tests/com/iciql/test/RuntimeQueryTest.java @@ -0,0 +1,92 @@ +/* + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.iciql.test; + +import static org.junit.Assert.assertEquals; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +import org.junit.Test; + +import com.iciql.Db; +import com.iciql.test.models.Product; +import com.iciql.util.JdbcUtils; + +/** + * Tests the runtime dynamic query function. + */ +public class RuntimeQueryTest { + + @Test + public void testRuntimeQuery() { + Db db = Db.open("jdbc:h2:mem:", "sa", "sa"); + db.insertAll(Product.getList()); + + Product p = new Product(); + List products = db.from(p).where("unitsInStock=?", 120).orderBy(p.productId).select(); + assertEquals(1, products.size()); + + products = db.from(p).where("unitsInStock=? and productName like ? order by productId", 0, "Chef%") + .select(); + assertEquals(1, products.size()); + + db.close(); + } + + @Test + public void testExecuteQuery() throws SQLException { + Db db = Db.open("jdbc:h2:mem:", "sa", "sa"); + db.insertAll(Product.getList()); + + // test plain statement + List products = db.executeQuery(Product.class, "select * from product where unitsInStock=120"); + assertEquals(1, products.size()); + assertEquals("Condiments", products.get(0).category); + + // test prepared statement + products = db.executeQuery(Product.class, "select * from product where unitsInStock=?", 120); + assertEquals(1, products.size()); + assertEquals("Condiments", products.get(0).category); + + db.close(); + } + + @Test + public void testBuildObjects() throws SQLException { + Db db = Db.open("jdbc:h2:mem:", "sa", "sa"); + db.insertAll(Product.getList()); + + // test plain statement + ResultSet rs = db.executeQuery("select * from product where unitsInStock=120"); + List products = db.buildObjects(Product.class, rs); + JdbcUtils.closeSilently(rs, true); + + assertEquals(1, products.size()); + assertEquals("Condiments", products.get(0).category); + + // test prepared statement + rs = db.executeQuery("select * from product where unitsInStock=?", 120); + products = db.buildObjects(Product.class, rs); + JdbcUtils.closeSilently(rs, true); + + assertEquals(1, products.size()); + assertEquals("Condiments", products.get(0).category); + + db.close(); + } +} diff --git a/tests/com/iciql/test/SamplesTest.java b/tests/com/iciql/test/SamplesTest.java new file mode 100644 index 0000000..309bd5d --- /dev/null +++ b/tests/com/iciql/test/SamplesTest.java @@ -0,0 +1,406 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.test; + +import static com.iciql.Function.count; +import static com.iciql.Function.isNull; +import static com.iciql.Function.length; +import static com.iciql.Function.max; +import static com.iciql.Function.min; +import static com.iciql.Function.not; +import static com.iciql.Function.sum; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.math.BigDecimal; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.iciql.Db; +import com.iciql.Filter; +import com.iciql.test.models.ComplexObject; +import com.iciql.test.models.Customer; +import com.iciql.test.models.Order; +import com.iciql.test.models.Product; +import com.iciql.test.models.SupportedTypes; + +/** + * This is the implementation of the 101 LINQ Samples as described in + * http://msdn2.microsoft.com/en-us/vcsharp/aa336760.aspx + */ +public class SamplesTest { + + /** + * This object represents a database (actually a connection to the + * database). + */ + + Db db; + + @Before + public void setUp() { + db = Db.open("jdbc:h2:mem:", "sa", "sa"); + db.insertAll(Product.getList()); + db.insertAll(Customer.getList()); + db.insertAll(Order.getList()); + db.insertAll(ComplexObject.getList()); + } + + @After + public void tearDown() { + db.close(); + } + + /** + * A simple test table. The columns are in a different order than in the + * database. + */ + public static class TestReverse { + public String name; + public Integer id; + } + + @Test + public void testReverseColumns() { + db.executeUpdate("create table TestReverse(id int, name varchar, additional varchar)"); + TestReverse t = new TestReverse(); + t.id = 10; + t.name = "Hello"; + db.insert(t); + TestReverse check = db.from(new TestReverse()).selectFirst(); + assertEquals(t.name, check.name); + assertEquals(t.id, check.id); + } + + @Test + public void testWhereSimple2() { + + // var soldOutProducts = + // from p in products + // where p.UnitsInStock == 0 + // select p; + + Product p = new Product(); + List soldOutProducts = db.from(p).where(p.unitsInStock).is(0).orderBy(p.productId).select(); + + assertEquals("[Chef Anton's Gumbo Mix: 0]", soldOutProducts.toString()); + } + + @Test + public void testWhereSimple3() { + + // var expensiveInStockProducts = + // from p in products + // where p.UnitsInStock > 0 + // && p.UnitPrice > 3.00M + // select p; + + Product p = new Product(); + List expensiveInStockProducts = db.from(p).where(p.unitsInStock).exceeds(0).and(p.unitPrice) + .exceeds(30.0).orderBy(p.productId).select(); + + assertEquals("[Northwoods Cranberry Sauce: 6, Mishi Kobe Niku: 29, Ikura: 31]", + expensiveInStockProducts.toString()); + } + + @Test + public void testWhereSimple4() { + + // var waCustomers = + // from c in customers + // where c.Region == "WA" + // select c; + + Customer c = new Customer(); + List waCustomers = db.from(c).where(c.region).is("WA").select(); + + assertEquals("[ALFKI, ANATR]", waCustomers.toString()); + } + + @Test + public void testSelectSimple2() { + + // var productNames = + // from p in products + // select p.ProductName; + + Product p = new Product(); + List productNames = db.from(p).orderBy(p.productId).select(p.productName); + + List products = Product.getList(); + for (int i = 0; i < products.size(); i++) { + assertEquals(products.get(i).productName, productNames.get(i)); + } + } + + /** + * A result set class containing the product name and price. + */ + public static class ProductPrice { + public String productName; + public String category; + public Double price; + } + + @Test + public void testAnonymousTypes3() { + + // var productInfos = + // from p in products + // select new { + // p.ProductName, + // p.Category, + // Price = p.UnitPrice + // }; + + final Product p = new Product(); + List productInfos = db.from(p).orderBy(p.productId).select(new ProductPrice() { + { + productName = p.productName; + category = p.category; + price = p.unitPrice; + } + }); + + List products = Product.getList(); + assertEquals(products.size(), productInfos.size()); + for (int i = 0; i < products.size(); i++) { + ProductPrice pr = productInfos.get(i); + Product p2 = products.get(i); + assertEquals(p2.productName, pr.productName); + assertEquals(p2.category, pr.category); + assertEquals(p2.unitPrice, pr.price); + } + } + + /** + * A result set class containing customer data and the order total. + */ + public static class CustOrder { + public String customerId; + public Integer orderId; + public BigDecimal total; + + public String toString() { + return customerId + ":" + orderId + ":" + total; + } + } + + @Test + public void testSelectManyCompoundFrom2() { + + // var orders = + // from c in customers, + // o in c.Orders + // where o.Total < 500.00M + // select new { + // c.CustomerID, + // o.OrderID, + // o.Total + // }; + + final Customer c = new Customer(); + final Order o = new Order(); + List orders = db.from(c).innerJoin(o).on(c.customerId).is(o.customerId).where(o.total) + .lessThan(new BigDecimal("100.00")).orderBy(1).select(new CustOrder() { + { + customerId = c.customerId; + orderId = o.orderId; + total = o.total; + } + }); + + assertEquals("[ANATR:10308:88.80]", orders.toString()); + } + + @Test + public void testIsNull() { + Product p = new Product(); + String sql = db.from(p).whereTrue(isNull(p.productName)).getSQL(); + assertEquals("SELECT * FROM Product WHERE (productName IS NULL)", sql); + } + + @Test + public void testDelete() { + Product p = new Product(); + int deleted = db.from(p).where(p.productName).like("A%").delete(); + assertEquals(1, deleted); + deleted = db.from(p).delete(); + assertEquals(9, deleted); + db.insertAll(Product.getList()); + db.deleteAll(Product.getList()); + assertEquals(0, db.from(p).selectCount()); + db.insertAll(Product.getList()); + } + + @Test + public void testOrAndNot() { + Product p = new Product(); + String sql = db.from(p).whereTrue(not(isNull(p.productName))).getSQL(); + assertEquals("SELECT * FROM Product WHERE (NOT productName IS NULL)", sql); + sql = db.from(p).whereTrue(not(isNull(p.productName))).getSQL(); + assertEquals("SELECT * FROM Product WHERE (NOT productName IS NULL)", sql); + sql = db.from(p).whereTrue(db.test(p.productId).is(1)).getSQL(); + assertEquals("SELECT * FROM Product WHERE ((productId = ?))", sql); + } + + @Test + public void testLength() { + Product p = new Product(); + List lengths = db.from(p).where(length(p.productName)).lessThan(10).orderBy(1) + .selectDistinct(length(p.productName)); + assertEquals("[4, 5]", lengths.toString()); + } + + @Test + public void testSum() { + Product p = new Product(); + Long sum = db.from(p).selectFirst(sum(p.unitsInStock)); + assertEquals(323, sum.intValue()); + Double sumPrice = db.from(p).selectFirst(sum(p.unitPrice)); + assertEquals(313.35, sumPrice.doubleValue(), 0.001); + } + + @Test + public void testMinMax() { + Product p = new Product(); + Integer min = db.from(p).selectFirst(min(p.unitsInStock)); + assertEquals(0, min.intValue()); + String minName = db.from(p).selectFirst(min(p.productName)); + assertEquals("Aniseed Syrup", minName); + Double max = db.from(p).selectFirst(max(p.unitPrice)); + assertEquals(97.0, max.doubleValue(), 0.001); + } + + @Test + public void testLike() { + Product p = new Product(); + List aList = db.from(p).where(p.productName).like("Cha%").orderBy(p.productName).select(); + assertEquals("[Chai: 39, Chang: 17]", aList.toString()); + } + + @Test + public void testCount() { + long count = db.from(new Product()).selectCount(); + assertEquals(10, count); + } + + @Test + public void testComplexObject() { + ComplexObject co = new ComplexObject(); + String sql = db.from(co).where(co.id).is(1).and(co.amount).is(1L).and(co.birthday) + .lessThan(new java.util.Date()).and(co.created) + .lessThan(java.sql.Timestamp.valueOf("2005-05-05 05:05:05")).and(co.name).is("hello").and(co.time) + .lessThan(java.sql.Time.valueOf("23:23:23")).and(co.value).is(new BigDecimal("1")).getSQL(); + assertEquals("SELECT * FROM ComplexObject " + "WHERE id = ? " + "AND amount = ? " + "AND birthday < ? " + + "AND created < ? " + "AND name = ? " + "AND time < ? " + "AND value = ?", sql); + + long count = db.from(co).where(co.id).is(1).and(co.amount).is(1L).and(co.birthday) + .lessThan(new java.util.Date()).and(co.created) + .lessThan(java.sql.Timestamp.valueOf("2005-05-05 05:05:05")).and(co.name).is("hello").and(co.time) + .lessThan(java.sql.Time.valueOf("23:23:23")).and(co.value).is(new BigDecimal("1")).selectCount(); + assertEquals(1, count); + } + + @Test + public void testComplexObject2() { + testComplexObject2(1, "hello"); + } + + private void testComplexObject2(final int x, final String name) { + final ComplexObject co = new ComplexObject(); + + String sql = db.from(co).where(new Filter() { + public boolean where() { + return co.id == x && co.name.equals(name) && co.name.equals("hello"); + } + }).getSQL(); + assertEquals("SELECT * FROM ComplexObject " + "WHERE id=? " + "AND ?=name " + "AND 'hello'=name", sql); + + long count = db.from(co).where(new Filter() { + public boolean where() { + return co.id == x && co.name.equals(name) && co.name.equals("hello"); + } + }).selectCount(); + + assertEquals(1, count); + } + + @Test + public void testLimitOffset() { + Set ids = new HashSet(); + Product p = new Product(); + for (int i = 0; i < 5; i++) { + List products = db.from(p).limit(2).offset(2 * i).select(); + assertTrue(products.size() == 2); + for (Product prod : products) { + assertTrue("Failed to add product id. Duplicate?", ids.add(prod.productId)); + } + } + } + + @Test + public void testKeyRetrieval() { + List list = SupportedTypes.createList(); + List keys = db.insertAllAndGetKeys(list); + Set uniqueKeys = new HashSet(); + for (Long l : keys) { + assertTrue("Failed to add key. Duplicate?", uniqueKeys.add(l)); + } + } + + /** + * A result set class containing product groups. + */ + public static class ProductGroup { + public String category; + public Long productCount; + + public String toString() { + return category + ":" + productCount; + } + } + + @Test + public void testGroup() { + + // var orderGroups = + // from p in products + // group p by p.Category into g + // select new { + // Category = g.Key, + // Products = g + // }; + + final Product p = new Product(); + List list = db.from(p).groupBy(p.category).orderBy(1).select(new ProductGroup() { + { + category = p.category; + productCount = count(); + } + }); + + assertEquals("[Beverages:2, Condiments:5, " + "Meat/Poultry:1, Produce:1, Seafood:1]", list.toString()); + } + +} diff --git a/tests/com/iciql/test/StatementLoggerTest.java b/tests/com/iciql/test/StatementLoggerTest.java new file mode 100644 index 0000000..212faf2 --- /dev/null +++ b/tests/com/iciql/test/StatementLoggerTest.java @@ -0,0 +1,39 @@ +/* + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.test; + +import org.junit.Test; + +import com.iciql.Db; +import com.iciql.test.models.Product; +import com.iciql.util.StatementLogger; + +/** + * Tests the statement logger. + */ +public class StatementLoggerTest { + + @Test + public void testStatementLogger() { + StatementLogger.activateConsoleLogger(); + Db db = Db.open("jdbc:h2:mem:", "sa", "sa"); + db.insertAll(Product.getList()); + db.close(); + StatementLogger.logStats(); + StatementLogger.deactivateConsoleLogger(); + } +} diff --git a/tests/com/iciql/test/UpdateTest.java b/tests/com/iciql/test/UpdateTest.java new file mode 100644 index 0000000..9ff5593 --- /dev/null +++ b/tests/com/iciql/test/UpdateTest.java @@ -0,0 +1,160 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.test; + +import static java.sql.Date.valueOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.iciql.Db; +import com.iciql.test.models.Customer; +import com.iciql.test.models.Order; +import com.iciql.test.models.Product; + +/** + * Tests the Db.update() function. + * + * @author dmoebius at scoop dash gmbh dot de + */ +public class UpdateTest { + + private Db db; + + @Before + public void setUp() throws Exception { + db = Db.open("jdbc:h2:mem:", "sa", "sa"); + db.insertAll(Product.getList()); + db.insertAll(Customer.getList()); + db.insertAll(Order.getList()); + } + + @After + public void tearDown() { + db.close(); + } + + @Test + public void testSimpleUpdate() { + Product p = new Product(); + Product pChang = db.from(p).where(p.productName).is("Chang").selectFirst(); + // update unitPrice from 19.0 to 19.5 + pChang.unitPrice = 19.5; + // update unitsInStock from 17 to 16 + pChang.unitsInStock = 16; + db.update(pChang); + + Product p2 = new Product(); + Product pChang2 = db.from(p2).where(p2.productName).is("Chang").selectFirst(); + assertEquals(19.5, pChang2.unitPrice.doubleValue(), 0.001); + assertEquals(16, pChang2.unitsInStock.intValue()); + + // undo update + pChang.unitPrice = 19.0; + pChang.unitsInStock = 17; + db.update(pChang); + } + + @Test + public void testSimpleUpdateWithCombinedPrimaryKey() { + Order o = new Order(); + Order ourOrder = db.from(o).where(o.orderDate).is(valueOf("2007-01-02")).selectFirst(); + ourOrder.orderDate = valueOf("2007-01-03"); + db.update(ourOrder); + + Order ourUpdatedOrder = db.from(o).where(o.orderDate).is(valueOf("2007-01-03")).selectFirst(); + assertTrue("updated order not found", ourUpdatedOrder != null); + + // undo update + ourOrder.orderDate = valueOf("2007-01-02"); + db.update(ourOrder); + } + + @Test + public void testSimpleMerge() { + Product p = new Product(); + Product pChang = db.from(p).where(p.productName).is("Chang").selectFirst(); + // update unitPrice from 19.0 to 19.5 + pChang.unitPrice = 19.5; + // update unitsInStock from 17 to 16 + pChang.unitsInStock = 16; + db.merge(pChang); + + Product p2 = new Product(); + Product pChang2 = db.from(p2).where(p2.productName).is("Chang").selectFirst(); + assertEquals(19.5, pChang2.unitPrice, 0.001); + assertEquals(16, pChang2.unitsInStock.intValue()); + + // undo update + pChang.unitPrice = 19.0; + pChang.unitsInStock = 17; + db.merge(pChang); + } + + @Test + public void testSimpleMergeWithCombinedPrimaryKey() { + Order o = new Order(); + Order ourOrder = db.from(o).where(o.orderDate).is(valueOf("2007-01-02")).selectFirst(); + ourOrder.orderDate = valueOf("2007-01-03"); + db.merge(ourOrder); + + Order ourUpdatedOrder = db.from(o).where(o.orderDate).is(valueOf("2007-01-03")).selectFirst(); + assertTrue("updated order not found", ourUpdatedOrder != null); + + // undo update + ourOrder.orderDate = valueOf("2007-01-02"); + db.merge(ourOrder); + } + + @Test + public void testSetColumns() { + Product p = new Product(); + Product original = db.from(p).where(p.productId).is(1).selectFirst(); + + // update string and double columns + db.from(p).set(p.productName).to("updated").increment(p.unitPrice).by(3.14).increment(p.unitsInStock) + .by(2).where(p.productId).is(1).update(); + + // confirm the data was properly updated + Product revised = db.from(p).where(p.productId).is(1).selectFirst(); + assertEquals("updated", revised.productName); + assertEquals(original.unitPrice + 3.14, revised.unitPrice, 0.001); + assertEquals(original.unitsInStock + 2, revised.unitsInStock.intValue()); + + // restore the data + db.from(p).set(p.productName).to(original.productName).set(p.unitPrice).to(original.unitPrice) + .increment(p.unitsInStock).by(-2).where(p.productId).is(1).update(); + + // confirm the data was properly restored + Product restored = db.from(p).where(p.productId).is(1).selectFirst(); + assertEquals(original.productName, restored.productName); + assertEquals(original.unitPrice, restored.unitPrice); + assertEquals(original.unitsInStock, restored.unitsInStock); + + double unitPriceOld = db.from(p).where(p.productId).is(1).selectFirst().unitPrice; + // double the unit price + db.from(p).increment(p.unitPrice).by(p.unitPrice).where(p.productId).is(1).update(); + double unitPriceNew = db.from(p).where(p.productId).is(1).selectFirst().unitPrice; + assertEquals(unitPriceOld * 2, unitPriceNew, 0.001); + + } + +} diff --git a/tests/com/iciql/test/models/ComplexObject.java b/tests/com/iciql/test/models/ComplexObject.java new file mode 100644 index 0000000..294dc27 --- /dev/null +++ b/tests/com/iciql/test/models/ComplexObject.java @@ -0,0 +1,64 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.test.models; + +import static com.iciql.Define.primaryKey; + +import java.math.BigDecimal; +import java.sql.Time; +import java.sql.Timestamp; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import com.iciql.Iciql; + +/** + * A table containing all possible data types. + */ + +public class ComplexObject implements Iciql { + public Integer id; + public Long amount; + public String name; + public BigDecimal value; + public Date birthday; + public Time time; + public Timestamp created; + + static ComplexObject build(Integer id, boolean isNull) { + ComplexObject obj = new ComplexObject(); + obj.id = id; + obj.amount = isNull ? null : Long.valueOf(1); + obj.name = isNull ? null : "hello"; + obj.value = isNull ? null : new BigDecimal("1"); + obj.birthday = isNull ? null : java.sql.Date.valueOf("2001-01-01"); + obj.time = isNull ? null : Time.valueOf("10:20:30"); + obj.created = isNull ? null : Timestamp.valueOf("2002-02-02 02:02:02"); + return obj; + } + + public void defineIQ() { + primaryKey(id); + } + + public static List getList() { + return Arrays.asList(new ComplexObject[] { build(0, true), build(1, false) }); + } + +} diff --git a/tests/com/iciql/test/models/Customer.java b/tests/com/iciql/test/models/Customer.java new file mode 100644 index 0000000..c2b8da9 --- /dev/null +++ b/tests/com/iciql/test/models/Customer.java @@ -0,0 +1,49 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.test.models; + +import java.util.Arrays; +import java.util.List; + +/** + * A table containing customer data. + */ +public class Customer { + + public String customerId; + public String region; + + public Customer() { + // public constructor + } + + public Customer(String customerId, String region) { + this.customerId = customerId; + this.region = region; + } + + public String toString() { + return customerId; + } + + public static List getList() { + Customer[] list = { new Customer("ALFKI", "WA"), new Customer("ANATR", "WA"), new Customer("ANTON", "CA") }; + return Arrays.asList(list); + } + +} diff --git a/tests/com/iciql/test/models/Order.java b/tests/com/iciql/test/models/Order.java new file mode 100644 index 0000000..f0cf7f3 --- /dev/null +++ b/tests/com/iciql/test/models/Order.java @@ -0,0 +1,65 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.test.models; + +import static com.iciql.Define.primaryKey; +import static com.iciql.Define.tableName; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import com.iciql.Iciql; + +/** + * A table containing order data. + */ + +public class Order implements Iciql { + public String customerId; + public Integer orderId; + public Date orderDate; + public BigDecimal total; + + public Order(String customerId, Integer orderId, String total, String orderDate) { + this.customerId = customerId; + this.orderId = orderId; + this.total = new BigDecimal(total); + this.orderDate = java.sql.Date.valueOf(orderDate); + } + + public Order() { + // public constructor + } + + public void defineIQ() { + tableName("Orders"); + primaryKey(customerId, orderId); + } + + public static List getList() { + Order[] list = { new Order("ALFKI", 10702, "330.00", "2007-01-02"), + new Order("ALFKI", 10952, "471.20", "2007-02-03"), new Order("ANATR", 10308, "88.80", "2007-01-03"), + new Order("ANATR", 10625, "479.75", "2007-03-03"), new Order("ANATR", 10759, "320.00", "2007-04-01"), + new Order("ANTON", 10365, "403.20", "2007-02-13"), new Order("ANTON", 10682, "375.50", "2007-03-13"), + new Order("ANTON", 10355, "480.00", "2007-04-11") }; + return Arrays.asList(list); + } + +} diff --git a/tests/com/iciql/test/models/Product.java b/tests/com/iciql/test/models/Product.java new file mode 100644 index 0000000..735ebab --- /dev/null +++ b/tests/com/iciql/test/models/Product.java @@ -0,0 +1,82 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.test.models; + +import static com.iciql.Define.index; +import static com.iciql.Define.maxLength; +import static com.iciql.Define.primaryKey; +import static com.iciql.Define.tableName; + +import java.util.Arrays; +import java.util.List; + +import com.iciql.Iciql; + +/** + * A table containing product data. + */ + +public class Product implements Iciql { + + public Integer productId; + public String productName; + public String category; + public Double unitPrice; + public Integer unitsInStock; + + public Product() { + // public constructor + } + + private Product(int productId, String productName, String category, double unitPrice, int unitsInStock) { + this.productId = productId; + this.productName = productName; + this.category = category; + this.unitPrice = unitPrice; + this.unitsInStock = unitsInStock; + } + + public void defineIQ() { + tableName("Product"); + primaryKey(productId); + maxLength(category, 255); + index(productName, category); + } + + private static Product create(int productId, String productName, String category, double unitPrice, int unitsInStock) { + return new Product(productId, productName, category, unitPrice, unitsInStock); + } + + public static List getList() { + Product[] list = { create(1, "Chai", "Beverages", 18, 39), create(2, "Chang", "Beverages", 19.0, 17), + create(3, "Aniseed Syrup", "Condiments", 10.0, 13), + create(4, "Chef Anton's Cajun Seasoning", "Condiments", 22.0, 53), + create(5, "Chef Anton's Gumbo Mix", "Condiments", 21.3500, 0), + create(6, "Grandma's Boysenberry Spread", "Condiments", 25.0, 120), + create(7, "Uncle Bob's Organic Dried Pears", "Produce", 30.0, 15), + create(8, "Northwoods Cranberry Sauce", "Condiments", 40.0, 6), + create(9, "Mishi Kobe Niku", "Meat/Poultry", 97.0, 29), create(10, "Ikura", "Seafood", 31.0, 31), }; + + return Arrays.asList(list); + } + + public String toString() { + return productName + ": " + unitsInStock; + } + +} diff --git a/tests/com/iciql/test/models/ProductAnnotationOnly.java b/tests/com/iciql/test/models/ProductAnnotationOnly.java new file mode 100644 index 0000000..7921237 --- /dev/null +++ b/tests/com/iciql/test/models/ProductAnnotationOnly.java @@ -0,0 +1,94 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.test.models; + +import java.util.Arrays; +import java.util.List; + +import com.iciql.Iciql.IQColumn; +import com.iciql.Iciql.IQIndex; +import com.iciql.Iciql.IQTable; + +/** + * A table containing product data. + */ + +@IQTable(name = "AnnotatedProduct", primaryKey = "id") +@IQIndex(standard = "name, cat") +public class ProductAnnotationOnly { + + @IQColumn(autoIncrement = true) + public Integer autoIncrement; + + public String unmappedField; + + @IQColumn(name = "id") + public Integer productId; + + @IQColumn(name = "cat", maxLength = 15, trimString = true) + public String category; + + @IQColumn(name = "name") + private String productName; + + @SuppressWarnings("unused") + @IQColumn + private Double unitPrice; + + @IQColumn + private Integer unitsInStock; + + public ProductAnnotationOnly() { + // public constructor + } + + private ProductAnnotationOnly(int productId, String productName, String category, double unitPrice, + int unitsInStock, String unmappedField) { + this.productId = productId; + this.productName = productName; + this.category = category; + this.unitPrice = unitPrice; + this.unitsInStock = unitsInStock; + this.unmappedField = unmappedField; + } + + private static ProductAnnotationOnly create(int productId, String productName, String category, double unitPrice, + int unitsInStock, String unmappedField) { + return new ProductAnnotationOnly(productId, productName, category, unitPrice, unitsInStock, unmappedField); + } + + public static List getList() { + String unmappedField = "unmapped"; + ProductAnnotationOnly[] list = { create(1, "Chai", "Beverages", 18, 39, unmappedField), + create(2, "Chang", "Beverages", 19.0, 17, unmappedField), + create(3, "Aniseed Syrup", "Condiments", 10.0, 13, unmappedField), + create(4, "Chef Anton's Cajun Seasoning", "Condiments", 22.0, 53, unmappedField), + create(5, "Chef Anton's Gumbo Mix", "Condiments", 21.3500, 0, unmappedField), + create(6, "Grandma's Boysenberry Spread", "Condiments", 25.0, 120, unmappedField), + create(7, "Uncle Bob's Organic Dried Pears", "Produce", 30.0, 15, unmappedField), + create(8, "Northwoods Cranberry Sauce", "Condiments", 40.0, 6, unmappedField), + create(9, "Mishi Kobe Niku", "Meat/Poultry", 97.0, 29, unmappedField), + create(10, "Ikura", "Seafood", 31.0, 31, unmappedField), }; + return Arrays.asList(list); + } + + public String toString() { + return productName + ": " + unitsInStock; + } + +} diff --git a/tests/com/iciql/test/models/ProductInheritedAnnotation.java b/tests/com/iciql/test/models/ProductInheritedAnnotation.java new file mode 100644 index 0000000..0a63aae --- /dev/null +++ b/tests/com/iciql/test/models/ProductInheritedAnnotation.java @@ -0,0 +1,63 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.test.models; + +import java.util.Arrays; +import java.util.List; + +import com.iciql.Iciql.IQTable; + +/** + * This class inherits all its fields from a parent class which has annotated + * columns. The IQTable annotation of the parent class is ignored and only the + * IQTable annotation of this class matters. However, this table inherits + * IQColumns from its super class. + */ +@IQTable(inheritColumns = true, annotationsOnly = false) +public class ProductInheritedAnnotation extends ProductMixedAnnotation { + + public ProductInheritedAnnotation() { + // public constructor + } + + private ProductInheritedAnnotation(int productId, String productName, String category, double unitPrice, + int unitsInStock, String mappedField) { + super(productId, productName, category, unitPrice, unitsInStock, mappedField); + } + + private static ProductInheritedAnnotation create(int productId, String productName, String category, + double unitPrice, int unitsInStock, String mappedField) { + return new ProductInheritedAnnotation(productId, productName, category, unitPrice, unitsInStock, mappedField); + } + + public static List getData() { + String mappedField = "mapped"; + ProductInheritedAnnotation[] list = { create(1, "Chai", "Beverages", 18, 39, mappedField), + create(2, "Chang", "Beverages", 19.0, 17, mappedField), + create(3, "Aniseed Syrup", "Condiments", 10.0, 13, mappedField), + create(4, "Chef Anton's Cajun Seasoning", "Condiments", 22.0, 53, mappedField), + create(5, "Chef Anton's Gumbo Mix", "Condiments", 21.3500, 0, mappedField), + create(6, "Grandma's Boysenberry Spread", "Condiments", 25.0, 120, mappedField), + create(7, "Uncle Bob's Organic Dried Pears", "Produce", 30.0, 15, mappedField), + create(8, "Northwoods Cranberry Sauce", "Condiments", 40.0, 6, mappedField), + create(9, "Mishi Kobe Niku", "Meat/Poultry", 97.0, 29, mappedField), + create(10, "Ikura", "Seafood", 31.0, 31, mappedField), }; + return Arrays.asList(list); + } + +} diff --git a/tests/com/iciql/test/models/ProductMixedAnnotation.java b/tests/com/iciql/test/models/ProductMixedAnnotation.java new file mode 100644 index 0000000..f90bd65 --- /dev/null +++ b/tests/com/iciql/test/models/ProductMixedAnnotation.java @@ -0,0 +1,94 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.test.models; + +import java.util.Arrays; +import java.util.List; + +import com.iciql.Iciql.IQColumn; +import com.iciql.Iciql.IQIndex; +import com.iciql.Iciql.IQTable; + +/** + * A table containing product data. + */ + +@IQTable(annotationsOnly = false) +@IQIndex(standard = "name, cat") +public class ProductMixedAnnotation { + + public Double unitPrice; + public Integer unitsInStock; + public String mappedField; + + @IQColumn(name = "cat", maxLength = 255) + public String category; + + @IQColumn(name = "id", primaryKey = true) + private Integer productId; + + @IQColumn(name = "name") + private String productName; + + public ProductMixedAnnotation() { + // public constructor + } + + protected ProductMixedAnnotation(int productId, String productName, String category, double unitPrice, + int unitsInStock, String mappedField) { + this.productId = productId; + this.productName = productName; + this.category = category; + this.unitPrice = unitPrice; + this.unitsInStock = unitsInStock; + this.mappedField = mappedField; + } + + private static ProductMixedAnnotation create(int productId, String productName, String category, double unitPrice, + int unitsInStock, String mappedField) { + return new ProductMixedAnnotation(productId, productName, category, unitPrice, unitsInStock, mappedField); + } + + public static List getList() { + String mappedField = "mapped"; + ProductMixedAnnotation[] list = { create(1, "Chai", "Beverages", 18, 39, mappedField), + create(2, "Chang", "Beverages", 19.0, 17, mappedField), + create(3, "Aniseed Syrup", "Condiments", 10.0, 13, mappedField), + create(4, "Chef Anton's Cajun Seasoning", "Condiments", 22.0, 53, mappedField), + create(5, "Chef Anton's Gumbo Mix", "Condiments", 21.3500, 0, mappedField), + create(6, "Grandma's Boysenberry Spread", "Condiments", 25.0, 120, mappedField), + create(7, "Uncle Bob's Organic Dried Pears", "Produce", 30.0, 15, mappedField), + create(8, "Northwoods Cranberry Sauce", "Condiments", 40.0, 6, mappedField), + create(9, "Mishi Kobe Niku", "Meat/Poultry", 97.0, 29, mappedField), + create(10, "Ikura", "Seafood", 31.0, 31, mappedField), }; + return Arrays.asList(list); + } + + public String toString() { + return productName + ": " + unitsInStock; + } + + public int id() { + return productId; + } + + public String name() { + return productName; + } + +} diff --git a/tests/com/iciql/test/models/ProductNoCreateTable.java b/tests/com/iciql/test/models/ProductNoCreateTable.java new file mode 100644 index 0000000..074af35 --- /dev/null +++ b/tests/com/iciql/test/models/ProductNoCreateTable.java @@ -0,0 +1,59 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.test.models; + +import java.util.Arrays; +import java.util.List; + +import com.iciql.Iciql.IQColumn; +import com.iciql.Iciql.IQTable; + +/** + * A table containing product data. + */ + +@IQTable(createIfRequired = false) +public class ProductNoCreateTable { + + @SuppressWarnings("unused") + @IQColumn(name = "id") + private Integer productId; + + @SuppressWarnings("unused") + @IQColumn(name = "name") + private String productName; + + public ProductNoCreateTable() { + // public constructor + } + + private ProductNoCreateTable(int productId, String productName) { + this.productId = productId; + this.productName = productName; + } + + private static ProductNoCreateTable create(int productId, String productName) { + return new ProductNoCreateTable(productId, productName); + } + + public static List getList() { + ProductNoCreateTable[] list = { create(1, "Chai"), create(2, "Chang") }; + return Arrays.asList(list); + } + +} diff --git a/tests/com/iciql/test/models/SupportedTypes.java b/tests/com/iciql/test/models/SupportedTypes.java new file mode 100644 index 0000000..26692fc --- /dev/null +++ b/tests/com/iciql/test/models/SupportedTypes.java @@ -0,0 +1,133 @@ +/* + * Copyright 2004-2011 H2 Group. + * Copyright 2011 James Moger. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.iciql.test.models; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Random; + +import com.iciql.Iciql.IQColumn; +import com.iciql.Iciql.IQTable; +import com.iciql.util.Utils; + +/** + * A data class that contains a column for each data type. + */ +@IQTable(strictTypeMapping = true, version = 1) +public class SupportedTypes { + + public static final SupportedTypes SAMPLE = new SupportedTypes(); + + @IQColumn(primaryKey = true, autoIncrement = true) + public Integer id; + + @IQColumn + private Boolean myBool = false; + + @IQColumn + private Byte myByte = 2; + + @IQColumn + private Short myShort; + + @IQColumn + private Integer myInteger; + + @IQColumn + private Long myLong; + + @IQColumn + private Float myFloat = 1.0f; + + @IQColumn + private Double myDouble; + + @IQColumn + private BigDecimal myBigDecimal; + + @IQColumn + private String myString; + + @IQColumn + private java.util.Date myUtilDate; + + @IQColumn + private java.sql.Date mySqlDate; + + @IQColumn + private java.sql.Time mySqlTime; + + @IQColumn + private java.sql.Timestamp mySqlTimestamp; + + public static List createList() { + List list = Utils.newArrayList(); + for (int i = 0; i < 10; i++) { + list.add(randomValue()); + } + return list; + } + + static SupportedTypes randomValue() { + Random rand = new Random(); + SupportedTypes s = new SupportedTypes(); + s.myBool = new Boolean(rand.nextBoolean()); + s.myByte = new Byte((byte) rand.nextInt(Byte.MAX_VALUE)); + s.myShort = new Short((short) rand.nextInt(Short.MAX_VALUE)); + s.myInteger = new Integer(rand.nextInt()); + s.myLong = new Long(rand.nextLong()); + s.myFloat = new Float(rand.nextFloat()); + s.myDouble = new Double(rand.nextDouble()); + s.myBigDecimal = new BigDecimal(rand.nextDouble()); + s.myString = Long.toHexString(rand.nextLong()); + s.myUtilDate = new java.util.Date(rand.nextLong()); + s.mySqlDate = new java.sql.Date(rand.nextLong()); + s.mySqlTime = new java.sql.Time(rand.nextLong()); + s.mySqlTimestamp = new java.sql.Timestamp(rand.nextLong()); + return s; + } + + public boolean equivalentTo(SupportedTypes s) { + boolean same = true; + same &= myBool.equals(s.myBool); + same &= myByte.equals(s.myByte); + same &= myShort.equals(s.myShort); + same &= myInteger.equals(s.myInteger); + same &= myLong.equals(s.myLong); + same &= myFloat.equals(s.myFloat); + same &= myDouble.equals(s.myDouble); + same &= myBigDecimal.equals(s.myBigDecimal); + same &= myUtilDate.getTime() == s.myUtilDate.getTime(); + same &= mySqlTimestamp.getTime() == s.mySqlTimestamp.getTime(); + same &= mySqlDate.toString().equals(s.mySqlDate.toString()); + same &= mySqlTime.toString().equals(s.mySqlTime.toString()); + same &= myString.equals(s.myString); + return same; + } + + /** + * This class demonstrates the table upgrade. + */ + @IQTable(name = "SupportedTypes", version = 2, inheritColumns = true, strictTypeMapping = true) + public static class SupportedTypes2 extends SupportedTypes { + + public SupportedTypes2() { + // nothing to do + } + } +} diff --git a/tools/ant-googlecode-0.0.3.jar b/tools/ant-googlecode-0.0.3.jar new file mode 100644 index 0000000000000000000000000000000000000000..452fa84f87776f74c4e5e9f858b88df430b79489 GIT binary patch literal 17983 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