├── .gitignore ├── LICENSE ├── README.md ├── annotations-test └── src │ └── test │ └── java │ └── nl │ └── kii │ └── util │ └── annotations │ ├── TestConstructorAnnotation.xtend │ ├── TestCopyMethodsAnnotation.xtend │ └── TestNamedParamsAnnotation.xtend ├── annotations └── src │ └── main │ └── java │ └── nl │ └── kii │ └── util │ └── annotation │ ├── ActiveAnnotationTools.xtend │ ├── Constructor.xtend │ ├── CopyMethods.xtend │ ├── NamedParams.xtend │ └── processor │ ├── ConstructorProcessor.xtend │ ├── CopyMethodsProcessor.xtend │ └── NamedParamsProcessor.xtend ├── build.gradle ├── core └── src │ ├── main │ └── java │ │ └── nl │ │ └── kii │ │ └── util │ │ ├── AssertionException.xtend │ │ ├── Cached.xtend │ │ ├── CloseableExtensions.xtend │ │ ├── DateExtensions.xtend │ │ ├── FunctionExtensions.xtend │ │ ├── IterableExtensions.xtend │ │ ├── Log.xtend │ │ ├── LogExtensions.xtend │ │ ├── MapExtensions.xtend │ │ ├── Matcher.xtend │ │ ├── MultiDateFormat.xtend │ │ ├── NumericExtensions.xtend │ │ ├── ObjectExtensions.xtend │ │ ├── Opt.xtend │ │ ├── OptExtensions.xtend │ │ ├── PartialURL.xtend │ │ ├── Period.xtend │ │ ├── SessionIDGenerator.xtend │ │ ├── SetOperationExtensions.xtend │ │ ├── StringExtensions.xtend │ │ ├── SyncExtensions.java │ │ ├── TemporalExtensions.xtend │ │ ├── ThrowableExtensions.xtend │ │ └── TupleExtensions.xtend │ └── test │ └── java │ └── nl │ └── kii │ └── util │ ├── TestCached.xtend │ ├── TestConversionSwitch.xtend │ ├── TestDateExtensions.xtend │ ├── TestIterableExtensions.xtend │ ├── TestMapExtensions.xtend │ ├── TestMultiDateFormat.xtend │ ├── TestPartialURL.xtend │ ├── TestStringExtensions.xtend │ ├── TestTemporalExtensions.xtend │ └── TestThrowableExtensions.xtend ├── gradle.properties ├── settings.gradle └── test └── src ├── main └── java │ └── nl │ └── kii │ └── util │ └── JUnitExtensions.xtend └── test └── java └── nl └── kii └── util └── test └── TestXtendTools.xtend /.gitignore: -------------------------------------------------------------------------------- 1 | ## Eclipse 2 | 3 | *.pydevproject 4 | .metadata 5 | .gradle 6 | bin/ 7 | tmp/ 8 | *.tmp 9 | *.bak 10 | *.swp 11 | *~.nib 12 | local.properties 13 | .settings/ 14 | .loadpath 15 | 16 | # Eclipse Core 17 | .project 18 | 19 | # External tool builders 20 | .externalToolBuilders/ 21 | 22 | # Locally stored "Eclipse launch configurations" 23 | *.launch 24 | 25 | # CDT-specific 26 | .cproject 27 | 28 | # JDT-specific (Eclipse Java Development Tools) 29 | .classpath 30 | 31 | # PDT-specific 32 | .buildpath 33 | 34 | # sbteclipse plugin 35 | .target 36 | 37 | # TeXlipse plugin 38 | .texlipse 39 | 40 | ## OSX 41 | 42 | .DS_Store 43 | .AppleDouble 44 | .LSOverride 45 | 46 | # Icon must end with two \r 47 | Icon 48 | 49 | 50 | # Thumbnails 51 | ._* 52 | 53 | # Files that might appear in the root of a volume 54 | .DocumentRevisions-V100 55 | .fseventsd 56 | .Spotlight-V100 57 | .TemporaryItems 58 | .Trashes 59 | .VolumeIcon.icns 60 | 61 | # Directories potentially created on remote AFP share 62 | .AppleDB 63 | .AppleDesktop 64 | Network Trash Folder 65 | Temporary Items 66 | .apdisk 67 | 68 | ## Java 69 | 70 | *.class 71 | 72 | # Mobile Tools for Java (J2ME) 73 | .mtj.tmp/ 74 | 75 | # Package Files # 76 | *.jar 77 | *.war 78 | *.ear 79 | 80 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 81 | hs_err_pid* 82 | 83 | ## Linux 84 | 85 | *~ 86 | 87 | # KDE directory preferences 88 | .directory 89 | 90 | # Linux trash folder which might appear on any partition or disk 91 | .Trash-* 92 | 93 | ## Xtend 94 | 95 | build/ 96 | 97 | ## Gradle 98 | 99 | gradle/ 100 | gradlew 101 | gradlew.bat 102 | 103 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xtend-tools 2 | 3 | Some tools and Xtend extensions that make life with the Xtend programming language better: 4 | 5 | - @NamedParams: named and optional parameters 6 | - Options: helps avoid NullpointerExceptions 7 | - Easy date/time manipulation 8 | - List operations and shortcuts 9 | - SL4J Logging wrapper: allows more efficient and compact logging 10 | 11 | This library has many more handy tools that Kimengi gathered over time, that are not documented. However they are easy to use, as the tests are pretty self explanatory. [See them here](https://github.com/blueneogeo/xtend-tools/tree/master/core/src/test/java/nl/kii/util) 12 | 13 | Note: Promises and Streams have been moved to the [xtend-async](https://github.com/blueneogeo/xtend-async) project. 14 | 15 | For more information about the Xtend language: http://www.eclipse.org/xtend/ 16 | 17 | ## Getting Started 18 | 19 | If you use maven or gradle, the dependency is the following: 20 | 21 | com.kimengi.util:xtend-tools:11.0.0 22 | 23 | Note: currently this library is not yet on MavenCentral. However you can install it in your local maven repository: 24 | 25 | - install gradle if you do not already have it 26 | - clone this project 27 | - in a terminal, go into this project, and do `gradle install` 28 | 29 | This will install the library. To use it add the above dependency into your project, like any maven project. 30 | 31 | ## NamedParams 32 | 33 | A drawback in both Java and Xtend is that constructors and methods can have a lot of parameters, which are then often replaced by setters. However that means a lot of new properties, and you lose immutability. 34 | 35 | By using the **@NamedParams** Active Annotation, you can set your parameters using a closure. 36 | 37 | ### The Problem 38 | 39 | For example, you have this existing User constructor: 40 | 41 | new( 42 | String name, 43 | String owner, 44 | int age, 45 | int duration, 46 | boolean isActive, 47 | boolean isAdmin) { 48 | // some init code 49 | } 50 | 51 | Calling it is a pain: 52 | 53 | // what does each parameter mean? 54 | new User(‘John’, ‘Mary’, 40, 235, true, false) 55 | 56 | So you may make a shortcut constructor: 57 | 58 | // an extra constructor for most cases 59 | new(String name, int age) { 60 | this(name, null, age, 10, true, false) 61 | } 62 | 63 | All a lot of work, especially if more that one parameter is optional. Also, the null is dangerous. You would prefer to use an Optional type here, but setting that is more work. 64 | 65 | ### Using @NamedParams 66 | 67 | @NamedParams can be used for both methods and constructors. This is how you can implement the above long constructor: 68 | 69 | @NamedParams 70 | new( 71 | String name, 72 | @Nullable String owner, 73 | Integer age, 74 | @DefaultValue(10) int duration, 75 | @DefaultTrue boolean isActive, 76 | @DefaultFalse boolean isAdmin) { 77 | // some init code 78 | } 79 | 80 | In short, you annotate it with @NamedParams, and use annotations for parameters to indicate if they have a default value or if they are options. Nothing may be null, except if it is annotated with @Nullable 81 | 82 | The default annotations are: 83 | 84 | - @Default(String value) for default string value 85 | - @DefaultValue(double value) for default number values 86 | - @DefaultTrue for default boolean true 87 | - @DefaultFalse for default boolean false 88 | - @Nullable if the value may be null (runtime exception otherwise if not passed and no default) 89 | - @Locked to keep a parameter in the method call parameters 90 | 91 | The @NamedParams method creates some extra methods for you in the class, along with a parameters class. 92 | 93 | You can then use it like this: 94 | 95 | new User [ 96 | name = ‘John’ 97 | admin = ‘Mary’ 98 | age = 40 99 | duration = 235 100 | active = true 101 | admin = false 102 | ] 103 | 104 | Notice that you can set the admin property directly as a String, using a setter method on the closure. It will set the option for you. 105 | 106 | You can also call the constructor like this: 107 | 108 | new User [ 109 | name = ‘John’ 110 | age = 40 111 | ] 112 | 113 | Now admin will be None, duration will be 10, active will be true, and admin will be false. 114 | 115 | If you fail to set name or age, you will get a NullPointerException at runtime, before your constructor is called. 116 | 117 | #### Locking a Parameter 118 | 119 | To use a method as an extension method, you need the first parameter to be fixed. For example: 120 | 121 | def static save(Database db, String key, Role role, Security security) { … } 122 | 123 | Which can then be called like this: 124 | 125 | db.save(key, role, security) 126 | 127 | If you were to add the @NamedParams method here, you would not be able to call it as an extension with the closure. 128 | 129 | What you want is to lock the first parameter to the method in place. You can do this with the @Locked annotation. 130 | 131 | @NamedParams 132 | def static save(@Locked Database db, String key, Role role, Security security) { … } 133 | 134 | Now you can use it like an extension: 135 | 136 | db.save [ key = … role = … security = … ] 137 | 138 | ## Option Programming 139 | 140 | import static extension nl.kii.util.OptExtensions.* 141 | import static extension nl.kii.util.* 142 | 143 | A problem in Java is catching NullPointerExceptions. You end up with a lot of NullPointerException checks, or with code that throws one while your program runs. Languages like Scala use the idea of an Option to solve this. It forces you to catch these errors at compile time. By marking something an Opt instead of a T, you are saying that the result is optional, and that the programmer should handle the case of no result. 144 | 145 | The interface of Opt is very simple: 146 | 147 | https://github.com/blueneogeo/xtend-tools/blob/master/core/src/main/java/nl/kii/util/Opt.xtend 148 | 149 | #### Opt, Some, None, Err 150 | 151 | An Opt can be a Some, None or Err. If it is a Some, you can use the value method to get the value. The option extensions let you create an option from a value really easily. For example: 152 | 153 | import static extension nl.kii.util.OptExtensions.* 154 | … 155 | val o = new Some(12) 156 | o.value == 12 // true 157 | 158 | some(12).value == 12 // extension shortcut 159 | 12.option.value == 12 // extension shortcut 160 | 161 | #### .option and .or Extensions 162 | 163 | The .option extension also works on null values. This allows you to wrap calls that can return null into options. For example, say that you have a method: 164 | 165 | def User getUser(long userId) { … } // may return null 166 | 167 | You can then either choose to alter the method itself: 168 | 169 | def Opt getUser(long userId) { 170 | .. original code .. 171 | return user.option 172 | } 173 | 174 | Or if this is impractical, wrap it when you make the call: 175 | 176 | val Opt result = getUser(userId).option 177 | 178 | All this becomes useful when you use it as the result of a function: 179 | 180 | val user = getUser(12).or(defaultUser) 181 | 182 | Here findUser returns an Opt. The .or extension method gives either the found value, or if there is no value, a default. There is also one that executes a closure: 183 | 184 | val user = findUser(12).or [ loadDefaultUser() ] 185 | 186 | These can also be chained: 187 | 188 | val user = findUser(12) 189 | .or [ findUser(defaultUserId) ] 190 | .orThrow [ new Exception('help!') ] 191 | 192 | #### Attempt 193 | 194 | With attempt, you can catch errors, much like when you perform a try/catch. However the difference is that attempt will not throw an Exception, but instead will return an Err, None or Some(value), depending on what happened when executing the passed function. 195 | 196 | val Opt user = attempt [ findUser(id) ] 197 | 198 | #### Conditional Processing 199 | 200 | To perform conditional processing (user is an Opt): 201 | 202 | ifSome(user) [ ..do something with the user.. ] 203 | 204 | #### Optional Mapping 205 | 206 | To map a value that may not exist: 207 | 208 | val Opt age = user.mapOpt [ it.age ] // user is an Opt 209 | 210 | This is just a selection. For more extensions, check the source of OptExtensions.xtend: 211 | 212 | https://github.com/blueneogeo/xtend-tools/blob/master/core/src/main/java/nl/kii/util/OptExtensions.xtend 213 | 214 | ## Easy Date Manipulating 215 | 216 | import static extension nl.kii.util.DateExtensions.* 217 | 218 | Date manipulation becomes very natural with these extensions, since they overload the common operators + - < <= > and =>. Some code examples: 219 | 220 | // does what it says: 221 | println(now + 4.mins + 2.secs) 222 | 223 | // easily calculate moments and periods 224 | val Date yesterday = now - 24.hours 225 | val Date tomorrow = now + 1.days 226 | val Date in1Hour = now + 1.hour 227 | val Period oneHour30Minutes = 1.hour + 30.mins 228 | 229 | // calculate newest and oldest between values 230 | println(newest(tomorrow, in1Hour, yesterday)) // prints the value of tomorrow 231 | println(oldest(tomorrow, in1Hour)) // prints the value of in1Hour 232 | 233 | println((now - yesterday).days) // prints 1 234 | println(now > yesterday) // prints true 235 | println(2.mins.secs) // prints 120 236 | 237 | ## List Operations 238 | 239 | import static extension nl.kii.util.IterableExtensions.* 240 | 241 | Lists have been augmented with Opt and attempt operations as well: (silly example!) 242 | 243 | api.getUsers() 244 | .attemptMap [ ..do something that may throw an exception.. ] 245 | .mapOpt [ userName ] // get the usernames, only if there is a value 246 | .filterEmpty // removes the errors 247 | .count // count how often each name occurs: List> 248 | .toMap // becomes Map 249 | .toPairs // back to List> 250 | .each [ println('name:' + key) ] 251 | .each [ println('occurrence:' + value) ] 252 | .map [ value ] // getting just the occurrences 253 | .avg // average the occurrences 254 | 255 | As you can see, the difference between the standard List.forEach in Xtend and List.each here is that you can chain it. 256 | 257 | See for more operations IterableExtensions.xtend: 258 | 259 | https://github.com/blueneogeo/xtend-tools/blob/master/core/src/main/java/nl/kii/util/IterableExtensions.xtend 260 | 261 | #### List Operator Overloading 262 | 263 | You can perform each with an overload: 264 | 265 | users.each [ … ] 266 | 267 | You can also add to a list with an overload: 268 | 269 | list.add(3) 270 | list.add(5) 271 | list << 3 << 5 // almost the same thing 272 | 273 | What is different is that the overload either calls List.add or IteratorExtensions.safeAdd, depending on whether the list is immutable. If it is immutable, it will perform an immutable add. 274 | 275 | This means that if your list is immutable, you have to catch the result: 276 | 277 | // Integer.string creates an immutable list 278 | val list = Integer.string 279 | val list2 = list << 2 << 5 // catch result in list2 280 | 281 | Often you can also reverse the direction. For example, this has the same result: 282 | 283 | 2 >> list // results in a longer list 284 | list << 2 // same 285 | 286 | list >> [ println(it) ] // print each item in the list 287 | [ println(it) ] << list // same 288 | 289 | ## Logging 290 | 291 | import static extension nl.kii.util.LogExtensions.* 292 | import nl.kii.util.Log 293 | 294 | You can use the logging wrapper like this: 295 | 296 | class MyClass { 297 | extension Log log = Log.create(this) 298 | 299 | def someFunction() { 300 | debug['minor implementation detail'] 301 | info['something happened!'] 302 | warn['watch out!'] 303 | error['crashed!'] 304 | } 305 | } 306 | 307 | The lambda expression/function will only be called if necessary, which helps performance. 308 | 309 | #### Logger Naming 310 | 311 | You can also add a name to the logger when you create it: 312 | 313 | extension Log log = Log.create(this, 'foo') 314 | 315 | When you do this, this message will be put in front of every log statement made by this logger. This can be handy when you want to distinguish easily between types of messages in your log. 316 | 317 | #### Logging List Results 318 | 319 | Sometimes you have a list of things that you want to log. A common pattern is to do something like: 320 | 321 | api.getUsers(condition).each [ 322 | val user = it 323 | info ['found user ' + user ] 324 | .. perform more code .. 325 | ] 326 | 327 | This pollutes your code with side effects (logging). A better alternative would be: 328 | 329 | api.getUsers(condition) 330 | .each [ 331 | val user = it 332 | info ['found user ' + user ] 333 | ] 334 | .each [ 335 | .. perform more code .. 336 | ] 337 | 338 | Since this is a common pattern, here is a shortcut: 339 | 340 | api.getUsers(condition) 341 | .info(logger) 342 | .each [ .. perform more code .. ] 343 | 344 | Optionally, you can also pass a logging message: 345 | api.getUsers(condition) 346 | .info('found users:', logger) 347 | .each [ .. perform more code .. ] 348 | 349 | #### Logger Functions 350 | 351 | You can also log like this: 352 | 353 | api.getUsers(condition) 354 | .each(info) 355 | .each [ .. perform some code .. ] 356 | 357 | Here, info actually is a function that produces a function that gets called by each. It looks cleaner with the operators described below: 358 | 359 | api.getUsers(condition) >> info >> [ .. perform some code .. ] 360 | 361 | Here too, you can add a message: 362 | 363 | api.getUsers(condition) >> info('found user:') >> [ .. perform some code .. ] 364 | 365 | Note that in this case, the message is put in front of every user. 366 | 367 | Note: the logger functions only work if you have the "extension Log logger = class.logger.wrapper" line in your class. 368 | 369 | #### Printing 370 | 371 | If you don't want to log but just want to print, You can perform the same thing as above with printEach, which just prints all in the list, and returns the list, so you can keep processing the flow. 372 | 373 | // via list extension method 374 | #['Jane', 'Mark'].printEach 375 | #['Jane', 'Mark'].printEach('celebrities:') 376 | 377 | // via printEach lambda 378 | #['Jane', 'Mark'] >> printEach 379 | #['Jane', 'Mark'] >> printEach('celebrities:') 380 | 381 | // chaining example 382 | #[1, 2, 3] 383 | >> printEach('inputs') 384 | >> [ it * 2 ] 385 | >> printEach('doubled') 386 | -------------------------------------------------------------------------------- /annotations-test/src/test/java/nl/kii/util/annotations/TestConstructorAnnotation.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util.annotations 2 | 3 | import nl.kii.util.annotation.Constructor 4 | import org.junit.Test 5 | 6 | import static org.junit.Assert.* 7 | import java.util.List 8 | 9 | class TestConstructorAnnotation { 10 | 11 | @Test 12 | def void test() { 13 | assertEquals(3, new Adder(1, 2).sum) 14 | assertEquals('12', new GenericConcatinator(1, 2).concat) 15 | assertArrayEquals(#[1, 2, 3, 4], new GenericConcatinator2(#[1, 2], #[3, 4]).concat) 16 | } 17 | 18 | } 19 | 20 | @Constructor 21 | class Adder { 22 | 23 | // there create Adder(int a, int b) 24 | val int a 25 | val int b 26 | 27 | // val int c = 2 // not added to the constructor, has a value 28 | // val static int x = 1 // not added to the constructor, is static 29 | 30 | def sum() { 31 | a + b 32 | } 33 | 34 | } 35 | 36 | @Constructor 37 | class OverriddenConstructor { 38 | 39 | val int a 40 | val int b 41 | 42 | // the annotation does not break upon you overriding the constructor it wanted to create 43 | new(int a, int b) { 44 | this.a = a 45 | this.b = b 46 | } 47 | 48 | def sum() { 49 | a + b 50 | } 51 | 52 | } 53 | 54 | 55 | @Constructor 56 | class GenericConcatinator { 57 | 58 | val T a // generic types work 59 | val T b 60 | 61 | def concat() { 62 | a.toString + b.toString 63 | } 64 | 65 | } 66 | 67 | @Constructor 68 | class GenericConcatinator2 { 69 | 70 | val List a // generic type parameters types work 71 | val List b 72 | 73 | def concat() { 74 | newLinkedList => [ 75 | addAll(a) 76 | addAll(b) 77 | ] 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /annotations-test/src/test/java/nl/kii/util/annotations/TestCopyMethodsAnnotation.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util.annotations 2 | 3 | import java.util.List 4 | import nl.kii.util.annotation.CopyMethods 5 | import org.junit.Test 6 | 7 | import static org.junit.Assert.* 8 | 9 | class TestCopyMethodsAnnotation { 10 | 11 | @Test 12 | def void test() { 13 | assertEquals(Source.add(1, 2), Destination.add(1, 2)) 14 | assertEquals(Source.add(1, 2, 3), Destination.add(1, 2, 3)) 15 | assertEquals(Source.add(1, 2, 3), Destination.add(1, 2, 3)) 16 | Source.print(10, #[11]) 17 | Destination.print(10, #[11]) 18 | } 19 | 20 | } 21 | 22 | class Source { 23 | 24 | def static int add(int a, int b) { 25 | a + b 26 | } 27 | 28 | def static int add(int a, int b, int c) { 29 | a + b + c 30 | } 31 | 32 | def static T print(T value, List justForTestingGenerics) { 33 | println(value) 34 | value 35 | } 36 | 37 | def int add2(int a, int b) { 38 | a + b 39 | } 40 | 41 | def int add2(int a, int b, int c) { 42 | a + b + c 43 | } 44 | 45 | def T print2(T value, List justForTestingGenerics) { 46 | println(value) 47 | value 48 | } 49 | 50 | } 51 | 52 | @CopyMethods(value=Source, createExtensionMethods=true) 53 | class Destination { 54 | 55 | } 56 | -------------------------------------------------------------------------------- /annotations-test/src/test/java/nl/kii/util/annotations/TestNamedParamsAnnotation.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util.annotations 2 | 3 | import nl.kii.util.annotation.Default 4 | import nl.kii.util.annotation.DefaultTrue 5 | import nl.kii.util.annotation.DefaultValue 6 | import nl.kii.util.annotation.Locked 7 | import nl.kii.util.annotation.NamedParams 8 | import org.junit.Test 9 | 10 | import static org.junit.Assert.* 11 | 12 | class TestNamedParamsAnnotation { 13 | 14 | /** A more mundane test */ 15 | @NamedParams 16 | final def getGreeting(@Locked String user, @DefaultValue(12) Integer age, @DefaultTrue boolean happy) { 17 | 'hello I am ' + user + ' and I am ' + age + ' years old and ' + if(happy) 'happy' else 'sad' 18 | } 19 | 20 | @Test 21 | def void testMethodWithDefaults() { 22 | assertEquals('hello I am christian and I am 43 years old and happy', 'christian'.getGreeting [ age = 43 ]) 23 | assertEquals('hello I am christian and I am 20 years old and happy', 'christian'.getGreeting [ age = 20 ]) 24 | assertEquals('hello I am christian and I am 43 years old and sad', 'christian'.getGreeting [ age = 43 happy = false ]) 25 | assertEquals('hello I am christian and I am 12 years old and sad', 'christian'.getGreeting [ happy = false ]) 26 | } 27 | 28 | @Test 29 | def void testConstructorWithDefaults() { 30 | assertEquals('hello I am christian and I am 43 years old and happy', new Tester[ age = 43 ].greeting) 31 | assertEquals('hello I am christian and I am 20 years old and happy', new Tester[ age = 20 ].greeting) 32 | assertEquals('hello I am christian and I am 43 years old and sad', new Tester[ age = 43 happy = false ].greeting) 33 | assertEquals('hello I am christian and I am 12 years old and sad', new Tester[ happy = false ].greeting) 34 | } 35 | 36 | } 37 | 38 | class Tester { 39 | 40 | String name 41 | int age 42 | boolean happy 43 | 44 | @NamedParams 45 | new(@Default('christian') String name, @DefaultValue(12) int age, @DefaultTrue boolean happy) { 46 | this.name = name 47 | this.age = age 48 | this.happy = happy 49 | } 50 | 51 | final def getGreeting() { 52 | 'hello I am ' + name + ' and I am ' + age + ' years old and ' + if(happy) 'happy' else 'sad' 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /annotations/src/main/java/nl/kii/util/annotation/ActiveAnnotationTools.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util.annotation 2 | 3 | import java.util.List 4 | import org.eclipse.xtend.lib.macro.TransformationContext 5 | import org.eclipse.xtend.lib.macro.declaration.ConstructorDeclaration 6 | import org.eclipse.xtend.lib.macro.declaration.MethodDeclaration 7 | import org.eclipse.xtend.lib.macro.declaration.MutableClassDeclaration 8 | import org.eclipse.xtend.lib.macro.declaration.MutableConstructorDeclaration 9 | import org.eclipse.xtend.lib.macro.declaration.MutableInterfaceDeclaration 10 | import org.eclipse.xtend.lib.macro.declaration.MutableMethodDeclaration 11 | import org.eclipse.xtend.lib.macro.declaration.MutableTypeParameterDeclaration 12 | import org.eclipse.xtend.lib.macro.declaration.TypeParameterDeclaration 13 | import org.eclipse.xtend.lib.macro.declaration.TypeReference 14 | 15 | /** 16 | * Tools for creating Active Annotations. 17 | */ 18 | class ActiveAnnotationTools { 19 | 20 | val extension TransformationContext context 21 | 22 | new(TransformationContext context) { 23 | this.context = context 24 | } 25 | 26 | /** Shortcut for context.newTypeReference(cls) */ 27 | def TypeReference ref(Class cls, TypeReference... types) { 28 | cls.newTypeReference(types) 29 | } 30 | 31 | /** Shortcut for context.newTypeReference(clsDeclaration) */ 32 | def TypeReference ref(MutableClassDeclaration clsDeclaration, TypeReference... types) { 33 | clsDeclaration.newTypeReference(types) 34 | } 35 | 36 | /** Shortcut for context.newAnnotationReference(annotationClass) */ 37 | def annotationRef(Class annotationClass) { 38 | this.context.newAnnotationReference(annotationClass) 39 | } 40 | 41 | /** 42 | * Create a copy of a typereference that adds references to any used types by name. 43 | * Solves the issue of copying the type parameters from one method to another. 44 | * The newMethodTypeParams can be generated by the copyMethodTypeParameters() method in this class. 45 | */ 46 | def TypeReference add(TypeReference originalType, Iterable newMethodTypeParams) { 47 | // is it a wildcard? 48 | if(originalType.isWildCard) return originalType 49 | // is it a direct type reference? 50 | val t = newMethodTypeParams.findFirst [ simpleName == originalType.simpleName ] 51 | if(t !== null) return t.newTypeReference 52 | // try subtype reference 53 | val subtypes = originalType.actualTypeArguments.map [ add(newMethodTypeParams) ] 54 | originalType.type.newTypeReference(subtypes) 55 | } 56 | 57 | /** 58 | * Create a copy of an existing method signature, but without the body. 59 | * Provides you with a closure that allows you to alter the new method and create the body. 60 | * @param targetCls: the class to create the copy on 61 | * @param sourceCls: the class that contains the source method 62 | * @param newName: if not null, if the existing class method is not static and contains the same signature, rename the method 63 | * @param copyAnnotations : if true, also copy all annotations from the original method 64 | * @param copyParameters: if true, also copy all parameters (including generic types) from the original method 65 | * @param transformationFn: closure that takes the new method and method type parameters and can modify it before adding to the targetCls 66 | */ 67 | def MutableMethodDeclaration addMethodCopy(MutableClassDeclaration targetCls, TypeReference sourceCls, MethodDeclaration sourceMethod, String methodName, boolean copyAnnotations, boolean copyParameters, (MutableMethodDeclaration, List)=>void methodTransformerFn) { 68 | // make sure we don't create a double copy 69 | targetCls.addMethodSafely(if(methodName !== null) methodName else sourceMethod.simpleName) [ 70 | val newMethod = it 71 | // set the basic properties of the new method 72 | primarySourceElement = sourceMethod 73 | static = sourceMethod.static 74 | final = sourceMethod.final 75 | varArgs = sourceMethod.varArgs 76 | visibility = sourceMethod.visibility 77 | deprecated = sourceMethod.deprecated 78 | docComment = ''' 79 | «sourceMethod.docComment» 80 | Original class types: «sourceMethod.parameters.map [ type.extractTypeParameterNames ].flatten» 81 | Generated extension method of «sourceMethod.signature» 82 | @see «sourceCls»#«sourceMethod.simpleName»(«FOR p : sourceMethod.parameters SEPARATOR ','»«p.simpleName»«ENDFOR») «sourceMethod.simpleName» 83 | ''' 84 | exceptions = sourceMethod.exceptions 85 | synchronized = sourceMethod.synchronized 86 | // add all types from the original method to the new method 87 | for(type : sourceCls.actualTypeArguments) { 88 | newMethod.addTypeParameter(type.simpleName) 89 | } 90 | for(type : sourceMethod.typeParameters) { 91 | newMethod.addTypeParameter(type.simpleName) 92 | } 93 | // if this is a static extension method, also detect the types being used 94 | // in the class from the used parameters and add them to the method 95 | // bit of a hack... but no way found yet to extract type parameters from a class typereference 96 | if(!sourceMethod.static) { 97 | val methodTypeParameterNames = sourceMethod.extractTypeParameterNamesFromSignature 98 | val existingTypeParameterNames = sourceMethod.typeParameters.map [ simpleName ].toList 99 | methodTypeParameterNames.removeAll(existingTypeParameterNames) 100 | for(typeParamName : methodTypeParameterNames) { 101 | newMethod.addTypeParameter(typeParamName) 102 | } 103 | } 104 | // copy annotations 105 | if(copyAnnotations) { 106 | for(annotation : sourceMethod.annotations) { 107 | addAnnotation(annotation) 108 | } 109 | } 110 | // copy parameters 111 | if(copyParameters) { 112 | if(!sourceMethod.static) { 113 | static = true 114 | val instanceName = sourceCls.simpleName.toFirstLower 115 | addParameter(instanceName + 'Instance', sourceCls) 116 | } 117 | for(parameter : sourceMethod.parameters) { 118 | addParameter(parameter.simpleName, parameter.type.add(newMethod.typeParameters)) 119 | } 120 | } 121 | returnType = sourceMethod.returnType.add(newMethod.typeParameters) 122 | methodTransformerFn.apply(it, newMethod.typeParameters.toList) 123 | ] 124 | } 125 | 126 | /** 127 | * Create a copy of an existing method signature, but without the body. 128 | * Provides you with a closure that allows you to alter the new method and create the body. 129 | * @param targetCls: the class to create the copy on 130 | * @param sourceCls: the class that contains the source method 131 | * @param newName: if not null, if the existing class method is not static and contains the same signature, rename the method 132 | * @param copyAnnotations : if true, also copy all annotations from the original method 133 | * @param copyParameters: if true, also copy all parameters (including generic types) from the original method 134 | * @param transformationFn: closure that takes the new method and method type parameters and can modify it before adding to the targetCls 135 | */ 136 | def MutableMethodDeclaration addMethodCopy(MutableInterfaceDeclaration targetCls, TypeReference sourceCls, MethodDeclaration sourceMethod, String methodName, boolean copyAnnotations, boolean copyParameters, (MutableMethodDeclaration, List)=>void methodTransformerFn) { 137 | // make sure we don't create a double copy 138 | targetCls.addMethodSafely(if(methodName !== null) methodName else sourceMethod.simpleName) [ 139 | val newMethod = it 140 | // set the basic properties of the new method 141 | primarySourceElement = sourceMethod 142 | static = sourceMethod.static 143 | final = sourceMethod.final 144 | varArgs = sourceMethod.varArgs 145 | visibility = sourceMethod.visibility 146 | deprecated = sourceMethod.deprecated 147 | docComment = ''' 148 | «sourceMethod.docComment» 149 | Original class types: «sourceMethod.parameters.map [ type.extractTypeParameterNames ].flatten» 150 | Generated extension method of «sourceMethod.signature» 151 | @see «sourceCls»#«sourceMethod.simpleName»(«FOR p : sourceMethod.parameters SEPARATOR ','»«p.simpleName»«ENDFOR») «sourceMethod.simpleName» 152 | ''' 153 | exceptions = sourceMethod.exceptions 154 | synchronized = sourceMethod.synchronized 155 | // add all types from the original method to the new method 156 | for(type : sourceCls.actualTypeArguments) { 157 | newMethod.addTypeParameter(type.simpleName) 158 | } 159 | for(type : sourceMethod.typeParameters) { 160 | newMethod.addTypeParameter(type.simpleName) 161 | } 162 | // if this is a static extension method, also detect the types being used 163 | // in the class from the used parameters and add them to the method 164 | // bit of a hack... but no way found yet to extract type parameters from a class typereference 165 | if(!sourceMethod.static) { 166 | val methodTypeParameterNames = sourceMethod.extractTypeParameterNamesFromSignature 167 | val existingTypeParameterNames = sourceMethod.typeParameters.map [ simpleName ].toList 168 | methodTypeParameterNames.removeAll(existingTypeParameterNames) 169 | for(typeParamName : methodTypeParameterNames) { 170 | newMethod.addTypeParameter(typeParamName) 171 | } 172 | } 173 | // copy annotations 174 | if(copyAnnotations) { 175 | for(annotation : sourceMethod.annotations) { 176 | addAnnotation(annotation) 177 | } 178 | } 179 | // copy parameters 180 | if(copyParameters) { 181 | if(!sourceMethod.static) { 182 | static = true 183 | val instanceName = sourceCls.simpleName.toFirstLower 184 | addParameter(instanceName + 'Instance', sourceCls) 185 | } 186 | for(parameter : sourceMethod.parameters) { 187 | addParameter(parameter.simpleName, parameter.type.add(newMethod.typeParameters)) 188 | } 189 | } 190 | returnType = sourceMethod.returnType.add(newMethod.typeParameters) 191 | methodTransformerFn.apply(it, newMethod.typeParameters.toList) 192 | ] 193 | } 194 | 195 | /** Only add a new method if it does not already exist */ 196 | def MutableMethodDeclaration addMethodSafely(MutableClassDeclaration cls, String simpleName, (MutableMethodDeclaration)=>void initializer) { 197 | val newMethod = cls.addMethod(simpleName, initializer) 198 | if(cls.newTypeReference.hasDuplicateMethod(newMethod, false)) { 199 | newMethod.remove 200 | null 201 | } else { 202 | newMethod 203 | } 204 | } 205 | 206 | /** Only add a new method if it does not already exist */ 207 | def MutableMethodDeclaration addMethodSafely(MutableInterfaceDeclaration cls, String simpleName, (MutableMethodDeclaration)=>void initializer) { 208 | val newMethod = cls.addMethod(simpleName, initializer) 209 | if(cls.newTypeReference.hasDuplicateMethod(newMethod, false)) { 210 | newMethod.remove 211 | null 212 | } else { 213 | newMethod 214 | } 215 | } 216 | 217 | /** Only add a new constructor if it does not already exist */ 218 | def MutableConstructorDeclaration addConstructorSafely(MutableClassDeclaration cls, (MutableConstructorDeclaration)=>void initializer) { 219 | val newConstructor = cls.addConstructor(initializer) 220 | if(cls.newTypeReference.hasDuplicateConstructor(newConstructor)) { 221 | newConstructor.remove 222 | null 223 | } else { 224 | newConstructor 225 | } 226 | } 227 | 228 | /** Tests if the type is the same or extends another type */ 229 | def boolean extendsType(TypeReference type, TypeReference sameOrSuperType) { 230 | sameOrSuperType.isAssignableFrom(type) 231 | } 232 | 233 | /** Find all method type parameters being references that we could not find in the method declaration. Only use when creating extension methods. */ 234 | private def extractTypeParameterNamesFromSignature(MethodDeclaration originalMethod) { 235 | val extractedParameterTypeNames = originalMethod.parameters.map [ type.extractTypeParameterNames ].flatten 236 | val extractedReturnTypeNames = originalMethod.returnType.extractTypeParameterNames 237 | val allExtracted = extractedParameterTypeNames + extractedReturnTypeNames 238 | allExtracted.toSet 239 | } 240 | 241 | /** 242 | *
  • Promise> should result in #[T, A, B] 243 | *
  • T should result in #[T] 244 | * 245 | * Cheats, tries to identify type parameters by being uppercase only... 246 | * However, it is impossible to extract them from a class typereference. 247 | */ 248 | def Iterable extractTypeParameterNames(TypeReference type) { 249 | if(type.actualTypeArguments.empty) { 250 | if(type.simpleName == type.simpleName.toUpperCase) #[ type.simpleName ] else #[] 251 | } 252 | else type.actualTypeArguments.map [ extractTypeParameterNames ].flatten 253 | } 254 | 255 | /** 256 | * Check if the cls has a duplicate signature of the method being passed. 257 | * @param cls the class to check 258 | * @param method the method we use to check the signature from 259 | * @param includeSuperClasses if true, also include superclasses of cls for matching method signatures 260 | */ 261 | def boolean hasDuplicateMethod(TypeReference cls, MethodDeclaration method, boolean includeSuperClasses) { 262 | val clsMethods = if(includeSuperClasses) cls.allResolvedMethods else cls.declaredResolvedMethods 263 | val matchingMethods = clsMethods.filter [ declaration.hasSameSignatureAs(method) ] 264 | matchingMethods.size > 1 265 | } 266 | 267 | /** 268 | * Check if the cls has a duplicate signature of the method being passed. 269 | * @param cls the class to check 270 | * @param method the method we use to check the signature from 271 | * @param includeSuperClasses if true, also include superclasses of cls for matching method signatures 272 | */ 273 | def boolean hasDuplicateConstructor(TypeReference cls, ConstructorDeclaration constructor) { 274 | val matchingMethods = cls.declaredResolvedConstructors.filter [ declaration.hasSameSignatureAs(constructor) ] 275 | matchingMethods.size > 1 276 | } 277 | 278 | /** 279 | * Check if method1 and method2 have the same signature (name and parameter types) 280 | */ 281 | def hasSameSignatureAs(MethodDeclaration method1, MethodDeclaration method2) { 282 | if(method1.simpleName != method2.simpleName) return false 283 | if(method1.parameters.size != method2.parameters.size) return false 284 | if(method1.parameters.empty) return true 285 | for(var i = 0; i < method1.parameters.size; i++) { 286 | val p1 = method1.parameters.get(i) 287 | val p2 = method2.parameters.get(i) 288 | if(p1.type.name != p2.type.name) return false 289 | } 290 | true 291 | } 292 | 293 | /** 294 | * Check if method1 and method2 have the same signature (name and parameter types) 295 | */ 296 | def hasSameSignatureAs(ConstructorDeclaration constructor1, ConstructorDeclaration constructor2) { 297 | if(constructor1.simpleName != constructor2.simpleName) return false 298 | if(constructor1.parameters.size != constructor2.parameters.size) return false 299 | if(constructor1.parameters.empty) return true 300 | for(var i = 0; i < constructor1.parameters.size; i++) { 301 | val p1 = constructor1.parameters.get(i) 302 | val p2 = constructor2.parameters.get(i) 303 | if(p1.type.name != p2.type.name) return false 304 | } 305 | true 306 | } 307 | 308 | /** Print out the signature of a method:
    name(param-type,param-type,...)
     */
    309 | 	def String signature(MethodDeclaration method) {
    310 | 		'''«method.simpleName»(«FOR param : method.parameters SEPARATOR ','»«param.type.simpleName»«ENDFOR»)'''
    311 | 	}
    312 | 
    313 | 	/** Print out the signature of a method: 
    name(param-type,param-type,...)
     */
    314 | 	def String signature(ConstructorDeclaration constructor) {
    315 | 		'''«constructor.simpleName»(«FOR param : constructor.parameters SEPARATOR ','»«param.type.simpleName»«ENDFOR»)'''
    316 | 	}
    317 | 	
    318 | }
    319 | 
    
    
    --------------------------------------------------------------------------------
    /annotations/src/main/java/nl/kii/util/annotation/Constructor.xtend:
    --------------------------------------------------------------------------------
     1 | package nl.kii.util.annotation
     2 | 
     3 | import java.lang.annotation.Target
     4 | import nl.kii.util.annotation.processor.ConstructorProcessor
     5 | import org.eclipse.xtend.lib.macro.Active
     6 | 
     7 | /** 
     8 |  * Create a constructor based on all the non-static, uninitialised final fields in the class.
     9 |  * 

    10 | * For example, these are the same: 11 | *

    12 | *

    13 |  * class Foo {
    14 |  *    val int a
    15 |  *    val int b
    16 |  * 
    17 |  *    new(int a, int b) {
    18 |  *       this.a = a
    19 |  *       this.b = b
    20 |  *    }
    21 |  * }
    22 |  * 
    23 | *

    24 | *

    25 |  * \@Constructor
    26 |  * class Foo {
    27 |  *    val int a
    28 |  *    val int b
    29 |  * }
    30 |  * 
    31 | * 32 | */ 33 | @Target(TYPE) 34 | @Active(ConstructorProcessor) 35 | annotation Constructor { 36 | 37 | } 38 | -------------------------------------------------------------------------------- /annotations/src/main/java/nl/kii/util/annotation/CopyMethods.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util.annotation 2 | 3 | import java.lang.annotation.Repeatable 4 | import java.lang.annotation.Target 5 | import nl.kii.util.annotation.processor.CopyMethodsProcessor 6 | import org.eclipse.xtend.lib.macro.Active 7 | 8 | /** 9 | * Create a wrapper extension class that converts methods with async handlers and handlers to promises. 10 | */ 11 | @Target(TYPE) 12 | @Repeatable(CopyStaticMethodsValues) 13 | @Active(CopyMethodsProcessor) 14 | annotation CopyMethods { 15 | 16 | /** The class to copy methods from */ 17 | Class value 18 | 19 | /** Do we also pick up methods from supertypes? */ 20 | boolean includeSuperTypes = false 21 | 22 | /** Do we create extension methods for instance methods? */ 23 | boolean createExtensionMethods = true 24 | 25 | /** 26 | * Method signatures to ignore. 27 | * Use the format "[methodname]([fulltypename],...)" (from MethodSignature.toString()) 28 | * For example: "equals(java.lang.Object)" 29 | */ 30 | String[] ignoredMethods = #[] 31 | } 32 | 33 | @Target(TYPE) 34 | annotation CopyStaticMethodsValues { 35 | 36 | CopyMethods[] value 37 | 38 | } 39 | -------------------------------------------------------------------------------- /annotations/src/main/java/nl/kii/util/annotation/NamedParams.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util.annotation 2 | 3 | import java.lang.annotation.Target 4 | import nl.kii.util.annotation.processor.NamedParamsProcessor 5 | import org.eclipse.xtend.lib.macro.Active 6 | import org.eclipse.xtext.xbase.lib.Procedures.Procedure1 7 | 8 | @Active(NamedParamsProcessor) 9 | @Target(METHOD, CONSTRUCTOR) 10 | annotation NamedParams { 11 | } 12 | 13 | /** 14 | * Pin a named parameter in the method. This means a parameter will not become settable with the closure, 15 | * and instead will stay part of the method signature. 16 | */ 17 | @Target(PARAMETER) 18 | annotation Locked { 19 | } 20 | 21 | /** 22 | * Indicate that this parameter may be null. If you leave a parameter empty when it may not be null, 23 | * calling the method will throw a nullpointer exception from the start. 24 | */ 25 | @Target(PARAMETER) 26 | annotation Nullable { 27 | } 28 | 29 | /** 30 | * Set the default string value of a named parameter. 31 | * For number values, use DefaultValue instead. 32 | */ 33 | @Target(PARAMETER) 34 | annotation Default { 35 | String value 36 | } 37 | 38 | /** Set the default number value of a named parameter. */ 39 | @Target(PARAMETER) 40 | annotation DefaultValue { 41 | double value 42 | } 43 | 44 | /** Set the default value of a named parameter to true */ 45 | @Target(PARAMETER) 46 | annotation DefaultTrue { 47 | } 48 | 49 | /** Set the default value of a named parameter to false */ 50 | @Target(PARAMETER) 51 | annotation DefaultFalse { 52 | } 53 | 54 | /** 55 | * This interface gets used and implemented by the NamedParams active annotation, so set up parameters 56 | * and implement the validation of the passed parameters. 57 | */ 58 | interface MethodParameters { 59 | 60 | def void validate() throws NullPointerException 61 | 62 | } 63 | 64 | abstract class MethodParameterSetter implements Procedure1 { 65 | 66 | def T call(T parameters) { 67 | apply(parameters) 68 | parameters.validate 69 | parameters 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /annotations/src/main/java/nl/kii/util/annotation/processor/ConstructorProcessor.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util.annotation.processor 2 | 3 | import nl.kii.util.annotation.ActiveAnnotationTools 4 | import org.eclipse.xtend.lib.macro.AbstractClassProcessor 5 | import org.eclipse.xtend.lib.macro.TransformationContext 6 | import org.eclipse.xtend.lib.macro.declaration.MutableClassDeclaration 7 | 8 | class ConstructorProcessor extends AbstractClassProcessor { 9 | 10 | override doTransform(MutableClassDeclaration cls, extension TransformationContext context) { 11 | val extension tools = new ActiveAnnotationTools(context) 12 | 13 | // find all fields that are final, not static and not already initialised 14 | val finalFields = cls.declaredFields.filter[ !static && final && initializer === null ] 15 | 16 | // create a constructor with these fields as parameters that sets these fields 17 | cls.addConstructorSafely [ 18 | for(field : finalFields) { 19 | addParameter(field.simpleName, field.type) 20 | } 21 | body = ''' 22 | «FOR field : finalFields» 23 | this.«field.simpleName» = «field.simpleName»; 24 | «ENDFOR» 25 | ''' 26 | ] 27 | 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /annotations/src/main/java/nl/kii/util/annotation/processor/CopyMethodsProcessor.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util.annotation.processor 2 | 3 | import nl.kii.util.annotation.ActiveAnnotationTools 4 | import nl.kii.util.annotation.CopyMethods 5 | import org.eclipse.xtend.lib.macro.AbstractClassProcessor 6 | import org.eclipse.xtend.lib.macro.TransformationContext 7 | import org.eclipse.xtend.lib.macro.declaration.AnnotationReference 8 | import org.eclipse.xtend.lib.macro.declaration.MethodDeclaration 9 | import org.eclipse.xtend.lib.macro.declaration.MutableClassDeclaration 10 | import org.eclipse.xtend.lib.macro.declaration.TypeReference 11 | 12 | class CopyMethodsProcessor extends AbstractClassProcessor { 13 | 14 | /** The annotation to search for */ 15 | def Class getAnnotationType() { CopyMethods } 16 | 17 | override doTransform(MutableClassDeclaration cls, extension TransformationContext context) { 18 | val extension tools = new ActiveAnnotationTools(context) 19 | val annotations = cls.annotations.filter [ annotationTypeDeclaration.qualifiedName == annotationType.name ] 20 | for(annotation : annotations) { 21 | val ignoredMethods = annotation.getStringArrayValue('ignoredMethods') 22 | val includeSuperTypes = annotation.getBooleanValue('includeSuperTypes') 23 | val createExtensionMethods = annotation.getBooleanValue('createExtensionMethods') 24 | 25 | val sourceCls = annotation.getClassValue('value') 26 | val sourceMethods = if(includeSuperTypes) sourceCls.allResolvedMethods else sourceCls.declaredResolvedMethods 27 | 28 | val copyMethods = sourceMethods 29 | .map [ declaration ] 30 | .filter [ !returnType.inferred ] 31 | .filter [ static || createExtensionMethods ] 32 | .filter [ method | !ignoredMethods.contains(method.signature) ] 33 | 34 | for(method : copyMethods) { 35 | doCopyMethod(cls, sourceCls, method, annotation, context) 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * Perform the copy operation 42 | * @param targetClass the class to copy to 43 | * @param originalCls the class to copy from 44 | * @param originalMethod the method to copy 45 | * @param annotation the annotation with settings for copying 46 | * @param context the transformation context 47 | */ 48 | def doCopyMethod(MutableClassDeclaration targetClass, TypeReference originalCls, MethodDeclaration originalMethod, AnnotationReference annotation, extension TransformationContext context) { 49 | val extension tools = new ActiveAnnotationTools(context) 50 | targetClass.addMethodCopy(originalCls, originalMethod, null, true, true) [ extension newMethod, allTypeParameters | 51 | if(originalMethod.static) { 52 | body = [''' 53 | «IF !newMethod.returnType.isVoid»return «ENDIF» 54 | «originalCls.name».«newMethod.simpleName»( 55 | «FOR parameter : originalMethod.parameters SEPARATOR ', '» 56 | «parameter.simpleName» 57 | «ENDFOR» 58 | ); 59 | '''] 60 | } else { 61 | body = [''' 62 | «IF !newMethod.returnType.isVoid»return «ENDIF» 63 | «newMethod.parameters.head.simpleName».«newMethod.simpleName»( 64 | «FOR parameter : originalMethod.parameters SEPARATOR ', '» 65 | «parameter.simpleName» 66 | «ENDFOR» 67 | ); 68 | '''] 69 | } 70 | ] 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /annotations/src/main/java/nl/kii/util/annotation/processor/NamedParamsProcessor.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util.annotation.processor 2 | 3 | import java.util.List 4 | import nl.kii.util.annotation.ActiveAnnotationTools 5 | import nl.kii.util.annotation.Default 6 | import nl.kii.util.annotation.DefaultFalse 7 | import nl.kii.util.annotation.DefaultTrue 8 | import nl.kii.util.annotation.DefaultValue 9 | import nl.kii.util.annotation.Locked 10 | import nl.kii.util.annotation.MethodParameterSetter 11 | import nl.kii.util.annotation.MethodParameters 12 | import nl.kii.util.annotation.Nullable 13 | import org.eclipse.xtend.lib.macro.RegisterGlobalsContext 14 | import org.eclipse.xtend.lib.macro.RegisterGlobalsParticipant 15 | import org.eclipse.xtend.lib.macro.TransformationContext 16 | import org.eclipse.xtend.lib.macro.TransformationParticipant 17 | import org.eclipse.xtend.lib.macro.declaration.ConstructorDeclaration 18 | import org.eclipse.xtend.lib.macro.declaration.ExecutableDeclaration 19 | import org.eclipse.xtend.lib.macro.declaration.MethodDeclaration 20 | import org.eclipse.xtend.lib.macro.declaration.MutableConstructorDeclaration 21 | import org.eclipse.xtend.lib.macro.declaration.MutableExecutableDeclaration 22 | import org.eclipse.xtend.lib.macro.declaration.MutableMethodDeclaration 23 | import org.eclipse.xtend.lib.macro.declaration.MutableNamedElement 24 | import org.eclipse.xtend.lib.macro.declaration.NamedElement 25 | import org.eclipse.xtend.lib.macro.declaration.ParameterDeclaration 26 | import org.eclipse.xtext.xbase.lib.Procedures.Procedure1 27 | 28 | import static org.eclipse.xtend.lib.macro.declaration.Visibility.* 29 | 30 | class NamedParamsProcessor implements RegisterGlobalsParticipant, TransformationParticipant { 31 | 32 | override doRegisterGlobals(List elements, RegisterGlobalsContext context) { 33 | for (NamedElement element: elements) { 34 | switch element { 35 | MethodDeclaration: doRegisterGlobals(element, context) 36 | ConstructorDeclaration: doRegisterGlobals(element, context) 37 | } 38 | } 39 | } 40 | 41 | override doTransform(List elements, extension TransformationContext context) { 42 | for (MutableNamedElement element: elements) { 43 | switch element { 44 | MutableMethodDeclaration: doTransform(element, context) 45 | MutableConstructorDeclaration: doTransform(element, context) 46 | } 47 | } 48 | } 49 | 50 | def doRegisterGlobals(ExecutableDeclaration method, extension RegisterGlobalsContext context) { 51 | // register our new parameters object class for the method 52 | registerClass(method.compilationUnit.packageName + '.' + method.parametersObjectName) 53 | } 54 | 55 | def doTransform(MutableExecutableDeclaration member, extension TransformationContext context) { 56 | val extension tools = new ActiveAnnotationTools(context) 57 | 58 | // method generics are not allowed, since Xtend 2.9.X does not expose that information yet. 59 | if(!member.typeParameters.empty) member.addError('@NamedParam does not (yet) support generic type parameters for methods.') 60 | val allParameterTypes = member.parameters.map[type.actualTypeArguments].flatten 61 | if(!allParameterTypes.empty) member.addError('@NamedParam does not (yet) support generic type parameters in methods.') 62 | 63 | // create the params class 64 | val paramsClazz = findClass(member.compilationUnit.packageName + '.' + member.parametersObjectName) => [ 65 | implementedInterfaces = #[MethodParameters.ref] 66 | ] 67 | // as fields, use all unlocked parameters of the method 68 | val validationCode = new StringBuffer 69 | // as fields, use all unlocked parameters of the method 70 | for(parameter : member.parameters.filter [ !isLocked(context) ]) { 71 | // for each parameter in the method, create a field in the params class 72 | paramsClazz.addField(parameter.simpleName) [ 73 | type = parameter.type 74 | visibility = PUBLIC 75 | // set the initializer based on default annotations 76 | switch type { 77 | case string: { 78 | val annotation = parameter.findAnnotation(Default.ref.type) 79 | val value = if(annotation !== null) annotation.getStringValue('value') 80 | if(value !== null) initializer = '''"«value»"''' 81 | } 82 | case int.ref, case Integer.ref: { 83 | val annotation = parameter.findAnnotation(DefaultValue.ref.type) 84 | val value = if(annotation !== null) annotation.getDoubleValue('value') 85 | initializer = '''new Double(«value»).intValue()''' 86 | } 87 | case long.ref, case Long.ref: { 88 | val annotation = parameter.findAnnotation(DefaultValue.ref.type) 89 | val value = if(annotation !== null) annotation.getDoubleValue('value') 90 | initializer = '''new Double(«value»).longValue()''' 91 | } 92 | case double.ref, case Double.ref: { 93 | val annotation = parameter.findAnnotation(DefaultValue.ref.type) 94 | val value = if(annotation !== null) annotation.getDoubleValue('value') 95 | initializer = '''«value»''' 96 | } 97 | case boolean.ref, case Boolean.ref: { 98 | val trueAnnotation = parameter.findAnnotation(DefaultTrue.ref.type) 99 | val falseAnnotation = parameter.findAnnotation(DefaultFalse.ref.type) 100 | if(trueAnnotation !== null) { 101 | initializer = '''true''' 102 | } else if(falseAnnotation !== null) { 103 | initializer = '''false''' 104 | } 105 | } 106 | } 107 | // add the field to the validation code 108 | if(!parameter.nullAllowed(context)) { 109 | validationCode.append(''' 110 | if(«parameter.simpleName» == null) 111 | throw new NullPointerException("«member.simpleName».«parameter.simpleName» may not be null. (annotate with @NULL if you want to allow null)"); 112 | ''') 113 | } 114 | ] 115 | } 116 | paramsClazz.addMethod('validate') [ 117 | body = '''«validationCode»''' 118 | ] 119 | 120 | // create a new method that takes just the parameters object 121 | val clazz = member.declaringType 122 | val Procedure1 addParametersMethod = [ 123 | primarySourceElement = member 124 | copyAllMethodProperties(member, it) 125 | // add the locked parameters 126 | for(param : member.parameters.filter [isLocked(context)]) { 127 | addParameter(param.simpleName, param.type) 128 | } 129 | // add the parameters paramer 130 | val paramsType = paramsClazz.newTypeReference 131 | addParameter('parameters', paramsType) 132 | // create the body which creates the params object, calls the closure with the params and then calls the original method 133 | body = switch member { 134 | MutableMethodDeclaration: ''' 135 | «IF member.returnType.simpleName != 'void'»return «ENDIF» 136 | «member.simpleName»( 137 | «FOR param : member.parameters SEPARATOR ', '» 138 | «IF !param.isLocked(context)»parameters.«ENDIF»«param.simpleName» 139 | «ENDFOR» 140 | ); 141 | ''' 142 | MutableConstructorDeclaration: ''' 143 | this( 144 | «FOR param : member.parameters SEPARATOR ', '» 145 | «IF !param.isLocked(context)»parameters.«ENDIF»«param.simpleName» 146 | «ENDFOR» 147 | ); 148 | ''' 149 | } 150 | ] 151 | 152 | // create the new method that calls the parameters object 153 | val Procedure1 addParametersFunctionMethod = [ 154 | primarySourceElement = member 155 | copyAllMethodProperties(member, it) 156 | // add the locked parameters 157 | for(param : member.parameters.filter [isLocked(context)]) { 158 | addParameter(param.simpleName, param.type) 159 | } 160 | // add the closure parameter 161 | val paramsType = paramsClazz.newTypeReference 162 | addParameter('parametersFn', MethodParameterSetter.ref(paramsType)) 163 | // create the body which creates the params object, calls the closure with the params and then calls the original method 164 | body = switch member { 165 | MutableMethodDeclaration: ''' 166 | «FOR param : member.parameters.filter [!nullAllowed(context) && isLocked(context)]» 167 | if(«param.simpleName» == null) throw new NullPointerException("«member.simpleName».«param.simpleName» may not be null. (annotate with @Nullable if you want to allow null)"); 168 | «ENDFOR» 169 | «IF member.returnType.simpleName != 'void'»return «ENDIF» 170 | «member.simpleName»( 171 | «FOR param : member.parameters.filter[isLocked(context)]» 172 | «param.simpleName», 173 | «ENDFOR» 174 | parametersFn.call(new «paramsType»()) 175 | ); 176 | ''' 177 | MutableConstructorDeclaration: ''' 178 | this( 179 | «FOR param : member.parameters.filter[isLocked(context)]» 180 | «param.simpleName», 181 | «ENDFOR» 182 | parametersFn.call(new «paramsType»()) 183 | ); ''' 184 | } 185 | ] 186 | 187 | switch member { 188 | MutableMethodDeclaration: { 189 | clazz.addMethod(member.simpleName) [ addParametersMethod.apply(it) ] 190 | clazz.addMethod(member.simpleName) [ addParametersFunctionMethod.apply(it) ] 191 | } 192 | MutableConstructorDeclaration: { 193 | clazz.addConstructor [ addParametersMethod.apply(it) ] 194 | clazz.addConstructor [ addParametersFunctionMethod.apply(it) ] 195 | } 196 | } 197 | } 198 | 199 | def static copyAllMethodProperties(MutableExecutableDeclaration member, MutableExecutableDeclaration it) { 200 | // copy all method properties 201 | deprecated = member.deprecated 202 | docComment = member.docComment 203 | exceptions = member.exceptions 204 | varArgs = member.varArgs 205 | visibility = member.visibility 206 | // these are only for methods, not for constructors 207 | if(member instanceof MutableMethodDeclaration) { 208 | if(it instanceof MutableMethodDeclaration) { 209 | abstract = member.abstract 210 | ^default = member.^default 211 | final = member.final 212 | native = member.native 213 | returnType = member.returnType 214 | static = member.static 215 | strictFloatingPoint = member.strictFloatingPoint 216 | synchronized = member.synchronized 217 | } 218 | } 219 | } 220 | 221 | def static getParametersObjectName(ExecutableDeclaration method) { 222 | // method.declaringType.simpleName + '_' + method.simpleName.toFirstUpper + 'Params' 223 | method.declaringType.simpleName + '.' + method.simpleName.toFirstUpper + 'Params' 224 | } 225 | 226 | def static nullAllowed(ParameterDeclaration param, extension TransformationContext context) { 227 | param.type.primitive || param.findAnnotation(Nullable.newTypeReference.type) !== null 228 | } 229 | 230 | def static isLocked(ParameterDeclaration param, extension TransformationContext context) { 231 | param.findAnnotation(Locked.newTypeReference.type) !== null 232 | } 233 | 234 | } 235 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // SETUP BUILDSCRIPT /////////////////////////////////////////////////////////// 2 | buildscript { 3 | repositories { 4 | jcenter() 5 | maven { 6 | url 'https://plugins.gradle.org/m2/' 7 | } 8 | } 9 | 10 | dependencies { 11 | classpath 'org.xtext:xtext-gradle-plugin:1.0.19' 12 | classpath 'com.netflix.nebula:gradle-extra-configurations-plugin:3.0.3' 13 | } 14 | } 15 | 16 | task wrapper(type: Wrapper) { 17 | gradleVersion = '4.0' 18 | } 19 | 20 | // CONFIGURE PROJECTS /////////////////////////////////////////////////// 21 | subprojects { 22 | apply plugin: 'eclipse' 23 | apply plugin: 'java' 24 | apply plugin: 'org.xtext.xtend' 25 | apply plugin: 'maven-publish' 26 | apply plugin: 'jacoco' 27 | 28 | sourceCompatibility = javaVersion 29 | targetCompatibility = javaVersion 30 | 31 | eclipse.project.name = rootProject.name + '-' + project.name 32 | 33 | repositories { 34 | mavenLocal() 35 | mavenCentral() 36 | } 37 | 38 | dependencies { 39 | compile "org.eclipse.xtend:org.eclipse.xtend.lib:${xtendVersion}" 40 | compile "org.slf4j:slf4j-api:${slf4jVersion}" 41 | testCompile "ch.qos.logback:logback-classic:${logbackVersion}" 42 | testCompile "junit:junit:${junitVersion}" 43 | } 44 | 45 | task sourcesJar( type: Jar, dependsOn: classes ) { 46 | classifier = 'sources' 47 | from sourceSets.main.allSource 48 | } 49 | 50 | task javadocJar( type: Jar, dependsOn: javadoc ) { 51 | classifier = 'javadoc' 52 | from javadoc.destinationDir 53 | } 54 | 55 | tasks.withType( Javadoc ) { 56 | if ( JavaVersion.current().isJava8Compatible() ) { 57 | options.addStringOption( 'Xdoclint:none', '-quiet' ) 58 | } 59 | } 60 | 61 | task install( dependsOn: publishToMavenLocal ) 62 | 63 | jacoco { 64 | toolVersion = '0.7.6.201602180812' 65 | } 66 | 67 | jacocoTestReport { 68 | group = "Reporting" 69 | description = "Generate Jacoco coverage reports after running tests." 70 | reports.html.enabled true 71 | } 72 | 73 | publishing { 74 | publications { 75 | mavenJava( MavenPublication ) { 76 | artifactId rootProject.name + '-' + project.name 77 | from components.java 78 | artifact sourcesJar 79 | artifact javadocJar 80 | pom.withXml { 81 | asNode().dependencies.'*'.each() { 82 | it.scope*.value = 'compile' 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | 90 | project( 'annotations' ) { 91 | dependencies { 92 | } 93 | } 94 | 95 | project( 'annotations-test' ) { 96 | dependencies { 97 | compile project(":annotations") 98 | } 99 | } 100 | 101 | project( 'core' ) { 102 | dependencies { 103 | compile project(":annotations") 104 | } 105 | } 106 | 107 | project( 'test' ) { 108 | dependencies { 109 | compile project( ':core' ) 110 | compile "junit:junit:${junitVersion}" 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/AssertionException.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | /** 4 | * Lets you place assertion points in your code, throw this to indicate a code assertion failed. 5 | * Unlike AssertionError, this should be caught. 6 | */ 7 | class AssertionException extends Exception { 8 | 9 | new(String message) { 10 | super(message) 11 | } 12 | 13 | new(String message, Throwable cause) { 14 | super(message, cause) 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/Cached.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import java.util.Date 4 | import org.eclipse.xtext.xbase.lib.Functions.Function0 5 | 6 | import static extension nl.kii.util.DateExtensions.* 7 | 8 | /** A simple lightweight way to cache a single value */ 9 | class Cached implements Function0 { 10 | 11 | val Period retainTime 12 | val =>T fetchFn 13 | 14 | var T data 15 | var Date lastFetched 16 | 17 | new(Period retainTime, =>T fetchFn) { 18 | this.retainTime = retainTime 19 | this.fetchFn = fetchFn 20 | } 21 | 22 | def get() { apply } 23 | 24 | override T apply() { 25 | if(data === null || lastFetched === null || now - lastFetched > retainTime) { 26 | data = fetchFn.apply 27 | lastFetched = now 28 | data 29 | } else data 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/CloseableExtensions.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import java.io.Closeable 4 | 5 | import static extension nl.kii.util.OptExtensions.* 6 | 7 | class CloseableExtensions { 8 | 9 | // CLOSING 10 | 11 | /** Perform an operation on a closable, and close it when finished */ 12 | def static using(I closable, (I)=>void fn) { 13 | try { 14 | fn.apply(closable) 15 | } finally { 16 | closable.close 17 | } 18 | } 19 | 20 | /** Perform an operation on a closable, and close it when finished */ 21 | def static using(I closable, (I)=>T fn) { 22 | try { 23 | fn.apply(closable) 24 | } finally { 25 | closable.close 26 | } 27 | } 28 | 29 | def static Opt attemptUsing(I closable, (I)=>T fn) { 30 | try { 31 | using(closable, fn).option 32 | } catch(Throwable e) { 33 | err(e) 34 | } 35 | } 36 | 37 | def static using(I closeable, (I)=>T fn) { 38 | try { 39 | fn.apply(closeable) 40 | } finally { 41 | closeable?.close 42 | } 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/DateExtensions.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import java.text.SimpleDateFormat 4 | import java.util.Calendar 5 | import java.util.Date 6 | 7 | import static java.util.Calendar.* 8 | import static extension java.lang.Math.* 9 | import static extension java.util.TimeZone.* 10 | import java.util.concurrent.TimeUnit 11 | 12 | final class DateExtensions { 13 | 14 | val static standardDateFormat = "yyyy-MM-dd'T'HH:mm:ss" 15 | 16 | /** The current date */ 17 | def static now() { new Date } 18 | 19 | /** Create a date for a specific calendar moment */ 20 | def static moment(int year, int month, int day) { 21 | moment(year, month, day, 0, 0, 0, 0) 22 | } 23 | 24 | /** Create a date for a specific calendar moment */ 25 | def static moment(int year, int month, int day, int hour, int min, int sec) { 26 | moment(year, month, day, hour, min, sec, 0) 27 | } 28 | 29 | /** Create a date for a specific calendar moment */ 30 | def static moment(int year, int month, int day, int hour, int min, int sec, int ms) { 31 | new Date().withCalendar [ 32 | set(YEAR, year) 33 | set(MONTH, month) 34 | set(DAY_OF_MONTH, day) 35 | set(HOUR_OF_DAY, hour) 36 | set(MINUTE, min) 37 | set(SECOND, sec) 38 | set(MILLISECOND, ms) 39 | ] 40 | } 41 | 42 | // DATE CONVERSIONS /////////////////////////////////////////////////// 43 | 44 | /** Convert a date to UTC timezone */ 45 | def static toUTC(Date date) { 46 | val cal = date.toCalendar 47 | date - new Period(cal.get(DST_OFFSET) + cal.get(ZONE_OFFSET) - cal.get(DST_OFFSET)) 48 | } 49 | 50 | /** Convert a date to a specific timezone */ 51 | def static toTimeZone(Date date, String zone) { 52 | date.toUTC + new Period(zone.timeZone.rawOffset) 53 | } 54 | 55 | /** Convert a date to a calendar instance */ 56 | def static toCalendar(Date date) { 57 | Calendar.instance => [ time = date ] 58 | } 59 | 60 | /** Convert a time amount and unit to a period */ 61 | def static toPeriod(long amount, TimeUnit timeunit) { 62 | new Period(timeunit.toMillis(amount)) 63 | } 64 | 65 | // DATE GETTERS /////////////////////////////////////////////////////// 66 | 67 | @Deprecated 68 | def static getCurrentMinute(Date date) { date.mins } 69 | @Deprecated 70 | def static getCurrentSecond(Date date) { date.secs } 71 | @Deprecated 72 | def static getHourOfDay(Date date) { date.hours24 } 73 | 74 | def static int getMs(Date date) { date.toCalendar.get(MILLISECOND) } 75 | def static int getSecs(Date date) { date.toCalendar.get(SECOND) } 76 | def static int getMins(Date date) { date.toCalendar.get(MINUTE) } 77 | def static int getHours24(Date date) { date.toCalendar.get(HOUR_OF_DAY) } 78 | def static int getDays(Date date) { date.toCalendar.get(DAY_OF_MONTH) } 79 | def static int getMonths(Date date) { date.toCalendar.get(MONTH) } 80 | def static int getYearAD(Date date) { date.toCalendar.get(YEAR) } 81 | 82 | // DATE MANIPULATION /////////////////////////////////////////////////// 83 | 84 | /** Update a date using a calendar object. See the moment(...) methods for example usage. */ 85 | def static Date withCalendar(Date date, (Calendar)=>void updateFn) { 86 | val calendar = date.toCalendar 87 | updateFn.apply(calendar) 88 | return calendar.time 89 | } 90 | 91 | // DATE FORMATTING //////////////////////////////////////////////////// 92 | 93 | /** quickly format a date to the standard "yyyy-MM-dd'T'HH:mm:ss" format. */ 94 | def static format(Date date) { date.format(standardDateFormat) } 95 | 96 | /** 97 | * quickly format a date to a specified format. see all formatting options at 98 | * http://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html 99 | */ 100 | def static format(Date date, String format) { new SimpleDateFormat(format).format(date) } 101 | 102 | // DATE COMPARISONS /////////////////////////////////////////////////// 103 | 104 | /** Return the most recent date */ 105 | def static newest(Date... dates) { dates.filter[it!==null].sortBy[time].reverse.head } 106 | 107 | /** Return the oldest date */ 108 | def static oldest(Date... dates) { dates.filter[it!==null].sortBy[time].head } 109 | 110 | /** Return the date nearest to the specified {@code target}, or {@code null} if the iterable is empty. */ 111 | def static nearest(Iterable dates, Date target) { 112 | dates.filterNull.reduce [ leading, contestant | 113 | if (interval(target, contestant) < interval(target, leading)) contestant 114 | else leading 115 | ] 116 | } 117 | 118 | /** the date is more than [period] old */ 119 | def static > (Date date, Period period) { now - date > period } 120 | 121 | /** the date is more or equals than [period] old */ 122 | def static >= (Date date, Period period) { now - date >= period } 123 | 124 | /** the date is less than [period] old */ 125 | def static < (Date date, Period period) { now - date < period } 126 | 127 | /** the date is less or equals than [period] old */ 128 | def static <= (Date date, Period period) { now - date <= period } 129 | 130 | /** Difference between dates, largest first */ 131 | def static - (Date d1, Date d2) { new Period(d1.time - d2.time) } 132 | 133 | /** Absolute difference between two dates. */ 134 | def static interval(Date d1, Date d2) { new Period((d1.time - d2.time).abs) } 135 | 136 | // PERIOD CREATION //////////////////////////////////////////////////// 137 | 138 | def static ms(long value) { new MilliSeconds(value) } 139 | def static sec(long value) { new Seconds(value) } 140 | def static secs(long value) { new Seconds(value) } 141 | def static min(long value) { new Minutes(value) } 142 | def static mins(long value) { new Minutes(value) } 143 | def static hour(long value) { new Hours(value) } 144 | def static hours(long value) { new Hours(value) } 145 | def static day(long value) { new Days(value) } 146 | def static days(long value) { new Days(value) } 147 | def static year(long value) { new Years(value) } 148 | def static years(long value) { new Years(value) } 149 | 150 | // PERIOD MANIPULATION //////////////////////////////////////////////// 151 | 152 | def static + (Period p1, Period p2) { new Period(p1.time + p2.time) } 153 | def static - (Period p1, Period p2) { new Period(p1.time - p2.time) } 154 | def static / (Period p1, Period p2) { p1.time.doubleValue / p2.time.doubleValue } 155 | def static / (Period p1, int n) { new Period(p1.time / n) } 156 | def static * (int n, Period p1) { new Period(n * p1.time) } 157 | def static * (Period p1, int n) { n * p1 } 158 | def static abs(Period p) { new Period(p.time.abs) } 159 | 160 | // PERIOD COMPARISON ////////////////////////////////////////////////// 161 | 162 | def static > (Period p1, Period p2) { p1.time > p2.time } 163 | def static >= (Period p1, Period p2) { p1.time >= p2.time } 164 | def static < (Period p1, Period p2) { p1.time < p2.time } 165 | def static <= (Period p1, Period p2) { p1.time <= p2.time } 166 | def static min(Period p1, Period p2) { new Period(min(p1.time, p2.time)) } 167 | def static max(Period p1, Period p2) { new Period(max(p1.time, p2.time)) } 168 | 169 | // DATE CHANGING USING PERIODS //////////////////////////////////////// 170 | 171 | def static add(Date date, Period p) { new Date(date.time + p.time) } 172 | def static subtract(Date date, Period p) { new Date(date.time - p.time) } 173 | def static + (Date date, Period p) { add(date, p) } 174 | def static - (Date date, Period p) { subtract(date, p) } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/FunctionExtensions.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import org.eclipse.xtext.xbase.lib.Functions.Function0 4 | import org.eclipse.xtext.xbase.lib.Functions.Function1 5 | import org.eclipse.xtext.xbase.lib.Functions.Function2 6 | import org.eclipse.xtext.xbase.lib.Procedures.Procedure1 7 | 8 | class FunctionExtensions { 9 | 10 | // OPERATOR OVERLOADING /////////////////////////////////////////////////// 11 | /** Negate the result of a function */ 12 | def static Function0 !(Function0 fn) { 13 | [|!fn.apply] 14 | } 15 | 16 | /** Negate the result of a function */ 17 | def static Function1 !(Function1 fn) { 18 | [!fn.apply(it)] 19 | } 20 | 21 | /** Negate the result of a function */ 22 | def static Function2 !(Function2 fn) { 23 | [a, b|!fn.apply(a, b)] 24 | } 25 | 26 | /** Let the << operator also work on Procedure1.apply */ 27 | def static <<(Procedure1 fn, Param p) { 28 | fn.apply(p) 29 | } 30 | 31 | /** Let the << operator also work on Function1.apply */ 32 | def static T <<(Function1 fn, Param p) { 33 | fn.apply(p) 34 | } 35 | 36 | /** Let the >> operator also work on Procedure1.apply */ 37 | def static >>(Param p, (Param)=>void fn) { 38 | fn.apply(p) 39 | } 40 | 41 | /** Let the >> operator also work on Function1.apply */ 42 | def static T >>(Param p, (Param)=>T fn) { 43 | fn.apply(p) 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/IterableExtensions.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import com.google.common.collect.ImmutableList 4 | import com.google.common.collect.ImmutableSet 5 | import java.util.HashMap 6 | import java.util.HashSet 7 | import java.util.Iterator 8 | import java.util.List 9 | import java.util.Map 10 | import java.util.NoSuchElementException 11 | import java.util.Random 12 | import java.util.Set 13 | 14 | import static extension nl.kii.util.OptExtensions.* 15 | 16 | class IterableExtensions { 17 | 18 | // CREATING (immutable) /////////////////////////////////////////////////// 19 | 20 | /** Create an immutable list of the given type */ 21 | def static List list(Class cls) { 22 | newImmutableList 23 | } 24 | 25 | /** Create an immutable list for the given objects */ 26 | def static List list(T... objects) { 27 | newImmutableList(objects) 28 | } 29 | 30 | // REFLECTION ///////////////////////////////////////////////////////////// 31 | 32 | /** 33 | * Returns if a list is of a certain value type by looking at the first value. 34 | * @return true if the list is not empty and the type matches 35 | *
     36 | 	 * #[1, 2, 3].isListOf(Integer) // true
     37 | 	 * #[1, 2, 3].isListOf(String) // false
     38 | 	 * #[].isListOf(Integer) // false
     39 | 	 */
     40 | 	def static boolean isListOf(List list, Class type) {
     41 | 		if(list.empty) return false
     42 | 		list.get(0).class.isAssignableFrom(type)
     43 | 	}
     44 | 
     45 | 	/** 
     46 | 	 * Returns if a map is of a certain key and value type by looking at the first value. 
     47 | 	 * @return true if the map is not empty and the types match
     48 | 	 * 
     49 | 	 * #{1->'A', 5->'C'}.isMapOf(Integer, String) // true
     50 | 	 * #{1->'A', 5->'C'}.isMapOf(Integer, Integer) // false
     51 | 	 * #{}.isMapOf(Integer, String) // false
     52 | 	 */
     53 | 	def static boolean isMapOf(Map map, Class keyType, Class valueType) {
     54 | 		if(map.empty) return false
     55 | 		val key0 = map.keySet.get(0)
     56 | 		key0.class.isAssignableFrom(keyType) && map.get(key0).class.isAssignableFrom(valueType)
     57 | 	}
     58 | 
     59 | 	// CONVERTING (immutable) /////////////////////////////////////////////////
     60 | 	
     61 | 	/** Always returns an immutable list, even if a null result is passed. handy when chaining, eliminates null checks
     62 | 	 * 
    example: getUsers.filter[age>20].list
    63 | */ 64 | def static List toList(Iterable iterable) { 65 | if(!iterable.defined) newImmutableList else iterable.iterator.toList.immutableCopy 66 | } 67 | 68 | /** Always returns an immutable set, even if a null result was passed. handy when chaining, eliminates null checks. 69 | * note: double values will be removed! 70 | */ 71 | def static Set toSet(Iterable iterable) { 72 | if (!iterable.defined) 73 | newHashSet 74 | else { 75 | val uniques = iterable.distinct.toList 76 | new HashSet(uniques).immutableCopy 77 | } 78 | } 79 | 80 | // ADDING (immutable) ///////////////////////////////////////////////////// 81 | 82 | def static concat(Iterable list, Iterable values) { 83 | if(values !== null) ImmutableList.builder.addAll(list).addAll(values).build 84 | else throw new NullPointerException('concat: passed values are null') 85 | } 86 | 87 | def static concat(T value, Iterable list) { 88 | if(list !== null) ImmutableList.builder.add(value).addAll(list).build 89 | else throw new NullPointerException('concat: passed list is null') 90 | } 91 | 92 | def static concat(Iterable list, T value) { 93 | if(value !== null) ImmutableList.builder.addAll(list).add(value).build 94 | else throw new NullPointerException('concat: passed value is null') 95 | } 96 | 97 | def static concat(Set set, T value) { 98 | if(value !== null) ImmutableSet.builder.addAll(set).add(value).build 99 | else throw new NullPointerException('concat: passed value is null') 100 | } 101 | 102 | def static concat(T value, Set set) { 103 | if(set !== null) ImmutableSet.builder.add(value).addAll(set).build 104 | else throw new NullPointerException('concat: passed set is null') 105 | } 106 | 107 | // REMOVING (immutable) /////////////////////////////////////////////////// 108 | 109 | def static uncat(List list, T value) { 110 | val filtered = list.filter[it != value] 111 | ImmutableList.builder.addAll(filtered).build 112 | } 113 | 114 | def static uncat(List list, List list2) { 115 | val filtered = list.filter[!list2.contains(it)] 116 | ImmutableList.builder.addAll(filtered).build 117 | } 118 | 119 | def static removeLast(Iterable items) { 120 | items.take(items.size - 1) 121 | } 122 | 123 | def static removeLast(Iterable items, int amount) { 124 | items.take(items.size - amount) 125 | } 126 | 127 | // SIDEEFFECTS //////////////////////////////////////////////////////////// 128 | 129 | def static effect(Iterable iterable, (T)=>void fn) { 130 | iterable.forEach(fn) 131 | iterable 132 | } 133 | 134 | def static effect(Iterable iterable, (T, int)=>void fn) { 135 | iterable.forEach(fn) 136 | iterable 137 | } 138 | 139 | /** Triggers the passed or function for each none in the list, 140 | * handy for tracking empty results, for example: 141 | *
    usersIds.map [ get user ].onNone [ println('could not find user') ].filterNone
    142 | */ 143 | def static Iterable> onNone(Iterable> iterable, (None)=>void noneHandler) { 144 | iterable.filter[hasNone].effect[noneHandler.apply(it as None)] 145 | iterable 146 | } 147 | 148 | /** Triggers the passed or function for each error in the list, 149 | * handy for tracking errors for example: 150 | *
    usersIds.attemptMap [ get user ].onError [ error handling ].filterEmpty
    151 | */ 152 | def static Iterable> onError(Iterable> iterable, (Err)=>void errorHandler) { 153 | iterable.filter[hasError].effect[errorHandler.apply(it as Err)] 154 | iterable 155 | } 156 | 157 | // FILTERING ////////////////////////////////////////////////////////////// 158 | 159 | /** 160 | * Let through all values that when mapped with the mappingfunction, are contained in the allowedValues list. 161 | *

    162 | * Use this to easily see if some collection has properties that are in another collection. 163 | *

    164 | * Example: 165 | *

    166 | 	 * val investors = users.filterAll(#['Koos', 'Henk']) [ name ]
    167 | 	 * 
    168 | * 169 | * @param values The values to filter 170 | * @param checkValues the values that make a value pass the filter 171 | * @param mapToCheckedValueFn mapping function that transform a value to something to check 172 | * @return an iterable of values that when mapped, were in the checkValues. 173 | */ 174 | def static filterAll(Iterable values, List checkValues, (T)=>X mapToCheckedValueFn) { 175 | values.filter [ value | 176 | val candidate = mapToCheckedValueFn.apply(value) 177 | checkValues.contains(candidate) 178 | ] 179 | } 180 | 181 | /** 182 | * Let through all values that when mapped with the mappingfunction, are not contained in the disallowedValues list. 183 | *

    184 | * Use this to easily see if some collection has properties that are in another collection. 185 | *

    186 | * Example: 187 | *

    188 | 	 * val notInvestors = users.filterAll(#['Koos', 'Henk']) [ name ]
    189 | 	 * 
    190 | * * @param values The values to filter 191 | * @param checkValues the values that make a value miss the filter 192 | * @param mapToCheckedValueFn mapping function that transform a value to something to check 193 | * @return an iterable of values that when mapped, were not in the checkValues. 194 | */ 195 | def static filterNone(Iterable values, List checkValues, (T)=>X mapToCheckedValueFn) { 196 | values.filter [ value | 197 | val candidate = mapToCheckedValueFn.apply(value) 198 | !checkValues.contains(candidate) 199 | ] 200 | } 201 | 202 | def static T last(Iterable values) { 203 | values.toList.reverse.head 204 | } 205 | 206 | def static Iterable last(Iterable iterable, int amount) { 207 | if (iterable.size <= amount) iterable 208 | else iterable.tail.last(amount) 209 | } 210 | 211 | /** Returns a random entry of the {@code Iterable} it is performed on. In case of an empty list, {@code null} is returned. */ 212 | def static T random(Iterable values) { 213 | if (values.size == 0) return null 214 | val rand = new Random().nextInt(values.size) 215 | values.toList.get(rand) 216 | } 217 | 218 | /** Convert a list of options into actual values, filtering out the none and error values. 219 | Like filterNull, but then for a list of Options */ 220 | def static Iterable filterEmpty(Iterable> iterable) { 221 | iterable.map[orNull].filterNull 222 | } 223 | 224 | def static filterError(Iterable> iterable) { 225 | iterable.filter[!hasError] 226 | } 227 | 228 | /** Remove all double values in a list, turning it into a list of unique values */ 229 | def static distinct(Iterable values) { 230 | values.distinctBy[it] 231 | } 232 | 233 | /** Returns a filtered list of unique values, distinguished by {@code distinction}, first-come first-served. */ 234 | def static distinctBy(Iterable values, (T)=>D distinction) { 235 | values.groupBy(distinction).toPairs.map[value.head] 236 | } 237 | 238 | /** Returns the position/index of the value in the iterable, starting at 0 */ 239 | def static int indexOf(Iterable iterable, T value) { 240 | var counter = 0 241 | for (o : iterable) { 242 | if(o.equals(value)) return counter 243 | counter = counter + 1 244 | } 245 | return -1 246 | } 247 | 248 | // MAPPING //////////////////////////////////////////////////////////////// 249 | 250 | /** map all the some's, leaving alone the none's */ 251 | def static Iterable> mapOpt(Iterable> iterable, (T)=>R fn) { 252 | iterable.map[map(fn)] 253 | } 254 | 255 | /** try to map the values in the iterable, and give back a list of options instead of direct values */ 256 | def static attemptMap(Iterable iterable, (T)=>R fn) { 257 | iterable.map [ 258 | val o = it 259 | attempt [fn.apply(o)] 260 | ] 261 | } 262 | 263 | /** Turns a flat iterable into iterable groups of size {@code groupSize}. 264 | * Note: last entry size might be smaller than the specified {@code groupSize}. 265 | * @throws IllegalArgumentException if groupSize < 1 266 | */ 267 | def static unflatten(Iterable iterable, int groupSize) { 268 | if (groupSize < 1) throw new IllegalArgumentException('groupSize must be > 0') 269 | 270 | val List> list = newLinkedList(newLinkedList) 271 | 272 | iterable.fold(list) [ total, incoming | 273 | if (total.last.size == groupSize) total.add(newLinkedList(incoming)) 274 | else total.last.add(incoming) 275 | total 276 | ] 277 | } 278 | 279 | /** transforms a map into a list of pairs */ 280 | def static toPairs(Map map) { 281 | map.entrySet.map[it.key -> it.value].toList 282 | } 283 | 284 | /** Convert a list of pairs to a map */ 285 | def static Map toMap(Iterable> pairs) { 286 | val map = newHashMap 287 | if(pairs.defined) pairs.forEach[map.put(key, value)] 288 | map 289 | } 290 | 291 | /** 292 | * Create a grouped index for a list of items. if the keys are unique, use toMap instead! 293 | *

    294 | * Example: val groups = levels.groupBy [ difficulty ] // creates a map with per difficulty level a list of levels 295 | */ 296 | def static Map> groupBy(Iterable list, (V)=>K indexFn) { 297 | val map = new HashMap> 298 | list.forEach [ 299 | val index = indexFn.apply(it) 300 | if (map.containsKey(index)) { 301 | val values = map.get(index) 302 | values.add(it) 303 | } else { 304 | val values = newLinkedList(it) 305 | map.put(index, values) 306 | } 307 | ] 308 | map 309 | } 310 | 311 | /** Lets you map a pair using */ 312 | def static map(List> list, (K, V)=>R mapFn) { 313 | list.map [ mapFn.apply(key, value) ] 314 | } 315 | 316 | /** 317 | * Convert to a different type. will throw a class cast exception at runtime 318 | * if you convert to the wrong type! 319 | */ 320 | def static Iterable mapAs(Iterable iterable, Class type) { 321 | iterable.map[it as T] 322 | } 323 | 324 | /** 325 | * Flatten a list of keys -> list of pair values into more key->value pairs 326 | */ 327 | def static Iterable> flattenValues(Iterable>> pairs) { 328 | val newList = newLinkedList 329 | pairs.forEach [ pair | pair.value?.forEach [ newList.add(pair.key->it) ] ] 330 | newList 331 | } 332 | 333 | // REDUCTION ////////////////////////////////////////////////////////////// 334 | 335 | // count for each value how often it occurs 336 | // example: #[1, 1, 3, 2].count == #[1->2, 3->1, 2->1] 337 | def static Map count(Iterable values) { 338 | values.countBy[it] 339 | } 340 | 341 | // count the occurance of values, but you can tell how to identify doubles using the index/identity function 342 | // example: #[user1, user2, user3].countBy [ userId ] 343 | def static Map countBy(Iterable values, (V)=>K indexFn) { 344 | values.groupBy(indexFn).toPairs.map[value.head -> value.size].toMap 345 | } 346 | 347 | // alias for toMap for indexing a list of values 348 | def static Map index(Iterable iterable, (V)=>K indexFn) { 349 | iterable.toMap[indexFn.apply(it)] 350 | } 351 | 352 | def static sum(Iterable values) { 353 | var double total = 0 354 | for (T value : values) { 355 | total = total + value.doubleValue 356 | } 357 | total 358 | } 359 | 360 | def static average(Iterable values) { 361 | values.sum / values.length 362 | } 363 | 364 | def static median(Iterable values) { 365 | val sortedValues = values.sort 366 | switch length : sortedValues.size { 367 | case 0: throw new IllegalArgumentException('an empty list does not have a median') 368 | case 1: sortedValues.head 369 | case length % 2 == 0: (sortedValues.get(length/2 - 1) + sortedValues.get(length/2)) / 2 370 | default: sortedValues.get(length/2) 371 | } 372 | } 373 | 374 | def static List> groupByChange(Iterable items, (T, T, T)=>boolean isSameGroupFn) { 375 | val result = newLinkedList 376 | var currentGroup = newLinkedList 377 | result.add(currentGroup) 378 | var first = true 379 | var T head = null 380 | var T last = null 381 | for(item : items) { 382 | if(first) { 383 | first = false 384 | head = item 385 | last = item 386 | } else { 387 | if(!isSameGroupFn.apply(head, last, item)) { 388 | currentGroup = newLinkedList 389 | result.add(currentGroup) 390 | head = item 391 | } 392 | } 393 | currentGroup.add(item) 394 | last = item 395 | } 396 | return result 397 | } 398 | 399 | /** 400 | * Group by changes in the passed items. This only works on a list of items that are incrementally changing. 401 | *

    402 | * example: #[1, 2, 4, 6, 7].groupIncrementally [ a, b | b - a > 1 ] == #[[1, 2],[4],[6,7]] 403 | *

    404 | * @param items - the items to group by change 405 | * @param sameGroupFn - a function that takes the last item and the current item, 406 | * and returns true if they should be grouped, or false if the current item should 407 | * be seperated from the last one in a new group. 408 | */ 409 | def static List> groupByChange(Iterable items, (T, T)=>boolean isSameGroupFn) { 410 | groupByChange(items) [ head, last, item | 411 | isSameGroupFn.apply(last, item) 412 | ] 413 | } 414 | 415 | // BOOLEAN CHECKS ///////////////////////////////////////////////////////// 416 | 417 | /** Checks if any of the values is truthy */ 418 | def static boolean any(Iterable values) { 419 | for(value : values) { 420 | if(value.truthy) return true 421 | } 422 | false 423 | } 424 | 425 | /** Checks if all of the values are truthy */ 426 | def static boolean all(Iterable values) { 427 | for(value : values) { 428 | if(!value.truthy) return false 429 | } 430 | true 431 | } 432 | 433 | /** Checks if none of the values are truthy */ 434 | def static boolean none(Iterable values) { 435 | for(value : values) { 436 | if(value.truthy) return false 437 | } 438 | true 439 | } 440 | 441 | // IN CHECKS ////////////////////////////////////////////////////////////// 442 | 443 | // check if an object is one of the following 444 | // example: 12.in(3, 4, 12, 6) == true 445 | def static boolean in(T instance, T... objects) { 446 | instance.defined && objects.defined && objects.contains(instance) 447 | } 448 | 449 | def static boolean in(T instance, Iterable objects) { 450 | // instance.defined && objects.defined && 451 | objects.toList.contains(instance) 452 | } 453 | 454 | def static boolean in(T instance, Iterator objects) { 455 | instance.defined && objects.defined && objects.toList.contains(instance) 456 | } 457 | 458 | // LIST OPERATORS ///////////////////////////////////////////////////////// 459 | 460 | /** Create a new immutable list from this list that does not contain the value */ 461 | def static - (List list, T value) { 462 | list.uncat(value) 463 | } 464 | 465 | /** Create a new immutable list from a list and a value */ 466 | def static + (Iterable list, T value) { 467 | list.concat(value) 468 | } 469 | 470 | /** Create a new immutable list from this list that does not contain the value */ 471 | def static - (List list, List list2) { 472 | list.uncat(list2) 473 | } 474 | 475 | // FUNCTION OPERATORS ///////////////////////////////////////////////////// 476 | 477 | // map with -> 478 | def static -> (Iterable original, Functions.Function1 transformation) { 479 | original.map(transformation) 480 | } 481 | 482 | // filter with + 483 | def static + (Iterable unfiltered, Functions.Function1 predicate) { 484 | unfiltered.filter(predicate) 485 | } 486 | 487 | // remove with - 488 | def static - (Iterable unfiltered, Functions.Function1 predicate) { 489 | unfiltered.filter [ 490 | !predicate.apply(it) 491 | ] 492 | } 493 | 494 | // negating a filter with ! 495 | def static Functions.Function1 ! ( 496 | Functions.Function1 predicate) { 497 | [!predicate.apply(it)] 498 | } 499 | 500 | def static > minSafe(T... values) { 501 | try org.eclipse.xtext.xbase.lib.IterableExtensions.min(values) catch(NoSuchElementException it) null 502 | } 503 | 504 | def static > maxSafe(T... values) { 505 | try org.eclipse.xtext.xbase.lib.IterableExtensions.max(values) catch(NoSuchElementException it) null 506 | } 507 | 508 | } 509 | -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/Log.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import org.slf4j.Logger 4 | import org.eclipse.xtend.lib.annotations.Data 5 | import org.slf4j.LoggerFactory 6 | 7 | /** 8 | * Deferred logging for Xtend. Wraps a org.slf4j.Logger. 9 | *

    10 | * This loggers lowers the performance penalty of logging a lot 11 | * of trace or debug statements. 12 | *

    13 | *

     14 |  * val logger = LoggerFactory.getLogger(this.class)
     15 |  * 
     16 |  * // this can become expensive:
     17 |  * logger.trace('my message ' + someValue + 'blabla' + calculate())
     18 |  * 
     19 |  * // so you would do this instead every time:
     20 |  * if(logger.traceEnabled) {
     21 |  * 	// string is only built if we have trace enabled
     22 |  * 	logger.trace('my message ' + someValue + 'blabla' + calculate())
     23 |  * }
     24 |  * 
     25 |  * // but now you can do this, which does the same:
     26 |  * val log = new Log(logger)
     27 |  * log.trace [ 'my message ' + someValue + 'blabla' + calculate() ]
     28 |  * 
    29 | *

    30 | * Usage: 31 | *

      32 | *
    • Declaration: extension Log logger = Log.create(this, 'foo') 33 | *
    • Usage example: info ['''bulk shown «bulkShownUsers.size» out of the requested '''] 34 | *
    35 | */ 36 | @Data class Log { 37 | 38 | /** Create a org.slf4j.Logger */ 39 | static def create(Object instance) { 40 | create(instance, null) 41 | } 42 | 43 | /** Create a org.slf4j.Logger that logs under a given name. */ 44 | static def create(Object instance, String name) { 45 | val logger = LoggerFactory.getLogger(instance.class) 46 | new Log(logger, name) 47 | } 48 | 49 | protected Logger logger 50 | protected String name 51 | 52 | // deferred logging functions 53 | 54 | def trace(=>String message) { 55 | if(logger.traceEnabled) logger.trace(message.entry) 56 | } 57 | 58 | def debug(=>String message) { 59 | if(logger.debugEnabled) logger.debug(message.entry) 60 | } 61 | 62 | def info(String message) { 63 | if(logger.infoEnabled) logger.info(message.entry) 64 | } 65 | 66 | def info(=>String message) { 67 | if(logger.infoEnabled) logger.info(message.entry) 68 | } 69 | 70 | def warn(String message) { 71 | if(logger.warnEnabled) logger.warn(message.entry) 72 | } 73 | 74 | def warn(=>String message) { 75 | if(logger.warnEnabled) logger.warn(message.entry) 76 | } 77 | 78 | def error(String message, Throwable t) { 79 | if(logger.errorEnabled) logger.error(message.entry, t) 80 | } 81 | 82 | // logging function functions 83 | 84 | def (T)=>void trace() { [ logger.trace(toString) ] } 85 | 86 | def (T)=>void debug() { [ logger.debug(toString) ] } 87 | 88 | def (T)=>void info() { [ logger.info(toString) ] } 89 | 90 | def (T)=>void warn() { [ logger.warn(toString) ] } 91 | 92 | def (T)=>void error() { [ logger.error(toString) ] } 93 | 94 | // protected helper methods 95 | 96 | def protected getEntry(String message) { 97 | if(name !== null) '''«name»: «message»'''.toString 98 | else message 99 | } 100 | 101 | def protected getEntry(=>String message) { 102 | message.apply.entry 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/LogExtensions.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import org.slf4j.Logger 4 | 5 | import static extension nl.kii.util.OptExtensions.* 6 | 7 | class LogExtensions { 8 | 9 | // WRAPPER //////////////////////////////////////////////////////////////// 10 | 11 | // create a logging wrapper 12 | // example: extension Log logger = class.logger.wrapper 13 | def static wrapper(Logger logger) { 14 | new Log(logger, null) 15 | } 16 | 17 | // create a logging wrapper 18 | // example: extension Log logger = class.logger.wrapper 19 | def static wrapper(Logger logger, String name) { 20 | new Log(logger, name) 21 | } 22 | 23 | // PRINTING /////////////////////////////////////////////////////////////// 24 | 25 | // print a list in seperate lines 26 | def static print(CharSequence ...s) { 27 | s.forEach[ println(it) ] 28 | } 29 | 30 | // print the result of a function 31 | // example: print [ new User() ] 32 | def static print(=>T s) { 33 | println(s.apply) 34 | } 35 | 36 | /** Chainable println */ 37 | def static print(T object) { 38 | println(object) 39 | } 40 | 41 | // PRINTING FUNCTION ////////////////////////////////////////////////////// 42 | 43 | def static (T)=>void printEach() { 44 | [ println(it) ] 45 | } 46 | 47 | def static (T)=>void printEach(String msg) { 48 | [ println(msg + it) ] 49 | } 50 | 51 | // LOGGING LISTS ////////////////////////////////////////////////////////// 52 | 53 | def static printEach(Iterable list, String msg) { 54 | if(msg.defined) println(msg) 55 | list.forEach [ println(logEntry) ] 56 | } 57 | 58 | def static trace(Iterable list, String msg, Log log) { 59 | if(msg.defined) log.logger.trace(msg) 60 | list.forEach [ log.logger.trace(logEntry) ] 61 | } 62 | 63 | def static debug(Iterable list, String msg, Log log) { 64 | if(msg.defined) log.logger.debug(msg) 65 | list.forEach [ log.logger.debug(logEntry) ] 66 | } 67 | 68 | def static info(Iterable list, String msg, Log log) { 69 | if(msg.defined) log.logger.info(msg) 70 | list.forEach [ log.logger.info(logEntry) ] 71 | } 72 | 73 | def static warn(Iterable list, String msg, Log log) { 74 | if(msg.defined) log.logger.warn(msg) 75 | list.forEach [ log.logger.warn(logEntry) ] 76 | } 77 | 78 | def static error(Iterable list, String msg, Log log) { 79 | if(msg.defined) log.logger.error(msg) 80 | list.forEach [ log.logger.error(logEntry) ] 81 | } 82 | 83 | def protected static getLogEntry(Object o) { ' - ' + o.toString } 84 | 85 | def static printEach(Iterable list) { printEach(list, null) } 86 | def static trace(Iterable list, Log log) { trace(list, null, log) } 87 | def static debug(Iterable list, Log log) { debug(list, null, log) } 88 | def static info(Iterable list, Log log) { info(list, null, log) } 89 | def static warn(Iterable list, Log log) { warn(list, null, log) } 90 | def static error(Iterable list, Log log) { error(list, null, log) } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/MapExtensions.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import java.util.Map 4 | import java.util.Map.Entry 5 | import java.util.List 6 | import com.google.common.collect.ImmutableMap 7 | 8 | class MapExtensions { 9 | 10 | // OPERATOR OVERLOADING /////////////////////////////////////////////////// 11 | 12 | def static += (Map map, Pair pair) { 13 | map.put(pair.key, pair.value) 14 | map 15 | } 16 | 17 | def static += (Map map, Entry entry) { 18 | map.put(entry.key, entry.value) 19 | map 20 | } 21 | 22 | /** Immutable Pair-to-Map addition */ 23 | def static + (Map map, Pair pair) { 24 | ImmutableMap.builder.putAll(map).put(pair.key, pair.value).build 25 | } 26 | 27 | /** Immutable Map.Entry-to-Map addition */ 28 | def static + (Map map, Entry entry) { 29 | ImmutableMap.builder.putAll(map).put(entry).build 30 | } 31 | 32 | // MAP METHODS /////////////////////////////////////////////////////// 33 | 34 | def static add(Map> map, Pair> pairPair) { 35 | map.put(pairPair.key, pairPair.value.key, pairPair.value.value) 36 | } 37 | 38 | def static put(Map> map, K key1, K2 key2, V value) { 39 | val valueMap = if(map.containsKey(key1)) map.get(key1) else newHashMap 40 | valueMap.put(key2, value) 41 | } 42 | 43 | def static remove(Map> map, K key1, K2 key2) { 44 | val valueMap = map.get(key1) 45 | if(valueMap !== null) valueMap.remove(key2) 46 | } 47 | 48 | // MAP METHODS ////////////////////////////////////////////////////// 49 | 50 | /** Add a value to a multimap structure */ 51 | def static void add(Map> map, K key, V value) { 52 | if(map.containsKey(key)) map.get(key).add(value) 53 | else { 54 | map.put(key, newLinkedList) 55 | map.add(key, value) // recurse 56 | } 57 | } 58 | 59 | // ITERATIONS ///////////////////////////////////////////////////////////// 60 | 61 | /** Iterate through a map as if it were a list, but with key and value */ 62 | def static forEach(Map map, (K, V)=>void fn) { 63 | map.entrySet.map[key -> value].forEach[fn.apply(key, value)] 64 | map 65 | } 66 | 67 | // MAPPING //////////////////////////////////////////////////////////////// 68 | 69 | /** 70 | * Map both the keys and values of a map, 71 | * using a mapping function that returns a pair 72 | *
    73 | 	 * Example:
    74 | 	 * #{ 1->5, 2->6 }.map [ k, v | k+1->v+1 ]
    75 | 	 * Result:
    76 | 	 * #{ 2->6, 3->7 }
    77 | 	 * 
    78 | */ 79 | def static Map map(Map map, (K, V)=>Pair mapFn) { 80 | val newMap = newHashMap 81 | for(key : map.keySet) { 82 | val pair = mapFn.apply(key, map.get(key)) 83 | newMap.put(pair.key, pair.value) 84 | } 85 | newMap 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/Matcher.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | import static extension nl.kii.util.OptExtensions.* 3 | 4 | interface Matcher { 5 | 6 | def Opt match(I instance) 7 | 8 | } 9 | 10 | class InstanceMatcher implements Matcher { 11 | 12 | Class[] types 13 | 14 | new(Class... types) { 15 | this.types = types 16 | } 17 | 18 | override match(Object instance) { 19 | for(type : types) { 20 | if(type.isAssignableFrom(type)) 21 | return some(instance as T) 22 | } 23 | none 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/MultiDateFormat.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import java.text.DateFormat 4 | import java.text.ParseException 5 | import java.text.SimpleDateFormat 6 | import java.util.Date 7 | import java.util.List 8 | import java.util.Locale 9 | 10 | class MultiDateFormat { 11 | 12 | val List dateFormats 13 | // val Locale locale 14 | 15 | new (String dateFormat, Locale locale) { 16 | this.dateFormats = dateFormat.split('\\;\\s').map [ new SimpleDateFormat(it, Locale.ENGLISH) ] 17 | // this.locale = locale 18 | } 19 | 20 | def parse(String string) { 21 | val possibleValidFormat = dateFormats.findFirst [ string.matches(it) ] 22 | (possibleValidFormat ?: dateFormats.head).parse(string) 23 | } 24 | 25 | def format(Date date) { 26 | dateFormats 27 | .findFirst [ date.matches(it) ] 28 | .format(date) 29 | } 30 | 31 | def matches(Object input, DateFormat format) { 32 | try { 33 | switch it : input { 34 | String: format.parse(it) 35 | Date: format.format(it) 36 | } 37 | true 38 | } catch(ParseException e) { 39 | false 40 | } 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/NumericExtensions.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | class NumericExtensions { 4 | 5 | def static thousand(int n) { n * 1000L } 6 | def static k(int n) { n.thousand } 7 | 8 | def static million(int n) { n * 1_000_000L } 9 | def static m(int n) { n.million } 10 | 11 | def static billion(int n) { n * 1_000_000_000L } 12 | def static b(int n) { n.billion } 13 | 14 | def static times(int n, (int)=>void action) { 15 | if (n > 0) 16 | (1..n).forEach(action) 17 | } 18 | 19 | /** 20 | * Wrapper for Long.parseLong(string) to be compatible with the Xtend null-safe call ({@code ?.}) 21 | */ 22 | def static Long parseLong(String string) { 23 | Long.parseLong(string) 24 | } 25 | 26 | /** 27 | * Wrapper for Integer.parseInt(string) to be compatible with the Xtend null-safe call ({@code ?.}) 28 | */ 29 | def static Integer parseInt(String string) { 30 | Integer.parseInt(string) 31 | } 32 | 33 | /** 34 | * Wrapper for Double.parseDouble(string) to be compatible with the Xtend null-safe call ({@code ?.}) 35 | */ 36 | def static Double parseDouble(String string) { 37 | Double.parseDouble(string) 38 | } 39 | 40 | /** 41 | * Wrapper for Float.parseFloat(string) to be compatible with the Xtend null-safe call ({@code ?.}) 42 | */ 43 | def static Float parseFloat(String string) { 44 | Float.parseFloat(string) 45 | } 46 | 47 | /** 48 | * Wrapper for Boolean.parseBoolean(string) to be compatible with the Xtend null-safe call ({@code ?.}) 49 | */ 50 | def static Boolean parseBoolean(String string) { 51 | Boolean.parseBoolean(string) 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/ObjectExtensions.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | class ObjectExtensions { 4 | 5 | /** Chainable version of the 'with'-operator ({@code object => [ ] }) */ 6 | def static with(T object, (T)=>void block) { 7 | object => block 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/Opt.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import org.eclipse.xtext.xbase.lib.Functions.Function0 4 | 5 | abstract class Opt implements Iterable { 6 | def T value() throws NoneException 7 | def boolean hasSome() 8 | def boolean hasNone() 9 | def boolean hasError() 10 | } 11 | 12 | class None extends Opt { 13 | override value() { throw new NoneException } 14 | override hasSome() { false } 15 | override hasNone() { true } 16 | override hasError() { false } 17 | override iterator() { newLinkedList.iterator } 18 | override equals(Object obj) { obj instanceof None } 19 | override hashCode() { 0 } 20 | override toString() '''None''' 21 | } 22 | 23 | class Err extends Opt implements Function0 { 24 | val Throwable exception 25 | 26 | new() { exception = new Exception } 27 | new(Throwable exception) { this.exception = exception } 28 | 29 | def getException() { exception } 30 | def getMessage() { exception.message } 31 | def getStackTrace() { exception.stackTrace } 32 | override apply() { exception } 33 | override value() { throw new NoneException } 34 | override hasSome() { false } 35 | override hasNone() { false } 36 | override hasError() { true } 37 | override iterator() { newLinkedList.iterator } 38 | override equals(Object obj) { obj instanceof Err } 39 | override hashCode() { -1 } 40 | override toString() '''Error («exception.message»)''' 41 | } 42 | 43 | class Some extends Opt implements Function0 { 44 | var T value 45 | 46 | new(T value) { 47 | if(value === null) throw new NullPointerException('cannot create new Some(null)') 48 | this.value = value 49 | } 50 | override value() { value } 51 | override apply() { value } 52 | override hasSome() { true } 53 | override hasNone() { false } 54 | override hasError() { false } 55 | override iterator() { newLinkedList(value).iterator } 56 | override hashCode() { value.hashCode } 57 | override equals(Object obj) { obj == value || (obj instanceof Some) && (obj as Some).value == value } 58 | override toString() '''Some(«value»)''' 59 | 60 | } 61 | 62 | class NoneException extends Exception { 63 | new() { } 64 | new(String message) { super(message) } 65 | } 66 | -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/OptExtensions.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | class OptExtensions { 4 | 5 | // BOOLEAN DEFINED CHECK ////////////////////////////////////////////////// 6 | 7 | /** 8 | * Checks if an object is defined, meaning here that it is not empty or faulty 9 | */ 10 | def static defined(Object o) { 11 | switch o { 12 | case null: false 13 | None: false 14 | Err: false 15 | default: true 16 | } 17 | } 18 | 19 | /** Checks if an object is truthy, meaning it is not false and it is defined. */ 20 | def static truthy(Object o) { 21 | switch o { 22 | Boolean: o 23 | default: o.defined 24 | } 25 | } 26 | 27 | /** 28 | * Only perform the function for a given condition. Returns an optional result. 29 | *
    val Opt user = ifTrue(isMale) [ getUser ]
    30 | */ 31 | def static Opt ifTrue(boolean condition, (Object)=>T fn) { 32 | if(condition) fn.apply(null).option 33 | else none 34 | } 35 | 36 | def static Opt => (Opt o, (T)=>void fn) { 37 | ifSome(o, fn) 38 | } 39 | 40 | /** 41 | * Only perform the function if something was set. Returns the result of the function for chaining 42 | */ 43 | def static Opt ifSome(Opt o, (T)=>void fn) { 44 | if(o.defined) fn.apply(o.value) 45 | o 46 | } 47 | 48 | /** 49 | * Only perform the function if o is a None. Returns the result of the function for chaining 50 | */ 51 | def static Opt ifNone(Opt o, =>void fn) { 52 | if(o.hasNone) fn.apply 53 | o 54 | } 55 | 56 | /** 57 | * Only perform the function if the passed argument was empty, 58 | * meaning null or empty or an error. Returns an optional result. 59 | *
    val Opt user = ifEmpty(userId) [ getDefaultUser ]
    60 | */ 61 | def static Opt ifEmpty(Opt o, =>void fn) { 62 | if(!o.defined) fn.apply 63 | o 64 | } 65 | 66 | /** 67 | * Only perform the function if o is an Err. Returns the result of the function for chaining 68 | */ 69 | def static Opt ifErr(Opt o, (Throwable)=>void fn) { 70 | switch(o) { Err: fn.apply(o.exception) } 71 | o 72 | } 73 | 74 | // OPERATOR OVERLOADING /////////////////////////////////////////////////// 75 | 76 | def static T ?:(Opt option, T fallback) { 77 | if(option.defined) option.value 78 | else fallback 79 | } 80 | 81 | def static Opt ?:(Opt option, Opt fallback) { 82 | if(option.defined) option 83 | else fallback 84 | } 85 | 86 | def static T ?:(Opt option, (Void)=>T fallback) { 87 | if(option.defined) option.value 88 | else fallback.apply(null) 89 | } 90 | 91 | // OPTION CREATION //////////////////////////////////////////////////////// 92 | 93 | /** 94 | * Create an option from an object. It detect if it is a None or a Some. 95 | *
    api.getUser(userId).option // if getUser returns null, it will be None, otherwise Some
    96 | */ 97 | def static Opt option(T value) { 98 | if(value.defined) some(value) 99 | else if(value instanceof Err) err(value.exception) 100 | else none 101 | } 102 | 103 | def static Some some(T value) { 104 | new Some(value) 105 | } 106 | 107 | def static None none() { 108 | new None 109 | } 110 | 111 | def static Err err(Throwable t) { 112 | new Err(t) 113 | } 114 | 115 | def static Err err() { 116 | new Err() 117 | } 118 | 119 | // ATTEMPTS /////////////////////////////////////////////////////////////// 120 | 121 | /** 122 | * wrap a call as an option (exception or null generates none)

    123 | * example: val userOption = attempt [ api.getUser(userId) ] // if API throws exception, return None 124 | */ 125 | def static Opt attempt(=>T function) { 126 | try function.apply.option 127 | catch(Exception e) err(e) 128 | } 129 | 130 | /** 131 | * wrap a call as an option (exception or null generates none)

    132 | * example: val userOption = attempt [ api.getUser(userId) ] // if API throws exception, return None 133 | */ 134 | def static Opt attempt(P param, (P)=>T function) { 135 | try function.apply(param).option 136 | catch(Exception e) err(e) 137 | } 138 | 139 | /** 140 | * Same as => but with optional execution and option result

    141 | * example: normally you do: user => [ name = 'john' ]

    142 | * but what if user is of type Option

    143 | * then you can do: user.attempt [ name = 'john' ]
    144 | * the assignment will only complete if there was a user 145 | */ 146 | def static Opt attempt(Opt o, (O)=>T fn) { 147 | if(o.defined) fn.apply(o.value) 148 | o 149 | } 150 | 151 | /** 152 | * Same as => but with optional execution and option result 153 | * example: normally you do: user => [ name = 'john' ] 154 | * but what if user is of type Option 155 | * then you can do: user.attempt [ name = 'john' ] 156 | * the assignment will only complete if there was a user 157 | *

    158 | * This version accept functions that have no result 159 | */ 160 | def static Opt attempt(Opt o, (O)=>void fn) { 161 | if(o.defined) fn.apply(o.value) 162 | o 163 | } 164 | 165 | // MAPPING //////////////////////////////////////////////////////////////// 166 | 167 | /** 168 | * Transform an option into a new option using a function. 169 | * The function allows you to transform the value of the passed option, 170 | * saving you the need to unwrap it yourself 171 | */ 172 | def static Opt map(Opt o, (I)=>T fn) { 173 | if(o.defined) fn.apply(o.value).option else none 174 | } 175 | 176 | // FLATTEN //////////////////////////////////////////////////////////////// 177 | 178 | /** 179 | * Flatten a double wrapped optional back to a single optional 180 | */ 181 | def static Opt flatten(Opt> option) { 182 | switch option { 183 | Some>: option.value 184 | None>: none 185 | Err>: err(option.exception) 186 | } 187 | } 188 | 189 | // OPTIONAL FALLBACK EXTENSIONS /////////////////////////////////////////// 190 | 191 | /** 192 | * Provide a fallback value if o is undefined 193 | *

    val user = foundUser.or(defaultUser)
    194 | */ 195 | def static T or(T o, T fallback) { 196 | if(o.defined) o else fallback 197 | } 198 | 199 | /** 200 | * provide a fallback value if o is undefined 201 | *
    val user = api.getUser(12).or(defaultUser) // getUser returns an Option
    202 | */ 203 | def static T or(Opt o, T fallback) { 204 | if(o.defined) o.value else fallback 205 | } 206 | 207 | // run a fallback function if o is undefined 208 | // example: val user = foundUser.or [ api.getDefaultUser() ] 209 | def static T or(T o, (Object)=>T fallbackFn) { 210 | if(o.defined) o else fallbackFn.apply(null) 211 | } 212 | 213 | // run a fallback function if o is undefined 214 | // example: val user = api.getUser(12).or [ api.getDefaultUser() ] // getUser returns an Option 215 | def static T or(Opt o, (Object)=>T fallbackFn) { 216 | if(o.defined) o.value else fallbackFn.apply(null) 217 | } 218 | 219 | def static T orNull(T o) { 220 | if(o.defined) o else null 221 | } 222 | 223 | def static T orNull(Opt o) { 224 | if(o.defined) o.value else null 225 | } 226 | 227 | def static T orThrow(Opt o) { 228 | switch(o) { 229 | Err: throw o.exception 230 | None: throw new NoneException 231 | Some: o.value 232 | } 233 | } 234 | 235 | // try to unwrap an option, and if there is nothing, throws an exception 236 | // example: val user = api.getUser(12).orThrow('could not find user') // getUser returns an option 237 | def static T orThrow(Opt o, String s) { 238 | if(o.defined) o.value else throw new NoneException(s) 239 | } 240 | 241 | // try to unwrap an option, and if there is nothing, calls the exceptionFn to get a exception to throw 242 | // example: val user = api.getUser(12).orThrow [ new UserNotFoundException ] // getUser returns an option 243 | def static T orThrow(Opt o, (Object)=>Throwable exceptionFn) { 244 | if(o.defined) o.value else throw exceptionFn.apply(null) 245 | } 246 | 247 | } 248 | -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/PartialURL.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import org.eclipse.xtend.lib.annotations.Accessors 4 | 5 | import static java.util.regex.Pattern.* 6 | 7 | import static extension nl.kii.util.IterableExtensions.* 8 | import java.net.MalformedURLException 9 | 10 | /** 11 | * This URL implementation is more flexible than the standard Java URL, it allows for incomplete URLs 12 | * to be parsed and constructed. This is useful for matching these partial urls especially, or for 13 | * creating relative urls. 14 | */ 15 | @Accessors 16 | class PartialURL { 17 | 18 | val static pattern = compile("^(?=[^&])(?:(?[^:/?#]+):)?(?://(?[^/?#]*))?(?[^?#]*)(?:\\?(?[^#]*))?(?:#(?.*))?") 19 | val static validate = compile('^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]') 20 | 21 | String protocol = 'http' 22 | String domain 23 | String path 24 | String query 25 | String hash 26 | 27 | new() { 28 | } 29 | 30 | new(String url) { 31 | val matcher = pattern.matcher(url) 32 | if(matcher.matches) { 33 | protocol = matcher.group('scheme') 34 | domain = matcher.group('authority') 35 | path = matcher.group('path') 36 | query = matcher.group('query') 37 | hash = matcher.group('fragment') 38 | } else throw new MalformedURLException 39 | } 40 | 41 | /** get a map of all the parameters passed in the url */ 42 | def getParameters() { 43 | query 44 | ?.split('&') 45 | ?.map [ split('=') ] 46 | ?.map [ 47 | switch length { 48 | case 1: get(0)->'' 49 | case 2: get(0)->get(1) 50 | default: null 51 | } 52 | ] 53 | ?.filterNull 54 | ?.toMap 55 | } 56 | 57 | /** combination of protocol, domain and path */ 58 | def getFullPath() { 59 | protocol + '://'+ (domain?:'') + (path?:'') 60 | } 61 | 62 | /** checks if the passed url is a valid url (adheres to standards) */ 63 | def static isValid(String url) { 64 | validate.matcher(url).matches 65 | } 66 | 67 | /** checks if the partial url is complete enough to be valid */ 68 | def isValid() { 69 | validate.matcher(toString).matches 70 | } 71 | 72 | override toString() 73 | '''«IF protocol!==null»«protocol»://«ENDIF»«IF domain!==null»«domain»«ENDIF»«IF path!==null»«path»«ENDIF»«IF query!==null»?«query»«ENDIF»«IF hash!==null»#«hash»«ENDIF»''' 74 | 75 | } 76 | -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/Period.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | class Period implements Comparable { 4 | val long time 5 | 6 | new(long time) { this.time = time } 7 | 8 | def long time() { time } 9 | 10 | def getMs() { time } 11 | def getSecs() { time / 1000 } 12 | def getMins() { time / 1000 / 60 } 13 | def getHours() { time / 1000 / 60 / 60 } 14 | def getDays() { time / 1000 / 60 / 60 / 24 } 15 | def getYears() { time / 1000 / 60 / 60 / 24 / 356 } 16 | 17 | override toString() { 18 | val t = switch time { 19 | case years > 1: new Years(years) 20 | case days > 1: new Days(days) 21 | case hours > 1: new Hours(hours) 22 | case mins > 1: new Minutes(mins) 23 | case secs > 1: new Seconds(secs) 24 | default: new MilliSeconds(ms) 25 | } 26 | val remainingMs = ms - t.ms 27 | '''«t»«IF remainingMs > 0», «new Period(remainingMs)»«ENDIF»''' 28 | } 29 | 30 | override equals(Object obj) { 31 | if(obj instanceof Period) obj.time == time else false 32 | } 33 | 34 | override hashCode() { new Long(time).hashCode } 35 | 36 | override compareTo(Period o) { this.time.compareTo(o.time) } 37 | } 38 | 39 | class MilliSeconds extends Period { 40 | new(long ms) { super(ms) } 41 | override toString() '''«ms» milliseconds''' 42 | } 43 | 44 | class Seconds extends Period { 45 | new(long secs) { super(secs * 1000) } 46 | override toString() '''«secs» seconds''' 47 | } 48 | 49 | class Minutes extends Period { 50 | new(long mins) { super(mins * 1000 * 60) } 51 | override toString() '''«mins» minutes''' 52 | } 53 | 54 | class Hours extends Period { 55 | new(long hrs) { super(hrs * 1000 * 60 * 60) } 56 | override toString() '''«hours» hours''' 57 | } 58 | 59 | class Days extends Period { 60 | new(long days) { super(days * 1000 * 60 * 60 * 24) } 61 | override toString() '''«days» days''' 62 | } 63 | 64 | class Years extends Period { 65 | new(long years) { super(years * 1000 * 60 * 60 * 24 * 365) } 66 | override toString() '''«years» years''' 67 | } 68 | -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/SessionIDGenerator.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import java.math.BigInteger 4 | import java.security.SecureRandom 5 | import static extension java.util.UUID.* 6 | 7 | /** got collisions when using the old version, now using official Java random ids to see if that fixes the issue. */ 8 | class SessionIDGenerator { 9 | 10 | def String nextSessionId() { 11 | randomUUID.toString 12 | } 13 | 14 | } 15 | 16 | class RandomIDGenerator { 17 | 18 | val random = new SecureRandom 19 | 20 | def String nextSessionId() { 21 | new BigInteger(130, random).toString(32) 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/SetOperationExtensions.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import static extension nl.kii.util.IterableExtensions.* 4 | 5 | class SetOperationExtensions { 6 | 7 | /** Union – A ∪ B */ 8 | def static unionBy(Iterable> sets, (T, T)=>boolean match) { 9 | sets.reduce [ total, incoming | 10 | val filtered = incoming.filter [ entry | !total.exists [ match.apply(entry, it) ] ].list 11 | total.concat(filtered) 12 | ] 13 | } 14 | 15 | /** Intersection – A ∩ B */ 16 | def static intersectionBy(Iterable> sets, (T, T)=>boolean match) { 17 | sets.reduce [ total, incoming | 18 | total.filter [ entry | incoming.exists [ match.apply(entry, it) ] ].list 19 | ] 20 | } 21 | 22 | /** Difference – A \ B */ 23 | def static subtractionBy(Iterable> sets, (T, T)=>boolean match) { 24 | sets.reduce [ total, incoming | 25 | total.filter [ entry | !incoming.exists [ match.apply(entry, it) ] ].list 26 | ] 27 | } 28 | 29 | /** Symmetric difference – A △ B */ 30 | def static differenceBy(Iterable> sets, (T, T)=>boolean match) { 31 | #[ sets.unionBy(match), sets.intersectionBy(match) ].subtractionBy(match) 32 | } 33 | 34 | // EQUALS SHORTCUTS ///////////////////////////////////////////////////////////////////////////////// 35 | 36 | /** Union – A ∪ B – unionBy shortcut that applies a.equals(b) */ 37 | def static union(Iterable> sets) { 38 | sets.unionBy [ a, b | a == b ] 39 | } 40 | 41 | /** Intersection – A ∩ B – intersectionBy shortcut that applies a.equals(b) */ 42 | def static intersection(Iterable> sets) { 43 | sets.intersectionBy [ a, b | a == b ] 44 | } 45 | 46 | /** Difference – A \ B – subtractionBy shortcut that applies a.equals(b) */ 47 | def static subtraction(Iterable> sets) { 48 | sets.subtractionBy [ a, b | a == b ] 49 | } 50 | 51 | /** Symmetric difference – A △ B – differenceBy shortcut that applies a.equals(b) */ 52 | def static difference(Iterable> sets) { 53 | sets.differenceBy [ a, b | a == b ] 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/StringExtensions.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import com.google.common.base.CaseFormat 4 | 5 | class StringExtensions { 6 | 7 | def static changeCase(String name, CaseFormat from, CaseFormat to) { 8 | from.to(to, name) 9 | } 10 | 11 | def static limit(String string, int maxLength) { 12 | if(string === null || string.length <= maxLength) return string 13 | string.substring(0, maxLength) 14 | } 15 | 16 | /** 17 | * Prints a string that replaces all {} in the string with the passed objects. 18 | * You can also pass the parameter number in the string like this: {n}, where 19 | * n starts from 1. 20 | */ 21 | def static println(String string, Object... objects) { 22 | println(string.fill(objects)) 23 | } 24 | 25 | /** 26 | * Returns a string that replaces all {} in the string with the passed objects. 27 | * You can also pass the parameter number in the string like this: {n}, where 28 | * n starts from 1. 29 | */ 30 | def static fill(String string, Object... objects) { 31 | var result = string 32 | var i = 0 33 | for(o : objects) { 34 | i++ 35 | if(result.contains('{' + i + '}')) { 36 | result = result.replace('{' + i + '}', o.toString) 37 | } else { 38 | result = result.replaceFirst('\\{\\}', o.toString) 39 | } 40 | } 41 | result 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/SyncExtensions.java: -------------------------------------------------------------------------------- 1 | package nl.kii.util; 2 | 3 | import org.eclipse.xtext.xbase.lib.Functions.Function0; 4 | import org.eclipse.xtext.xbase.lib.Functions.Function1; 5 | import org.eclipse.xtext.xbase.lib.Functions.Function2; 6 | import org.eclipse.xtext.xbase.lib.Procedures.Procedure0; 7 | import org.eclipse.xtext.xbase.lib.Procedures.Procedure1; 8 | 9 | public class SyncExtensions { 10 | 11 | /** 12 | * Apply Java synchronized block on an object and apply the procedure on it 13 | */ 14 | public static void sync(T obj, Procedure0 proc) { 15 | synchronized (obj) { 16 | proc.apply(); 17 | } 18 | } 19 | 20 | /** 21 | * Apply Java synchronized block on an object and apply the procedure on it 22 | */ 23 | public static R sync(T obj, Function0 proc) { 24 | synchronized (obj) { 25 | return proc.apply(); 26 | } 27 | } 28 | 29 | /** 30 | * Apply Java synchronized block on an object and apply the procedure on it. 31 | * Passes the object into the function. 32 | */ 33 | public static R sync(T obj, Function1 proc) { 34 | synchronized (obj) { 35 | return proc.apply(obj); 36 | } 37 | } 38 | 39 | 40 | /** 41 | * Create a new synchronized function out of an existing one 42 | */ 43 | public static Procedure1 sync(Procedure1 p) { 44 | final Procedure1 p2 = p; 45 | return new Procedure1() { 46 | 47 | @Override 48 | public void apply(T o) { 49 | synchronized(o) { 50 | p2.apply(o); 51 | } 52 | } 53 | 54 | }; 55 | } 56 | 57 | /** 58 | * Create a new synchronized function out of an existing one 59 | */ 60 | public static Function1 sync(Function1 fn) { 61 | final Function1 fn2 = fn; 62 | return new Function1() { 63 | 64 | @Override 65 | public R apply(T p) { 66 | synchronized(p) { 67 | return fn2.apply(p); 68 | } 69 | } 70 | 71 | }; 72 | } 73 | 74 | /** 75 | * Create a new synchronized function out of an existing one 76 | */ 77 | public static Function2 sync(Function2 fn) { 78 | final Function2 fn2 = fn; 79 | return new Function2() { 80 | 81 | @Override 82 | public R apply(T1 p1, T2 p2) { 83 | synchronized(p1) { 84 | synchronized(p2) { 85 | return fn2.apply(p1, p2); 86 | } 87 | } 88 | } 89 | 90 | }; 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/TemporalExtensions.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import java.time.Duration 4 | import java.time.Instant 5 | import java.time.format.DateTimeFormatter 6 | import java.time.temporal.TemporalAmount 7 | import java.util.Date 8 | 9 | import static extension nl.kii.util.IterableExtensions.* 10 | 11 | class TemporalExtensions { 12 | 13 | val static standardDateFormat = "yyyy-MM-dd'T'HH:mm:ss" 14 | 15 | /** The current date */ 16 | def static now() { Instant.now } 17 | 18 | /** Convert to old {@code java.util.Date} */ 19 | def static toDate(Instant instant) { 20 | Date.from(instant) 21 | } 22 | 23 | def static epoch(Duration epoch) { 24 | Instant.ofEpochMilli(epoch.ms) 25 | } 26 | 27 | def static epoch(Instant instant) { 28 | instant.epochSecond.secs + instant.nano.longValue.nanos 29 | } 30 | 31 | // def static toLegacyDate(Instant instant) { 32 | // Date.from(instant) 33 | // } 34 | 35 | // /** Create a date for a specific calendar moment */ 36 | // def static moment(int year, int month, int day) { 37 | // moment(year, month, day, 0, 0, 0, 0) 38 | // } 39 | 40 | // /** Create a date for a specific calendar moment */ 41 | // def static moment(int year, int month, int day, int hour, int min, int sec) { 42 | // moment(year, month, day, hour, min, sec, 0) 43 | // } 44 | // 45 | // /** Create a date for a specific calendar moment */ 46 | // def static moment(int year, int month, int day, int hour, int min, int sec, int ms) { 47 | // new Date().withCalendar [ 48 | // set(YEAR, year) 49 | // set(MONTH, month) 50 | // set(DAY_OF_MONTH, day) 51 | // set(HOUR_OF_DAY, hour) 52 | // set(MINUTE, min) 53 | // set(SECOND, sec) 54 | // set(MILLISECOND, ms) 55 | // ] 56 | // } 57 | 58 | // DATE CONVERSIONS /////////////////////////////////////////////////// 59 | 60 | // /** Convert a date to UTC timezone */ 61 | // def static toUTC(Instant date) { 62 | // val cal = date.toCalendar 63 | // date - new Duration(cal.get(DST_OFFSET) + cal.get(ZONE_OFFSET) - cal.get(DST_OFFSET)) 64 | // } 65 | 66 | // /** Convert a date to a specific timezone */ 67 | // def static toTimeZone(Date date, String zone) { 68 | // date.toUTC + new Duration(zone.timeZone.rawOffset) 69 | // } 70 | // 71 | // /** Convert a date to a calendar instance */ 72 | // def static toCalendar(Date date) { 73 | // Calendar.instance => [ time = date ] 74 | // } 75 | 76 | // DATE GETTERS /////////////////////////////////////////////////////// 77 | 78 | // @Deprecated 79 | // def static getCurrentMinute(Date date) { date.mins } 80 | // @Deprecated 81 | // def static getCurrentSecond(Date date) { date.secs } 82 | // @Deprecated 83 | // def static getHourOfDay(Date date) { date.hours24 } 84 | // 85 | // def static int getMs(Date date) { date.toCalendar.get(MILLISECOND) } 86 | // def static int getSecs(Date date) { date.toCalendar.get(SECOND) } 87 | // def static int getMins(Date date) { date.toCalendar.get(MINUTE) } 88 | // def static int getHours24(Date date) { date.toCalendar.get(HOUR_OF_DAY) } 89 | // def static int getDays(Date date) { date.toCalendar.get(DAY_OF_MONTH) } 90 | // def static int getMonths(Date date) { date.toCalendar.get(MONTH) } 91 | // def static int getYearAD(Date date) { date.toCalendar.get(YEAR) } 92 | 93 | // DATE MANIPULATION /////////////////////////////////////////////////// 94 | 95 | // /** Update a date using a calendar object. See the moment(...) methods for example usage. */ 96 | // def static Date withCalendar(Date date, (Calendar)=>void updateFn) { 97 | // val calendar = date.toCalendar 98 | // updateFn.apply(calendar) 99 | // return calendar.time 100 | // } 101 | 102 | // DATE FORMATTING //////////////////////////////////////////////////// 103 | 104 | /** quickly format a date to the standard "yyyy-MM-dd'T'HH:mm:ss" format. */ 105 | def static format(Instant instant) { instant.format(standardDateFormat) } 106 | 107 | /** 108 | * quickly format a date to a specified format. see all formatting options at 109 | * http://docs.oracle.com/javase/7/docs/api/java/text/SimpleDateFormat.html 110 | */ 111 | def static format(Instant instant, String format) { DateTimeFormatter.ofPattern(format).format(instant) } 112 | 113 | // DATE COMPARISONS /////////////////////////////////////////////////// 114 | 115 | /** Return the most recent date */ 116 | def static newest(Instant... instants) { instants.maxSafe } 117 | 118 | /** Return the oldest date */ 119 | def static oldest(Instant... instants) { instants.minSafe } 120 | 121 | /** Return the date nearest to the specified {@code target}, or {@code null} if the iterable is empty. */ 122 | def static nearest(Iterable instants, Instant target) { 123 | instants.filterNull.reduce [ leading, contestant | 124 | if (interval(target, contestant) < interval(target, leading)) contestant 125 | else leading 126 | ] 127 | } 128 | 129 | /** the date is more than [Duration] old */ 130 | def static > (Instant instant, Duration duration) { now - instant > duration } 131 | 132 | /** the date is more or equals than [Duration] old */ 133 | def static >= (Instant instant, Duration duration) { now - instant >= duration } 134 | 135 | /** the date is less than [Duration] old */ 136 | def static < (Instant instant, Duration duration) { now - instant < duration } 137 | 138 | /** the date is less or equals than [Duration] old */ 139 | def static <= (Instant instant, Duration duration) { now - instant <= duration } 140 | 141 | /** Difference between dates, largest first */ 142 | def static - (Instant i1, Instant i2) { Duration.between(i2, i1) } 143 | 144 | /** Absolute difference between two dates. */ 145 | def static interval(Instant i1, Instant i2) { Duration.between(i1, i2).abs } 146 | 147 | // DURATION CREATION //////////////////////////////////////////////////// 148 | 149 | def static nanos(long value) { Duration.ofNanos(value) } 150 | def static ms(long value) { Duration.ofMillis(value) } 151 | def static secs(long value) { Duration.ofSeconds(value) } 152 | def static sec(long value) { value.secs } 153 | def static mins(long value) { Duration.ofMinutes(value) } 154 | def static min(long value) { value.mins } 155 | def static hours(long value) { Duration.ofHours(value) } 156 | def static hour(long value) { value.hours } 157 | def static days(long value) { Duration.ofDays(value) } 158 | def static day(long value) { value.days } 159 | 160 | def static nanos(Long value) { Duration.ofNanos(value) } 161 | def static ms(Long value) { Duration.ofMillis(value) } 162 | def static secs(Long value) { Duration.ofSeconds(value) } 163 | def static sec(Long value) { value.secs } 164 | def static mins(Long value) { Duration.ofMinutes(value) } 165 | def static min(Long value) { value.mins } 166 | def static hours(Long value) { Duration.ofHours(value) } 167 | def static hour(Long value) { value.hours } 168 | def static days(Long value) { Duration.ofDays(value) } 169 | def static day(Long value) { value.days } //def static years(long value) { Duration.of(value, ChronoUnit.YEARS) } 170 | //def static year(long value) { value.years } 171 | 172 | def static nanos(Duration duration) { duration.toNanos } 173 | def static ms(Duration duration) { duration.toMillis } 174 | def static secs(Duration duration) { duration.toMillis / 1000 } 175 | def static mins(Duration duration) { duration.toMinutes } 176 | def static hours(Duration duration) { duration.toHours } 177 | def static days(Duration duration) { duration.toDays } 178 | def static years(Duration duration) { duration.toDays / 365 } 179 | 180 | // DURATION MANIPULATION //////////////////////////////////////////////// 181 | 182 | def static + (Duration d1, Duration d2) { d1.plus(d2) } 183 | def static - (Duration d1, Duration d2) { d1.minus(d2) } 184 | def static / (Duration d1, Duration d2) { d1.toMillis / d2.toMillis } 185 | def static / (Duration d1, int n) { d1.dividedBy(n) } 186 | def static * (int n, Duration d1) { d1.multipliedBy(n) } 187 | def static * (Duration d1, int n) { n * d1 } 188 | 189 | // DURATION COMPARISON ////////////////////////////////////////////////// 190 | 191 | def static > (Duration d1, Duration d2) { d1.compareTo(d2) > 0 } 192 | def static >= (Duration d1, Duration d2) { d1.compareTo(d2) >= 0 } 193 | def static < (Duration d1, Duration d2) { d1.compareTo(d2) < 0 } 194 | def static <= (Duration d1, Duration d2) { d1.compareTo(d2) <= 0 } 195 | def static min(Duration d1, Duration d2) { minSafe(d1, d2) } 196 | def static max(Duration d1, Duration d2) { maxSafe(d1, d2) } 197 | 198 | // DATE CHANGING USING DURATIONS //////////////////////////////////////// 199 | 200 | def static add(Instant instant, TemporalAmount amount) { instant.plus(amount) } 201 | def static subtract(Instant instant, TemporalAmount amount) { instant.minus(amount) } 202 | def static + (Instant instant, TemporalAmount amount) { add(instant, amount) } 203 | def static - (Instant instant, TemporalAmount amount) { subtract(instant, amount) } 204 | 205 | } 206 | -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/ThrowableExtensions.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import static extension nl.kii.util.IterableExtensions.* 4 | import com.google.common.base.Throwables 5 | 6 | class ThrowableExtensions { 7 | 8 | def static getRootCause(Throwable t) { 9 | Throwables.getRootCause(t) 10 | } 11 | 12 | /** 13 | * Removes stacktrace elements from a stacktrace. 14 | * Only tracktrace elements that do not match the filtered regexps pass. 15 | */ 16 | def static Iterable clean(StackTraceElement[] stacktrace, String... filtered) { 17 | if(filtered === null) return stacktrace 18 | // allow only those traces whose classname match none of the filtered items 19 | stacktrace.filter [ trace | 20 | none( filtered.map[trace.className.matches(it)]) 21 | ] 22 | } 23 | 24 | /** 25 | * Removes stacktrace elements from any Throwable and its cause tree. 26 | * Only tracktrace elements that do not match the filtered regexps pass. 27 | * Modifies the existing throwable, and also returns it for chaining. 28 | */ 29 | def static Throwable clean(Throwable t, String... filtered) { 30 | t.stackTrace = t.stackTrace.clean(filtered) 31 | if(t.cause !== null) t.cause.clean(filtered) 32 | t 33 | } 34 | 35 | /** Formats a throwable into a nicely formatted string */ 36 | def static String format(Throwable it, String message) ''' 37 | «message» 38 | «format(it)» 39 | ''' 40 | 41 | /** Formats a throwable into a nicely formatted string, with an optional message. */ 42 | def static String format(Throwable it) ''' 43 | «class.simpleName» «it.message»: 44 | «it.message» 45 | «FOR trace : stackTrace» 46 | at «trace» 47 | «ENDFOR» 48 | «val root = rootCause» 49 | «IF root != it» 50 | Caused by: «root.format» 51 | «ENDIF» 52 | ''' 53 | 54 | /** returns true if the error is of the type or the cause of the error is of the type. */ 55 | def static matches(Throwable err, Class type) { 56 | type.isAssignableFrom(err.class) || (if(err.cause !== null) type.isAssignableFrom(err.cause?.class) else false) 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /core/src/main/java/nl/kii/util/TupleExtensions.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import org.eclipse.xtend.lib.annotations.Data 4 | import java.util.List 5 | 6 | class TupleExtensions { 7 | 8 | def static tuple(A first) { 9 | new Tuple1(first) 10 | } 11 | 12 | def static tuple(A first, B second) { 13 | new Tuple2(first, second) 14 | } 15 | 16 | def static tuple(A first, B second, C third) { 17 | new Tuple3(first, second, third) 18 | } 19 | 20 | def static tuple(A first, B second, C third, D fourth) { 21 | new Tuple4(first, second, third, fourth) 22 | } 23 | 24 | } 25 | 26 | interface Tuple { 27 | def List getEntries() 28 | } 29 | 30 | @Data class Tuple1 implements Tuple { 31 | A first 32 | 33 | def $0() { first } 34 | 35 | override getEntries() { 36 | #[ $0 ] 37 | } 38 | 39 | override toString() '''«entries»''' 40 | } 41 | 42 | @Data class Tuple2 implements Tuple { 43 | A first 44 | B second 45 | 46 | def $0() { first } 47 | def $1() { second } 48 | 49 | override getEntries() { 50 | #[ $0, $1 ] 51 | } 52 | 53 | override toString() '''«entries»''' 54 | } 55 | 56 | @Data class Tuple3 implements Tuple { 57 | A first 58 | B second 59 | C third 60 | 61 | def $0() { first } 62 | def $1() { second } 63 | def $2() { third } 64 | 65 | override getEntries() { 66 | #[ $0, $1, $2 ] 67 | } 68 | 69 | override toString() '''«entries»''' 70 | } 71 | 72 | @Data class Tuple4 implements Tuple { 73 | A first 74 | B second 75 | C third 76 | D fourth 77 | 78 | def $0() { first } 79 | def $1() { second } 80 | def $2() { third } 81 | def $3() { fourth } 82 | 83 | override getEntries() { 84 | #[ $0, $1, $2, $3 ] 85 | } 86 | 87 | override toString() '''«entries»''' 88 | } 89 | -------------------------------------------------------------------------------- /core/src/test/java/nl/kii/util/TestCached.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import org.eclipse.xtend.lib.annotations.Data 4 | import org.junit.Test 5 | 6 | import static org.junit.Assert.* 7 | 8 | import static extension nl.kii.util.DateExtensions.* 9 | 10 | class TestCached { 11 | 12 | val cache = new Cached(1.min) [ fetchUser ] 13 | int fetches = 0 14 | 15 | @Test 16 | def void testCached() { 17 | cache.apply 18 | cache.apply 19 | cache.apply 20 | assertEquals(1, fetches) 21 | } 22 | 23 | def fetchUser() { 24 | fetches++ 25 | new User('emre', 24) 26 | } 27 | 28 | } 29 | 30 | @Data 31 | class User { 32 | String name 33 | int age 34 | } 35 | -------------------------------------------------------------------------------- /core/src/test/java/nl/kii/util/TestConversionSwitch.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import org.junit.Test 4 | 5 | import static org.junit.Assert.* 6 | 7 | import static extension nl.kii.util.IterableExtensions.* 8 | 9 | class TestConversionSwitch { 10 | 11 | @Test 12 | def void testSwitch() { 13 | 14 | val list = #[1, 2, 3] 15 | val map = #{ 1->'A', 2->'B' } 16 | 17 | switch list { 18 | case list.isListOf(Integer): {} 19 | case list.isListOf(String): fail('not a list of a string') 20 | default: fail('no match found for list') 21 | } 22 | 23 | switch map { 24 | case map.isMapOf(Integer, String): {} 25 | case map.isMapOf(String, String): fail('not map of string->string') 26 | default: fail('no match found for map') 27 | } 28 | 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /core/src/test/java/nl/kii/util/TestDateExtensions.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import org.junit.Test 4 | 5 | import static extension nl.kii.util.DateExtensions.* 6 | import static org.junit.Assert.* 7 | import java.util.TimeZone 8 | 9 | class TestDateExtensions { 10 | 11 | @Test 12 | def void test() { 13 | val x = now - 5.mins - 3.years 14 | val y = now + 4.days 15 | assertTrue((y - x) > 3.days) 16 | } 17 | 18 | @Test 19 | def void testUTCZone() { 20 | val originalTimeZone = TimeZone.^default 21 | TimeZone.^default = TimeZone.getTimeZone( 'UTC' ) 22 | 23 | assertEquals(0, (now - now.toUTC).hours) 24 | assertEquals(-1, (now - now.toTimeZone('GMT+1')).hours) 25 | 26 | TimeZone.^default = originalTimeZone 27 | } 28 | 29 | @Test 30 | def void testPeriodToString() { 31 | val period = 3.years + 2.days + 7.hours + 9.mins + 20.secs + 1.ms 32 | assertEquals('3 years, 2 days, 7 hours, 9 minutes, 20 seconds, 1 milliseconds', period.toString) 33 | } 34 | 35 | @Test 36 | def void testComparability() { 37 | val periods = #[ 1.day, 2.years, 10.ms, 100.secs, 1.min ] 38 | 39 | assertEquals( 40 | #[ 10.ms, 1.min, 100.secs, 1.day, 2.years ], 41 | periods.sort 42 | ) 43 | } 44 | 45 | @Test 46 | def void testNearest() { 47 | val now = now 48 | 49 | val d1 = now 50 | val d2 = now - 10.mins 51 | val d3 = now - 20.mins 52 | val d4 = now - 30.mins 53 | val d5 = now - 40.mins 54 | 55 | val target = now - 22.mins 56 | 57 | val dates = #[ d1, d5, d2, d3, d4 ] 58 | 59 | assertEquals(dates.nearest(target), d3) 60 | 61 | 62 | assertEquals(#[].nearest(target), null) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /core/src/test/java/nl/kii/util/TestIterableExtensions.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import org.junit.Test 4 | 5 | import static org.junit.Assert.* 6 | 7 | import static extension nl.kii.util.IterableExtensions.* 8 | 9 | class TestIterableExtensions { 10 | 11 | @Test 12 | def void testFilterAll() { 13 | val users = #[ 14 | new User('Henk', 55), 15 | new User('Koos', 60), 16 | new User('Christian', 45), 17 | new User('Eli', 39) 18 | ] 19 | 20 | val investors = users.filterAll(#['Henk', 'Koos']) [ name ] 21 | assertArrayEquals(#['Henk', 'Koos'], investors.map[name]) 22 | val notInvestors = users.filterNone(#['Henk', 'Koos']) [ name ] 23 | assertArrayEquals(#['Christian', 'Eli'], notInvestors.map[name]) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /core/src/test/java/nl/kii/util/TestMapExtensions.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import org.junit.Test 4 | 5 | import static extension nl.kii.util.MapExtensions.* 6 | import static extension org.junit.Assert.* 7 | 8 | class TestMapExtensions { 9 | 10 | @Test 11 | def void testMapMapping() { 12 | #{1->5, 2->6} 13 | .map [ k, v | k+1 -> v+1] 14 | .assertEquals(#{2->6, 3->7}) 15 | } 16 | 17 | @Test 18 | def void testImmutableMapOperations() { 19 | val map = #{ 'key1' -> 'value1', 'key2' -> 'value2' } 20 | val pair = 'key3' -> 'value3' 21 | 22 | /** Addition should succeed */ 23 | assertEquals(map + pair, #{ 'key1' -> 'value1', 'key2' -> 'value2', 'key3' -> 'value3' }) 24 | 25 | /** Old map should be untouched after addition */ 26 | assertEquals(map, #{ 'key1' -> 'value1', 'key2' -> 'value2' }) 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /core/src/test/java/nl/kii/util/TestMultiDateFormat.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import java.text.SimpleDateFormat 4 | import java.util.Date 5 | import java.util.Locale 6 | import java.util.TimeZone 7 | import org.junit.Test 8 | 9 | import static org.junit.Assert.* 10 | 11 | class TestMultiDateFormat { 12 | val someDateStrings = #[ 13 | 'Thu, 30 Oct 2014 15:35:13 +01:00', 14 | 'Thu, 30 Oct 2014 15:35:13 +0100', 15 | 'Thu, 30 Oct 2014 15:35:13 CET' 16 | ] 17 | 18 | @Test 19 | def void textMultipleInputs() { 20 | someDateStrings.forEach [ 21 | val date = toDate('EEE, d MMM yyyy HH:mm:ss z; EEE, d MMM yyyy HH:mm:ss X') 22 | println(date) 23 | assertEquals(new SimpleDateFormat('dd-MM-yyyy').format(date), '30-10-2014') 24 | ] 25 | } 26 | 27 | 28 | @Test 29 | def void testSingleInput() { 30 | val originalTimeZone = TimeZone.^default 31 | TimeZone.^default = TimeZone.getTimeZone( 'UTC' ) 32 | 33 | val date = 'Thu, 30 Oct 2014 15:35:13 +0100'.toDate('EEE, d MMM yyyy HH:mm:ss z') 34 | assertEquals('Thu Oct 30 14:35:13 UTC 2014', date.toString) 35 | 36 | TimeZone.^default = originalTimeZone 37 | } 38 | 39 | 40 | def Date toDate(String s, String dateFormat) { 41 | new MultiDateFormat(dateFormat, Locale.ENGLISH).parse(s) 42 | } 43 | } -------------------------------------------------------------------------------- /core/src/test/java/nl/kii/util/TestPartialURL.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | import static extension org.junit.Assert.* 3 | import org.junit.Test 4 | import static extension nl.kii.util.PartialURL.* 5 | 6 | class TestPartialURL { 7 | 8 | @Test 9 | def void testURLBuilding() { 10 | val url = new PartialURL => [ 11 | protocol = 'http' 12 | assertFalse(valid) 13 | domain = 'cnn.com' 14 | assertTrue(valid) 15 | path = '/index.html' 16 | hash = 'test' 17 | ] 18 | assertEquals('http://cnn.com/index.html#test', url.toString) 19 | } 20 | 21 | @Test 22 | def void testURLParsing() { 23 | val text = 'http://www.test.com/somepath/to/nowhere?p1=10&p2=hello#somehash:somewhere' 24 | val url = new PartialURL(text) 25 | assertEquals(text, url.toString) 26 | assertEquals('10', url.parameters.get('p1')) 27 | assertEquals('hello', url.parameters.get('p2')) 28 | assertTrue(url.valid) 29 | } 30 | 31 | @Test 32 | def void testBadURL() { 33 | val text = 'just a test' 34 | assertFalse(text.valid) 35 | } 36 | 37 | @Test 38 | def void testSupportsNoValueParameters() { 39 | val url = new PartialURL('/twitter/timeline?telegraaf') 40 | assertFalse(url.parameters.empty) 41 | assertEquals('telegraaf', url.parameters.keySet.head) 42 | } 43 | 44 | @Test 45 | def void testFullPath() { 46 | val url = new PartialURL('https://api.twitter.com/1/statuses/update.json?include_entities=true') 47 | assertEquals('https://api.twitter.com/1/statuses/update.json', url.fullPath) 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /core/src/test/java/nl/kii/util/TestStringExtensions.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | import static extension nl.kii.util.StringExtensions.* 3 | import org.junit.Test 4 | import static extension org.junit.Assert.* 5 | 6 | class TestStringExtensions { 7 | 8 | @Test 9 | def void testFill() { 10 | val message = 'Hello {}, this is your friendly {}.'.fill('Christian', 'xtend code') 11 | println(message) 12 | assertEquals('Hello Christian, this is your friendly xtend code.', message) 13 | } 14 | 15 | @Test 16 | def void testFillWithPositions() { 17 | val message = 'Hello {1}, this is your {} {2}.'.fill('Christian', 'xtend code', 'friendly') 18 | println(message) 19 | assertEquals('Hello Christian, this is your friendly xtend code.', message) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /core/src/test/java/nl/kii/util/TestTemporalExtensions.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import org.junit.Test 4 | 5 | import static org.junit.Assert.* 6 | 7 | import static extension nl.kii.util.TemporalExtensions.* 8 | 9 | class TestTemporalExtensions { 10 | 11 | @Test 12 | def void test() { 13 | val d1 = 2.days + 10.mins + 30.secs 14 | val d2 = 48.hours + 600.secs + 30_000.ms 15 | assertEquals(d1, d2) 16 | } 17 | 18 | // @Test 19 | // def void testUTCZone() { 20 | // val originalTimeZone = TimeZone.^default 21 | // TimeZone.^default = TimeZone.getTimeZone( 'UTC' ) 22 | // 23 | // assertEquals(0, (now - now.toUTC).hours) 24 | // assertEquals(-1, (now - now.toTimeZone('GMT+1')).hours) 25 | // 26 | // TimeZone.^default = originalTimeZone 27 | // } 28 | 29 | // @Test 30 | // def void testPeriodToString() { 31 | // val period = 2.days + 7.hours + 9.mins + 20.secs + 1.ms 32 | // println(period) 33 | // assertEquals('3 years, 2 days, 7 hours, 9 minutes, 20 seconds, 1 milliseconds', period.toString) 34 | // } 35 | 36 | @Test 37 | def void testComparability() { 38 | val periods = #[ 1.day, 10.ms, 100.secs, 1.min ] 39 | 40 | assertEquals( 41 | #[ 10.ms, 1.min, 100.secs, 1.day ], 42 | periods.sort 43 | ) 44 | } 45 | 46 | @Test 47 | def void testNearest() { 48 | val now = now 49 | 50 | val d1 = now 51 | val d2 = now - 10.mins 52 | val d3 = now - 20.mins 53 | val d4 = now - 30.mins 54 | val d5 = now - 40.mins 55 | 56 | val target = now - 22.mins 57 | 58 | val dates = #[ d1, d5, d2, d3, d4 ] 59 | 60 | assertEquals(dates.nearest(target), d3) 61 | 62 | 63 | assertEquals(#[].nearest(target), null) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /core/src/test/java/nl/kii/util/TestThrowableExtensions.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import static org.junit.Assert.* 4 | 5 | import static extension nl.kii.util.ThrowableExtensions.* 6 | 7 | class TestThrowableExtensions { 8 | 9 | // @Test // FIX: breaks in gradle test 10 | def void testCleanStacktrace() { 11 | try { 12 | throw new Exception 13 | } catch(Throwable t) { 14 | try { 15 | throw new Exception(t) 16 | } catch(Exception e) { 17 | // clean all the junit and reflect stuff from the exception stacktrace 18 | val s = e.clean('.+junit.+', '.+reflect.+') 19 | // now just the thrown location is left in the trace 20 | assertEquals(1, s.stackTrace.length) 21 | assertEquals(1, s.cause.stackTrace.length) 22 | println(s.stackTrace.get(0).toString) 23 | println(s.cause.stackTrace.get(0).toString) 24 | } 25 | } 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | group=com.kimengi.util 2 | version=11.0.0 3 | 4 | javaVersion=1.8 5 | xtendVersion=2.24.0 6 | junitVersion=4.13 7 | slf4jVersion=1.7.16 8 | logbackVersion=1.1.5 9 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include 'core' 2 | include 'test' 3 | include 'annotations' 4 | include 'annotations-test' 5 | 6 | -------------------------------------------------------------------------------- /test/src/main/java/nl/kii/util/JUnitExtensions.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util 2 | 3 | import com.google.common.util.concurrent.AtomicDouble 4 | import java.util.List 5 | import java.util.concurrent.atomic.AtomicBoolean 6 | import java.util.concurrent.atomic.AtomicInteger 7 | import java.util.concurrent.atomic.AtomicLong 8 | import java.util.concurrent.atomic.AtomicReference 9 | import org.junit.Assert 10 | 11 | import static extension org.junit.Assert.* 12 | 13 | class JUnitExtensions { 14 | 15 | def static <=> (T value, T expected) { 16 | assertEquals(expected, value) 17 | } 18 | 19 | def static <=> (List value, List expected) { 20 | assertArrayEquals(expected, value) 21 | } 22 | 23 | def static void assertNone(Opt option) { 24 | option.hasSome.assertFalse 25 | } 26 | 27 | def static void assertSome(Opt option) { 28 | option.hasSome.assertTrue 29 | } 30 | 31 | def static void assertSome(Opt option, T value) { 32 | option.assertSome 33 | value.assertEquals(option.value) 34 | } 35 | 36 | def static fail(String message) { 37 | Assert.fail(message) 38 | } 39 | 40 | def static <=> (AtomicReference value, T expected) { 41 | assertEquals(expected, value.get) 42 | } 43 | 44 | def static <=> (AtomicInteger value, T expected) { 45 | assertEquals(expected, value.get) 46 | } 47 | 48 | def static <=> (AtomicDouble value, T expected) { 49 | assertEquals(expected, value.get) 50 | } 51 | 52 | def static <=> (AtomicBoolean value, T expected) { 53 | assertEquals(expected, value.get) 54 | } 55 | 56 | def static <=> (AtomicLong value, T expected) { 57 | assertEquals(expected, value.get) 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /test/src/test/java/nl/kii/util/test/TestXtendTools.xtend: -------------------------------------------------------------------------------- 1 | package nl.kii.util.test 2 | 3 | import java.io.Closeable 4 | import java.io.IOException 5 | import java.util.Date 6 | import nl.kii.util.Log 7 | import nl.kii.util.None 8 | import nl.kii.util.Opt 9 | import nl.kii.util.annotation.NamedParams 10 | import org.eclipse.xtend.lib.annotations.Data 11 | import org.junit.Test 12 | 13 | import static nl.kii.util.CloseableExtensions.* 14 | 15 | import static extension nl.kii.util.IterableExtensions.* 16 | import static extension nl.kii.util.JUnitExtensions.* 17 | import static extension nl.kii.util.LogExtensions.* 18 | import static extension nl.kii.util.OptExtensions.* 19 | import static extension nl.kii.util.SetOperationExtensions.* 20 | import static extension org.junit.Assert.* 21 | import static extension org.slf4j.LoggerFactory.* 22 | import nl.kii.util.annotation.Default 23 | 24 | interface Greeter { 25 | def void sayGreeting(String name) 26 | } 27 | 28 | class TestXtendTools { 29 | 30 | extension Log logger = class.logger.wrapper 31 | 32 | def loadUser(long userId, (String)=>void onLoad) { 33 | Thread.sleep(3000) 34 | onLoad.apply('''user-«userId»'''.toString) 35 | } 36 | 37 | @Test def void testAttempt() { 38 | attempt [ none ].assertNone 39 | attempt [ throw new Exception ].assertNone 40 | attempt [ 'hello'].assertSome('hello') 41 | } 42 | 43 | @Test def void testIfValue() { 44 | // if that returns an optional value 45 | ifTrue(true) ['hello'].assertSome('hello') 46 | ifTrue(false) ['hello'].assertNone 47 | } 48 | 49 | @Test def void testOr() { 50 | // direct ors 51 | some('hi') 52 | .or('hello') 53 | .assertEquals('hi') 54 | none 55 | .or('hello') 56 | .assertEquals('hello') 57 | // ors with a function for getting the result 58 | some('hi') 59 | .or [ 'a' + 'b'] 60 | .assertEquals('hi') 61 | none 62 | .or [ 'a' + 'b'] 63 | .assertEquals('ab') 64 | // ors that throw an exception via a function 65 | try { 66 | some('hi').orThrow [ new Exception ] 67 | } catch(Exception c) { 68 | some('hello') 69 | }.assertSome('hi') 70 | // same for none 71 | try { 72 | (new None).orThrow [ new Exception ] 73 | } catch(Exception c) { 74 | some('hello') 75 | }.assertSome('hello') 76 | } 77 | 78 | @Test def void testIn() { 79 | 2.in(#[1, 2, 3]).assertTrue 80 | 2.in(1, 2, 3).assertTrue 81 | 6.in(#[1, 2, 3]).assertFalse 82 | 6.in(1, 2, 3).assertFalse 83 | null.in(1, 2, 3).assertFalse 84 | false.in(false).assertTrue 85 | val u1 = new User('a', 12) 86 | val u2 = new User('a', 12) 87 | val u3 = new User('b', 13) 88 | u2.in(#[u1, u3] as Iterable).assertTrue 89 | } 90 | 91 | // @Test def void testFilters() { 92 | // #{some(1), none, some(2), none, none}.filterEmpty.length.assertEquals(2) 93 | // #{1, 2, 3, 1}.distinct.length.assertEquals(3) 94 | // } 95 | 96 | // @Test def void testConversions() { 97 | // // toList 98 | // #{1, 2, 3}.toList.length.assertEquals(3) 99 | // // null.toList.length.assertEquals(0) 100 | // // toSet 101 | // #[1, 2, 3].toSet.length.assertEquals(3) 102 | // // null.toSet.length.assertEquals(0) 103 | // // toPairs 104 | // #{'john'->23, 'mary'->45}.toPairs.get(1) => [ 105 | // key.assertEquals('mary') 106 | // value.assertEquals(45) 107 | // ] 108 | // // toMap 109 | // #['john'->23, 'mary'->45].toMap.get('mary').assertEquals(45) 110 | // } 111 | 112 | @Test def void iteratorFunctions() { 113 | val users = #[new User('john', 23), new User('mary', 45), new User('jim', 23)] 114 | 115 | // groupBy 116 | users 117 | .groupBy [ age ] 118 | .get(23) 119 | .length 120 | .assertEquals(2) // two people of age 23 121 | // countBy 122 | users 123 | .countBy [ age ] 124 | .get(users.findFirst[ age == 23 ]) 125 | .assertEquals(2) // shortcut of above, same result 126 | // count 127 | #[1, 3, 3, 3, 3, 4] 128 | .count 129 | .get(3) 130 | .assertEquals(4) 131 | 132 | // distinct 133 | #[ 1,2,3,3,3,4 ] 134 | .distinct 135 | .assertEquals(#[ 1,2,3,4 ]) 136 | 137 | // distinctBy 138 | users 139 | .distinctBy [ age ] 140 | .length 141 | .assertEquals(2) 142 | 143 | // index 144 | users 145 | .index [ age ] 146 | .get(45) 147 | .name.assertEquals('mary') 148 | // attemptToMap 149 | // users 150 | // .attemptMap [ 1 / (age - 45) ] // throws division by 0 at mary 151 | // .filterEmpty // filter the empty result 152 | // .length.assertEquals(2) // only two results left 153 | } 154 | 155 | @Test 156 | def void testSetOperations() { 157 | val testingSet = #[ #[ 1, 2, 3 ], #[ 2, 3, 4 ] ] 158 | 159 | assertEquals( 160 | #[ 1, 2, 3, 4 ], 161 | testingSet.union 162 | ) 163 | 164 | assertEquals( 165 | #[ 2, 3 ], 166 | testingSet.intersection 167 | ) 168 | 169 | assertEquals( 170 | #[ 1 ], 171 | testingSet.subtraction 172 | ) 173 | 174 | assertEquals( 175 | #[ 1, 4 ], 176 | testingSet.difference 177 | ) 178 | } 179 | 180 | @Test def void testUsing() { 181 | val closeable = new Readable 182 | // it should give back the result of the function 183 | closeable.open 184 | attemptUsing(closeable) [ hello ].assertSome('hello, I am open!') 185 | closeable.isClosed.assertTrue 186 | // if there is an error, it should still close the closeable 187 | closeable.open 188 | attemptUsing(closeable) [ 189 | closeable.isClosed.assertFalse 190 | throw new Exception 191 | ] 192 | closeable.isClosed.assertTrue 193 | } 194 | 195 | @Test def void testSum() { 196 | #[1, 3, 2, 5, 7].sum.assertEquals(18, 0) 197 | } 198 | 199 | @Test def void testAvg() { 200 | #[1, 2, 3, 4].average.assertEquals(2.5, 0) 201 | } 202 | 203 | @Test def void testMedian() { 204 | #[1D, 2D, 3D, 4D].median.assertEquals(2.5, 0) 205 | #[1D, 2D, 3D].median.assertEquals(2D, 0) 206 | #[1D, 2D, 3D, 3.5D, 4D, 4D, 300D].median.assertEquals(3.5, 0) 207 | #[3D, 21D, 1D].median.assertEquals(3D, 0) 208 | } 209 | 210 | @Test def void testLastN() { 211 | #[1, 2, 3, 4, 5].last(3).list.assertEquals(#[3, 4, 5]) 212 | #[1, 2, 3, 4].last(6).list.assertEquals(#[1, 2, 3, 4]) 213 | #[1, 2, 3].last(0).list.assertEquals(#[ ]) 214 | #[ ].last(1).list.assertEquals(#[ ]) 215 | } 216 | 217 | @Test def void testLogging() { 218 | error('hello error!', new Exception('ooo!')) 219 | } 220 | 221 | @Test 222 | def void testFlatten() { 223 | // test simple value opt unwrapping 224 | val x = 5.option.option 225 | x.flatten.hasSome.assertTrue 226 | // errors should be propagated when flattening 227 | val Opt> e = err(new Exception('test')).option 228 | // println(e) 229 | e.flatten.hasError.assertTrue 230 | } 231 | 232 | @NamedParams 233 | def String addSomeThings(String username, @Default('Hey') String message, int age, Date birthday) ''' 234 | hello «username», «message». You are «age» years old and your birthday is at «birthday». 235 | ''' 236 | 237 | @Test 238 | def void testNamedParams() { 239 | val message = addSomeThings [ 240 | username = 'Johnny' 241 | age = 30 242 | birthday = new Date 243 | ] 244 | println(message) 245 | } 246 | 247 | } 248 | 249 | @Data class User { 250 | 251 | new(String name, int age) { 252 | this.name = name 253 | this.age = age 254 | } 255 | 256 | public String name 257 | public int age 258 | } 259 | 260 | class Readable implements Closeable { 261 | 262 | public var boolean isClosed = true 263 | 264 | def open() { 265 | isClosed = false 266 | } 267 | 268 | def hello() { 269 | if(isClosed) throw new Exception('cannot hello when closed') 270 | 'hello, I am open!' 271 | } 272 | 273 | override close() throws IOException { 274 | isClosed = true 275 | } 276 | 277 | } 278 | --------------------------------------------------------------------------------