├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── pom.xml └── src ├── main └── java │ └── com │ └── github │ └── phantomthief │ ├── model │ └── builder │ │ ├── ModelBuilder.java │ │ ├── context │ │ ├── BuildContext.java │ │ └── impl │ │ │ └── SimpleBuildContext.java │ │ ├── impl │ │ ├── LazyBuilder.java │ │ └── SimpleModelBuilder.java │ │ └── util │ │ └── MergeUtils.java │ └── view │ └── mapper │ ├── ViewMapper.java │ └── impl │ ├── DefaultViewMapperImpl.java │ ├── ForwardingViewMapper.java │ └── OverrideViewMapper.java └── test ├── java └── com │ └── github │ └── phantomthief │ └── model │ └── builder │ ├── ModelBuilderConflictTest.java │ ├── ModelBuilderTest.java │ ├── TestBuildContext.java │ ├── model │ ├── Comment.java │ ├── Fake.java │ ├── HasId.java │ ├── HasUser.java │ ├── Post.java │ ├── SubUser.java │ └── User.java │ └── util │ └── ToStringUtils.java └── resources └── logback.xml /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | .idea 3 | *.iml 4 | 5 | # Mobile Tools for Java (J2ME) 6 | .mtj.tmp/ 7 | 8 | # Package Files # 9 | *.jar 10 | *.war 11 | *.ear 12 | 13 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 14 | hs_err_pid* 15 | /target/ 16 | 17 | .project 18 | .settings 19 | .classpath 20 | 21 | .DS_Store 22 | 23 | .README.md.html 24 | 25 | pom.xml.releaseBackup 26 | release.properties 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk8 4 | after_success: 5 | - mvn clean test jacoco:report coveralls:report 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The Artistic License 2.0 2 | 3 | Copyright (c) 2014 w.vela 4 | 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | This license establishes the terms under which a given free software 11 | Package may be copied, modified, distributed, and/or redistributed. 12 | The intent is that the Copyright Holder maintains some artistic 13 | control over the development of that Package while still keeping the 14 | Package available as open source and free software. 15 | 16 | You are always permitted to make arrangements wholly outside of this 17 | license directly with the Copyright Holder of a given Package. If the 18 | terms of this license do not permit the full use that you propose to 19 | make of the Package, you should contact the Copyright Holder and seek 20 | a different licensing arrangement. 21 | 22 | Definitions 23 | 24 | "Copyright Holder" means the individual(s) or organization(s) 25 | named in the copyright notice for the entire Package. 26 | 27 | "Contributor" means any party that has contributed code or other 28 | material to the Package, in accordance with the Copyright Holder's 29 | procedures. 30 | 31 | "You" and "your" means any person who would like to copy, 32 | distribute, or modify the Package. 33 | 34 | "Package" means the collection of files distributed by the 35 | Copyright Holder, and derivatives of that collection and/or of 36 | those files. A given Package may consist of either the Standard 37 | Version, or a Modified Version. 38 | 39 | "Distribute" means providing a copy of the Package or making it 40 | accessible to anyone else, or in the case of a company or 41 | organization, to others outside of your company or organization. 42 | 43 | "Distributor Fee" means any fee that you charge for Distributing 44 | this Package or providing support for this Package to another 45 | party. It does not mean licensing fees. 46 | 47 | "Standard Version" refers to the Package if it has not been 48 | modified, or has been modified only in ways explicitly requested 49 | by the Copyright Holder. 50 | 51 | "Modified Version" means the Package, if it has been changed, and 52 | such changes were not explicitly requested by the Copyright 53 | Holder. 54 | 55 | "Original License" means this Artistic License as Distributed with 56 | the Standard Version of the Package, in its current version or as 57 | it may be modified by The Perl Foundation in the future. 58 | 59 | "Source" form means the source code, documentation source, and 60 | configuration files for the Package. 61 | 62 | "Compiled" form means the compiled bytecode, object code, binary, 63 | or any other form resulting from mechanical transformation or 64 | translation of the Source form. 65 | 66 | 67 | Permission for Use and Modification Without Distribution 68 | 69 | (1) You are permitted to use the Standard Version and create and use 70 | Modified Versions for any purpose without restriction, provided that 71 | you do not Distribute the Modified Version. 72 | 73 | 74 | Permissions for Redistribution of the Standard Version 75 | 76 | (2) You may Distribute verbatim copies of the Source form of the 77 | Standard Version of this Package in any medium without restriction, 78 | either gratis or for a Distributor Fee, provided that you duplicate 79 | all of the original copyright notices and associated disclaimers. At 80 | your discretion, such verbatim copies may or may not include a 81 | Compiled form of the Package. 82 | 83 | (3) You may apply any bug fixes, portability changes, and other 84 | modifications made available from the Copyright Holder. The resulting 85 | Package will still be considered the Standard Version, and as such 86 | will be subject to the Original License. 87 | 88 | 89 | Distribution of Modified Versions of the Package as Source 90 | 91 | (4) You may Distribute your Modified Version as Source (either gratis 92 | or for a Distributor Fee, and with or without a Compiled form of the 93 | Modified Version) provided that you clearly document how it differs 94 | from the Standard Version, including, but not limited to, documenting 95 | any non-standard features, executables, or modules, and provided that 96 | you do at least ONE of the following: 97 | 98 | (a) make the Modified Version available to the Copyright Holder 99 | of the Standard Version, under the Original License, so that the 100 | Copyright Holder may include your modifications in the Standard 101 | Version. 102 | 103 | (b) ensure that installation of your Modified Version does not 104 | prevent the user installing or running the Standard Version. In 105 | addition, the Modified Version must bear a name that is different 106 | from the name of the Standard Version. 107 | 108 | (c) allow anyone who receives a copy of the Modified Version to 109 | make the Source form of the Modified Version available to others 110 | under 111 | 112 | (i) the Original License or 113 | 114 | (ii) a license that permits the licensee to freely copy, 115 | modify and redistribute the Modified Version using the same 116 | licensing terms that apply to the copy that the licensee 117 | received, and requires that the Source form of the Modified 118 | Version, and of any works derived from it, be made freely 119 | available in that license fees are prohibited but Distributor 120 | Fees are allowed. 121 | 122 | 123 | Distribution of Compiled Forms of the Standard Version 124 | or Modified Versions without the Source 125 | 126 | (5) You may Distribute Compiled forms of the Standard Version without 127 | the Source, provided that you include complete instructions on how to 128 | get the Source of the Standard Version. Such instructions must be 129 | valid at the time of your distribution. If these instructions, at any 130 | time while you are carrying out such distribution, become invalid, you 131 | must provide new instructions on demand or cease further distribution. 132 | If you provide valid instructions or cease distribution within thirty 133 | days after you become aware that the instructions are invalid, then 134 | you do not forfeit any of your rights under this license. 135 | 136 | (6) You may Distribute a Modified Version in Compiled form without 137 | the Source, provided that you comply with Section 4 with respect to 138 | the Source of the Modified Version. 139 | 140 | 141 | Aggregating or Linking the Package 142 | 143 | (7) You may aggregate the Package (either the Standard Version or 144 | Modified Version) with other packages and Distribute the resulting 145 | aggregation provided that you do not charge a licensing fee for the 146 | Package. Distributor Fees are permitted, and licensing fees for other 147 | components in the aggregation are permitted. The terms of this license 148 | apply to the use and Distribution of the Standard or Modified Versions 149 | as included in the aggregation. 150 | 151 | (8) You are permitted to link Modified and Standard Versions with 152 | other works, to embed the Package in a larger work of your own, or to 153 | build stand-alone binary or bytecode versions of applications that 154 | include the Package, and Distribute the result without restriction, 155 | provided the result does not expose a direct interface to the Package. 156 | 157 | 158 | Items That are Not Considered Part of a Modified Version 159 | 160 | (9) Works (including, but not limited to, modules and scripts) that 161 | merely extend or make use of the Package, do not, by themselves, cause 162 | the Package to be a Modified Version. In addition, such works are not 163 | considered parts of the Package itself, and are not subject to the 164 | terms of this license. 165 | 166 | 167 | General Provisions 168 | 169 | (10) Any use, modification, and distribution of the Standard or 170 | Modified Versions is governed by this Artistic License. By using, 171 | modifying or distributing the Package, you accept this license. Do not 172 | use, modify, or distribute the Package, if you do not accept this 173 | license. 174 | 175 | (11) If your Modified Version has been derived from a Modified 176 | Version made by someone other than you, you are nevertheless required 177 | to ensure that your Modified Version complies with the requirements of 178 | this license. 179 | 180 | (12) This license does not grant you the right to use any trademark, 181 | service mark, tradename, or logo of the Copyright Holder. 182 | 183 | (13) This license includes the non-exclusive, worldwide, 184 | free-of-charge patent license to make, have made, use, offer to sell, 185 | sell, import and otherwise transfer the Package with respect to any 186 | patent claims licensable by the Copyright Holder that are necessarily 187 | infringed by the Package. If you institute patent litigation 188 | (including a cross-claim or counterclaim) against any party alleging 189 | that the Package constitutes direct or contributory patent 190 | infringement, then this Artistic License to you shall terminate on the 191 | date that such litigation is filed. 192 | 193 | (14) Disclaimer of Warranty: 194 | THE PACKAGE IS PROVIDED BYThe Artistic License 2.0 195 | 196 | Copyright (c) 2014 w.vela 197 | 198 | Everyone is permitted to copy and distribute verbatim copies 199 | of this license document, but changing it is not allowed. 200 | 201 | Preamble 202 | 203 | This license establishes the terms under which a given free software 204 | Package may be copied, modified, distributed, and/or redistributed. 205 | The intent is that the Copyright Holder maintains some artistic 206 | control over the development of that Package while still keeping the 207 | Package available as open source and free software. 208 | 209 | You are always permitted to make arrangements wholly outside of this 210 | license directly with the Copyright Holder of a given Package. If the 211 | terms of this license do not permit the full use that you propose to 212 | make of the Package, you should contact the Copyright Holder and seek 213 | a different licensing arrangement. 214 | 215 | Definitions 216 | 217 | "Copyright Holder" means the individual(s) or organization(s) 218 | named in the copyright notice for the entire Package. 219 | 220 | "Contributor" means any party that has contributed code or other 221 | material to the Package, in accordance with the Copyright Holder's 222 | procedures. 223 | 224 | "You" and "your" means any person who would like to copy, 225 | distribute, or modify the Package. 226 | 227 | "Package" means the collection of files distributed by the 228 | Copyright Holder, and derivatives of that collection and/or of 229 | those files. A given Package may consist of either the Standard 230 | Version, or a Modified Version. 231 | 232 | "Distribute" means providing a copy of the Package or making it 233 | accessible to anyone else, or in the case of a company or 234 | organization, to others outside of your company or organization. 235 | 236 | "Distributor Fee" means any fee that you charge for Distributing 237 | this Package or providing support for this Package to another 238 | party. It does not mean licensing fees. 239 | 240 | "Standard Version" refers to the Package if it has not been 241 | modified, or has been modified only in ways explicitly requested 242 | by the Copyright Holder. 243 | 244 | "Modified Version" means the Package, if it has been changed, and 245 | such changes were not explicitly requested by the Copyright 246 | Holder. 247 | 248 | "Original License" means this Artistic License as Distributed with 249 | the Standard Version of the Package, in its current version or as 250 | it may be modified by The Perl Foundation in the future. 251 | 252 | "Source" form means the source code, documentation source, and 253 | configuration files for the Package. 254 | 255 | "Compiled" form means the compiled bytecode, object code, binary, 256 | or any other form resulting from mechanical transformation or 257 | translation of the Source form. 258 | 259 | 260 | Permission for Use and Modification Without Distribution 261 | 262 | (1) You are permitted to use the Standard Version and create and use 263 | Modified Versions for any purpose without restriction, provided that 264 | you do not Distribute the Modified Version. 265 | 266 | 267 | Permissions for Redistribution of the Standard Version 268 | 269 | (2) You may Distribute verbatim copies of the Source form of the 270 | Standard Version of this Package in any medium without restriction, 271 | either gratis or for a Distributor Fee, provided that you duplicate 272 | all of the original copyright notices and associated disclaimers. At 273 | your discretion, such verbatim copies may or may not include a 274 | Compiled form of the Package. 275 | 276 | (3) You may apply any bug fixes, portability changes, and other 277 | modifications made available from the Copyright Holder. The resulting 278 | Package will still be considered the Standard Version, and as such 279 | will be subject to the Original License. 280 | 281 | 282 | Distribution of Modified Versions of the Package as Source 283 | 284 | (4) You may Distribute your Modified Version as Source (either gratis 285 | or for a Distributor Fee, and with or without a Compiled form of the 286 | Modified Version) provided that you clearly document how it differs 287 | from the Standard Version, including, but not limited to, documenting 288 | any non-standard features, executables, or modules, and provided that 289 | you do at least ONE of the following: 290 | 291 | (a) make the Modified Version available to the Copyright Holder 292 | of the Standard Version, under the Original License, so that the 293 | Copyright Holder may include your modifications in the Standard 294 | Version. 295 | 296 | (b) ensure that installation of your Modified Version does not 297 | prevent the user installing or running the Standard Version. In 298 | addition, the Modified Version must bear a name that is different 299 | from the name of the Standard Version. 300 | 301 | (c) allow anyone who receives a copy of the Modified Version to 302 | make the Source form of the Modified Version available to others 303 | under 304 | 305 | (i) the Original License or 306 | 307 | (ii) a license that permits the licensee to freely copy, 308 | modify and redistribute the Modified Version using the same 309 | licensing terms that apply to the copy that the licensee 310 | received, and requires that the Source form of the Modified 311 | Version, and of any works derived from it, be made freely 312 | available in that license fees are prohibited but Distributor 313 | Fees are allowed. 314 | 315 | 316 | Distribution of Compiled Forms of the Standard Version 317 | or Modified Versions without the Source 318 | 319 | (5) You may Distribute Compiled forms of the Standard Version without 320 | the Source, provided that you include complete instructions on how to 321 | get the Source of the Standard Version. Such instructions must be 322 | valid at the time of your distribution. If these instructions, at any 323 | time while you are carrying out such distribution, become invalid, you 324 | must provide new instructions on demand or cease further distribution. 325 | If you provide valid instructions or cease distribution within thirty 326 | days after you become aware that the instructions are invalid, then 327 | you do not forfeit any of your rights under this license. 328 | 329 | (6) You may Distribute a Modified Version in Compiled form without 330 | the Source, provided that you comply with Section 4 with respect to 331 | the Source of the Modified Version. 332 | 333 | 334 | Aggregating or Linking the Package 335 | 336 | (7) You may aggregate the Package (either the Standard Version or 337 | Modified Version) with other packages and Distribute the resulting 338 | aggregation provided that you do not charge a licensing fee for the 339 | Package. Distributor Fees are permitted, and licensing fees for other 340 | components in the aggregation are permitted. The terms of this license 341 | apply to the use and Distribution of the Standard or Modified Versions 342 | as included in the aggregation. 343 | 344 | (8) You are permitted to link Modified and Standard Versions with 345 | other works, to embed the Package in a larger work of your own, or to 346 | build stand-alone binary or bytecode versions of applications that 347 | include the Package, and Distribute the result without restriction, 348 | provided the result does not expose a direct interface to the Package. 349 | 350 | 351 | Items That are Not Considered Part of a Modified Version 352 | 353 | (9) Works (including, but not limited to, modules and scripts) that 354 | merely extend or make use of the Package, do not, by themselves, cause 355 | the Package to be a Modified Version. In addition, such works are not 356 | considered parts of the Package itself, and are not subject to the 357 | terms of this license. 358 | 359 | 360 | General Provisions 361 | 362 | (10) Any use, modification, and distribution of the Standard or 363 | Modified Versions is governed by this Artistic License. By using, 364 | modifying or distributing the Package, you accept this license. Do not 365 | use, modify, or distribute the Package, if you do not accept this 366 | license. 367 | 368 | (11) If your Modified Version has been derived from a Modified 369 | Version made by someone other than you, you are nevertheless required 370 | to ensure that your Modified Version complies with the requirements of 371 | this license. 372 | 373 | (12) This license does not grant you the right to use any trademark, 374 | service mark, tradename, or logo of the Copyright Holder. 375 | 376 | (13) This license includes the non-exclusive, worldwide, 377 | free-of-charge patent license to make, have made, use, offer to sell, 378 | sell, import and otherwise transfer the Package with respect to any 379 | patent claims licensable by the Copyright Holder that are necessarily 380 | infringed by the Package. If you institute patent litigation 381 | (including a cross-claim or counterclaim) against any party alleging 382 | that the Package constitutes direct or contributory patent 383 | infringement, then this Artistic License to you shall terminate on the 384 | date that such litigation is filed. 385 | 386 | (14) Disclaimer of Warranty: 387 | THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS 388 | IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED 389 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR 390 | NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL 391 | LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL 392 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 393 | DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF 394 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 395 | THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS 396 | IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED 397 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR 398 | NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL 399 | LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL 400 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 401 | DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF 402 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | model-view-builder 2 | ======================= 3 | [![Build Status](https://travis-ci.org/PhantomThief/model-view-builder.svg)](https://travis-ci.org/PhantomThief/model-view-builder) 4 | [![Coverage Status](https://coveralls.io/repos/PhantomThief/model-view-builder/badge.svg?branch=master&service=github)](https://coveralls.io/github/PhantomThief/model-view-builder?branch=master) 5 | [![Total alerts](https://img.shields.io/lgtm/alerts/g/PhantomThief/model-view-builder.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/PhantomThief/model-view-builder/alerts/) 6 | [![Language grade: Java](https://img.shields.io/lgtm/grade/java/g/PhantomThief/model-view-builder.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/PhantomThief/model-view-builder/context:java) 7 | [![Maven Central](https://img.shields.io/maven-central/v/com.github.phantomthief/model-view-builder)](https://search.maven.org/artifact/com.github.phantomthief/model-view-builder/) 8 | 9 | 对象依赖构建以及model到view的映射 10 | 11 | * 树形迭代构建整个model结构 12 | * 层级构建依赖 13 | * 对Model零侵入 14 | * 使用构建数据上下文进行View映射 15 | 16 | ## 为什么构建这个项目 17 | 18 | 在web场景中,经常会遇到,构建一个列表数据(渲染jsp或者输出json给客户端使用),但是列表用到的数据会依赖别的数据。 19 | 20 | 举个具体例子: 21 | 22 | 底层接口返回一个帖子列表: 23 | 24 | ```Java 25 | List postList = postService.getList(); 26 | ``` 27 | 28 | Post的定义可能是: 29 | ```Java 30 | public class Post { 31 | private int id; 32 | private int authorUserId; 33 | private String content; 34 | // ...other properties... 35 | // ...getter and setter 36 | public int getAuthorUserId() { 37 | return this.authorUserId; 38 | } 39 | } 40 | ``` 41 | 42 | 而最终输出的结果里,可能还需要帖子的作者(authorUserId)的用户信息,帖子的一些状态(比如帖子有多少个评论,当前访问者是否喜欢了帖子),以及作者的一些状态(比如当前用户是否关注了作者之类的)。 43 | 44 | 最终,构建过程会是一个树形的递归结构:拿到Post后,根据authorUserId,搜集所有用户id,然后再获取用户的数据。可能还需要根据帖子的id构建一些其它数据。 45 | 46 | 下面就是这些可能的接口定义: 47 | ```Java 48 | // 获取用户数据 49 | public Map getUserByIds(Collection userIds); 50 | 51 | // 获取帖子的评论数 52 | public Map getPostCommentCount(Collection postIds); 53 | 54 | // 获取一个用户是否喜欢过这些帖子的状态 55 | public Map isUserFavoritedPosts(int userId, Collection postIds); 56 | 57 | // 获取一个用户是否关注另外一些用户的状态 58 | public Map isUserFollowingUsers(int userId, Collection toCheckedUserIds); 59 | ``` 60 | 61 | 这样构建代码可能就会是一些硬编码的foreach循环搜集id,然后再调用接口构建,而且可复用程度不高。 62 | 63 | 本项目就是提供一个解决这样场景的方案。 64 | 65 | ### ModelBuilder的使用: 66 | 67 | #### 概念 68 | 69 | ModelBuilder构建器分成两组命名空间:id命名空间和value命名空间。命名空间可以是一个Class也可以是一个字符串。 70 | 71 | id命名空间存储的是构建过程中用到的id(比如上面提到的场景中,Post的id,User的id就会存入这里)。 72 | 73 | value命名空间存储的是构建过程中的具体实体数据(比如上面例子中,Post对象,User对象,以及﹝帖子评论数﹞,﹝是否喜欢过﹞之类的)。 74 | 75 | #### 使用方法 76 | 77 | 构建器ModelBuilder声明时使用下面的方式: 78 | ```Java 79 | SimpleModelBuilder modelBuilder = new SimpleModelBuilder() 80 | // 这里使用流式定义modelBuilder的依赖以及构建器之类的…… 81 | ``` 82 | 83 | 完成声明后,ModelBuilder对象就可以使用了(这个对象建议复用)。 84 | 85 | 每次构建对象时,用这样的调用: 86 | ```Java 87 | SimpleBuildContext buildContext = new SimpleBuildContext(); // 声明一个构建上下文,所有构建的结果都会存入这个上下文对象中 88 | modelBuilder.buildMulti(postList, buildContext); // 执行构建操作 89 | ``` 90 | 91 | 构建完成后,所有构建结果都会在上下文buildContext对象中,可以使用这样的语法获得数据: 92 | ```Java 93 | int specifyUserId = 23; 94 | User user = buildContext.getDatas(User.class).get(specifyUserId); // 从User.class的value命名空间获得数据 95 | 96 | int specifyPostId = 56; 97 | Map postCommentMap = buildContext.getDatas("postComments"); // 从postComments的value命名空间获得数据 98 | int postComment = postCommentMap.getOrDefault(specifyPostId, 0); 99 | ``` 100 | 101 | #### 构建依赖的声明 102 | 103 | 声明依赖包含三种情况: 104 | 105 | ##### 从原始对象抽取数据到id命名空间 106 | 107 | 上面例子中的使用场景:把Post.getAuthorUserId()返回的数据放到id命名空间User.class 108 | 109 | ```Java 110 | SimpleModelBuilder modelBuilder = new SimpleModelBuilder() 111 | .on(Post.class).id(Post::getAuthorUserId).to(User.class) //post.getAuthoUserId()返回值放到User.class的id命名空间中 112 | ``` 113 | 114 | ##### 从已有的value抽取value和id 115 | 116 | ```Java 117 | SimpleModelBuilder modelBuilder = new SimpleModelBuilder() 118 | .self(Post.class, Post::getId) // post对象放到value为Post.class的命名空间,同时Post.getId() 119 | ``` 120 | 121 | 如果遇到没有完成构建的Post对象,会直接把Post对象放到Post.class的value命名空间中,并把Post.getId()放到Post.class的id命名空间中 122 | 123 | ##### 从id命名空间构建数据到value命名空间 124 | 125 | ```Java 126 | SimpleModelBuilder modelBuilder = new SimpleModelBuilder() 127 | .build(User.class, userService::getUserByIds) // 把id命名空间User.class用userService.getUserByIds()方法构建数据,并回存到value命名空间User.class 128 | .build(Post.class). by(postService::getPostCommentCount).to("postComments") // 把id命名空间Post.class的数据用postService.getPostCommentCount()方法构建,构建结果存入postComments的value命名空间 129 | ``` 130 | 131 | ### ViewMapper的使用 132 | 133 | #### 概念 134 | 135 | ViewMapper负责把model对象转换为view对象。例如,一个Post对象(如上面定义)可能会和具体的Post对象存储结构耦合。 136 | 137 | 而最终输出到页面上时,可能并不是Post对象一一对应(比如本例子中,可能有一些字段不会输出,另外一些字段可能并不存在于Post对象中,比如作者的信息,或者一些和访问者相关的状态)。 138 | 139 | 所以会定义一个PostView,如下: 140 | 141 | ```Java 142 | public class PostView { 143 | private Post post; 144 | private BuildContext buildContext; 145 | public PostView(Post post, BuildContext buildContext){ 146 | this.post = Post; 147 | this.buildContext = buildContext; 148 | } 149 | public int getId() { 150 | return post.getId(); 151 | } 152 | public UserView getAuthor() { 153 | User author = buildContext.getDatas(User.class).get(post.getAuthorUserId()); 154 | if (author!=null) { 155 | return new UserView(author, buildContext); 156 | } else { 157 | return null; 158 | } 159 | } 160 | public int getCommentCount() { 161 | Map commentCountMap = buildContext.getDatas("postComments"); 162 | return commentCountMap.getOrDefault(post.getId(), 0); 163 | } 164 | // ...other fields... 165 | } 166 | ``` 167 | 168 | 在使用时,需要ViewMapper知道Model类到View类的映射,可以使用如下代码进行声明: 169 | 170 | ```Java 171 | ViewMapper viewMapper = new DefaultViewMapperImpl(); 172 | ((DefaultViewMapperImpl) viewMapper).addMapper(Post.class, (buildContext, post) -> new PostView(post, buildContext)); 173 | ``` 174 | 175 | 最终调用: 176 | 177 | ```Java 178 | List postViews = viewMapper.map(postList, bulidContext); 179 | ``` 180 | 181 | ## 高级技巧 182 | 183 | ### 自定义BuildContext 184 | 185 | 很多使用,希望把一些初始参数放入BuildContext中,这时候可以考虑使用自定义的BuildContext。以需要知道访问者身份的构建器为例: 186 | ```Java 187 | public class MyBuildContext extends SimpleBuildContext { 188 | private int visitor; 189 | public int getVisitor() { 190 | return this.visitor; 191 | } 192 | public void setVisitor(int visitor){ 193 | this.visitor = visitor; 194 | } 195 | } 196 | ``` 197 | 198 | 然后在声明ModelBuilder时,可以使用MyBuildContext代替默认的BuildContext: 199 | ```Java 200 | ModelBuilder modelBuilder = new SimpleModelBuilder() 201 | .build(Post.class). by((buildContext, postIds) -> postService.isUserFavoritedPosts(buildContext.getVisitor(), postIds)).to("userFavoritesPosts"); 202 | ``` 203 | 204 | 使用构建器时: 205 | ```Java 206 | int visitor = 999; 207 | MyBuildContext myBuildContext = new MyBuildContext(); 208 | myBuildContext.setVisitor(visitor); 209 | 210 | modelBuilder.buildMulti(posts, myBuildContext); 211 | ``` 212 | 213 | ### Model中可以直接抽出其它Model的情况 214 | 215 | 如果一个model里可以获得另外别的model,就可以使用这种方法来抽出元素。举例: 216 | ```Java 217 | public class Post { 218 | private User author; 219 | public User getAuthor() { 220 | return this.author; 221 | } 222 | private List atUsers; 223 | public List getAtUsers() { 224 | return this.atUsers; 225 | } 226 | } 227 | ``` 228 | 229 | 那么依赖声明时可以这样: 230 | ```Java 231 | SimpleModelBuilder modelBuilder = new SimpleModelBuilder() 232 | .on(Post.class).value(Post::getAuthor).id(User::getId).to(User.class); 233 | ``` 234 | 235 | ### 基于反射的ViewMapper声明 236 | 237 | 如果View可以按照某些约定去编写(例如放在特定包下,或者使用特定注解作为工厂方法/构建方法之类的),那么可以利用反射去完成构建。这也是ViewMapper声明的推荐做法。 238 | 239 | 由于View的实现各式各样,这里就不提供统一的工具方法,只是提供一个简单的例子: 240 | ```Java 241 | public static final ViewMapper scan(String pkg, Set> ignoreViews) { 242 | DefaultViewMapperImpl viewMapper = new DefaultViewMapperImpl(); 243 | try { 244 | ImmutableSet topLevelClasses = ClassPath.from( 245 | ViewerScanner.class.getClassLoader()).getTopLevelClassesRecursive(pkg); 246 | for (ClassInfo classInfo : topLevelClasses) { 247 | Class type = classInfo.load(); 248 | if (ignoreViews.contains(type)) { 249 | continue; 250 | } 251 | Constructor[] constructors = type.getConstructors(); 252 | for (Constructor constructor : constructors) { 253 | Class[] parameterTypes = constructor.getParameterTypes(); 254 | if (parameterTypes.length == 2 && parameterTypes[1] == BuildContext.class) { 255 | logger.info("register view [{}] for model [{}], with buildContext.", 256 | type.getSimpleName(), parameterTypes[0].getSimpleName()); 257 | viewMapper.addMapper(parameterTypes[0], (buildContext, i) -> { 258 | try { 259 | return constructor.newInstance(i, buildContext); 260 | } catch (Exception e) { 261 | logger.error("fail to construct model:{}", i, e); 262 | return null; 263 | } 264 | }); 265 | } 266 | if (parameterTypes.length == 1) { 267 | logger.info("register view [{}] for model [{}]", type.getSimpleName(), 268 | parameterTypes[0].getSimpleName()); 269 | viewMapper.addMapper(parameterTypes[0], (buildContext, i) -> { 270 | try { 271 | return constructor.newInstance(i); 272 | } catch (Exception e) { 273 | logger.error("fail to construct model:{}", i, e); 274 | return null; 275 | } 276 | }); 277 | } 278 | } 279 | } 280 | } catch (IOException e) { 281 | logger.error("Ops.", e); 282 | } 283 | return viewMapper; 284 | } 285 | ``` 286 | 287 | ### 使用OverrideViewMapper进行View映射的剪裁和定制 288 | 289 | 特定场景下,可能强制覆盖某些Model到View的映射关系,比如正常场景下,User对象会映射成UserView,但是在某个场景下,User对象需要映射到UserCustomizeView,这时候可以使用临时的View映射定制: 290 | ```Java 291 | ViewMapper defaultViewMapper = getDefaultViewMapper(); 292 | OverrideViewMapper overrideViewMapper = new OverrideViewMapper<>(defaultViewMapper) 293 | .addMapper(User.class, (user, buildContext) -> new UserCustmoizeView(user)); 294 | 295 | List views = overrideViewMapper.map(userList); 296 | ``` 297 | 298 | ## 注意事项 299 | 300 | ### 我不是太理解这个组件的设计、实现或者使用场景,感觉有点儿难用 301 | 302 | 不要使用!不要使用!不要使用! 303 | 304 | ### 对象不会被重复构建 305 | 306 | 如果一个对象(比如id=1的Post对象)如果已经被构建完(它的所有依赖都已经构建完成),在构建过程中,如果再次遇到相同的对象,将不会重复构建。 307 | 308 | 所以在声明构建依赖关系时,不用担心出现环状声明造成实际构建过程的死循环。 309 | 310 | 事实上,你完全可以把所有的构建依赖都声明到一个ModelBuilder里,然后工程全局使用这唯一一个ModelBuilder。因为这个ModelBuilder里已经定义了最齐全的依赖关系。 311 | 312 | ### ModelBuilder依赖声明支持接口 313 | 314 | 假如有一组对象,都实现了如下接口: 315 | ```Java 316 | public interface HasAuthor { 317 | int getAuthorUserId(); 318 | } 319 | ``` 320 | 321 | 那么在ModelBuilder声明依赖关系时,可以直接声明这个接口依赖: 322 | ```Java 323 | SimpleModelBuilder modelBuilder = new SimpleModelBuilder() 324 | .on(HasAuthor.class).id(HasAuthor::getAuthorUserId).to(User.class); 325 | ``` 326 | 327 | 那么所有实现了HasAuthor接口的Model就不用重复声明这个依赖了。抽象类或者父类上的声明关系也遵循这个规则。 328 | 329 | ### ModelBuilder的定义顺序与构建顺序无关 330 | 331 | ModelBuilder在声明时只定义构建过程中各个元素的依赖关系,声明顺序不会影响到构建顺序。而构建过程是一个查找-构建的过程。每次循环会把当前未完成构建的对象,依次执行﹝抽出id﹞、﹝抽出value﹞和﹝构建value﹞三部操作。 332 | 333 | 每次产生的新的value会在下一轮构建时重复进行。直到没有新的对象被构建出来为止。 334 | 335 | ### 为什么不提供构建对象的回填机制 336 | 337 | 考虑最开始的例子:Post有一个getAuthorUserId()方法,返回作者的id。如果需要对象回填的话,就需要额外提供如下方法: 338 | ```Java 339 | public User getAuthor(); 340 | public void setAuthor(User user); 341 | ``` 342 | 这样,Post就可能存在两个状态:回填前,getAuthor()方法是无效的,而回填后,getAuthor()才可用。这会给后续使用带来很多问题。 343 | 344 | 另外,回填操作其实是一个相当消耗资源的事情,使用上下文查找其实是把回填操作lazy化(在需要的使用,调用getter时才会查找)。 345 | 346 | 当然,其实回填操作也可以自己去实现。所以本组件就没有提供这样的机制。 347 | 348 | ### 为什么使用编程式而不是声明式? 349 | 350 | 因为我讨厌写配置文件,越复杂的事情,配置文件往往比编程要复杂的多。如果你喜欢配置文件,可以帮我实现一个,也不复杂:) 351 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | com.github.phantomthief 4 | model-view-builder 5 | 1.1.5-SNAPSHOT 6 | 7 | 8 | 3.0.4 9 | 10 | 11 | 12 | 1.7.26 13 | 28.1-jre 14 | 3.4 15 | 3.8.1 16 | 17 | 5.7.1 18 | 1.1.8 19 | 2.9.10.8 20 | 21 | 3.0.0-M5 22 | 1.6.8 23 | 0.8.6 24 | 4.3.0 25 | 3.2.1 26 | 3.2.0 27 | 2.2.6 28 | 3.2.0 29 | 30 | 31 | 32 | org.sonatype.oss 33 | oss-parent 34 | 9 35 | 36 | 37 | model-view-builder 38 | A hierarchy model builder with view mapper 39 | 40 | https://github.com/PhantomThief/model-view-builder 41 | 42 | 43 | 44 | w.vela 45 | 46 | 47 | 48 | 49 | 50 | The Artistic License 2.0 51 | http://www.perlfoundation.org/artistic_license_2_0 52 | repo 53 | 54 | 55 | 56 | 57 | scm:git:git@github.com:PhantomThief/model-view-builder.git 58 | https://github.com/PhantomThief/model-view-builder.git 59 | scm:git:git@github.com:PhantomThief/model-view-builder.git 60 | 61 | 62 | 63 | 64 | 65 | 66 | org.junit 67 | junit-bom 68 | ${junit.version} 69 | pom 70 | import 71 | 72 | 73 | 74 | 75 | 76 | 77 | org.slf4j 78 | slf4j-api 79 | ${slf4j-api.version} 80 | 81 | 82 | com.google.guava 83 | guava 84 | ${guava.version} 85 | 86 | 87 | org.apache.commons 88 | commons-lang3 89 | ${commons-lang3.version} 90 | 91 | 92 | 93 | org.junit.jupiter 94 | junit-jupiter-api 95 | test 96 | 97 | 98 | org.junit.jupiter 99 | junit-jupiter-engine 100 | test 101 | 102 | 103 | ch.qos.logback 104 | logback-classic 105 | ${logback-classic.version} 106 | test 107 | 108 | 109 | com.fasterxml.jackson.core 110 | jackson-databind 111 | ${jackson-databind.version} 112 | test 113 | 114 | 115 | 116 | 117 | 118 | 119 | org.apache.maven.plugins 120 | maven-compiler-plugin 121 | ${maven-compiler-plugin.version} 122 | 123 | 1.8 124 | 1.8 125 | true 126 | true 127 | UTF-8 128 | true 129 | true 130 | -parameters 131 | -parameters 132 | 133 | 134 | 135 | org.apache.maven.plugins 136 | maven-source-plugin 137 | ${maven-source-plugin.version} 138 | 139 | 140 | attach-sources 141 | 142 | jar 143 | 144 | 145 | 146 | 147 | 148 | org.apache.maven.plugins 149 | maven-javadoc-plugin 150 | ${maven-javadoc-plugin.version} 151 | 152 | none 153 | 8 154 | 155 | 156 | 157 | attach-javadocs 158 | 159 | jar 160 | 161 | 162 | 163 | 164 | 165 | org.sonatype.plugins 166 | nexus-staging-maven-plugin 167 | ${nexus-staging-maven-plugin.version} 168 | true 169 | 170 | sonatype-nexus-staging 171 | https://oss.sonatype.org/ 172 | true 173 | 174 | 175 | 176 | org.jacoco 177 | jacoco-maven-plugin 178 | ${jacoco-maven-plugin.version} 179 | 180 | 181 | prepare-agent 182 | 183 | prepare-agent 184 | 185 | 186 | 187 | 188 | 189 | org.eluder.coveralls 190 | coveralls-maven-plugin 191 | ${coveralls-maven-plugin.version} 192 | 193 | 194 | maven-surefire-plugin 195 | ${maven-surefire-plugin.version} 196 | 197 | 198 | pl.project13.maven 199 | git-commit-id-plugin 200 | ${git-commit-id-plugin.version} 201 | 202 | 203 | get-the-git-infos 204 | 205 | revision 206 | 207 | 208 | 209 | 210 | false 211 | 8 212 | yyyyMMddHHmmssSSS 213 | false 214 | false 215 | true 216 | 217 | true 218 | 219 | 220 | git.branch 221 | git.build 222 | git.commit.id 223 | git.commit.time 224 | git.commit.user 225 | git.remote.origin.url 226 | 227 | 228 | 229 | 230 | org.apache.maven.plugins 231 | maven-jar-plugin 232 | ${maven-jar-plugin.version} 233 | 234 | 235 | 236 | true 237 | 238 | 239 | ${git.commit.id} 240 | ${git.build.time} 241 | ${git.branch} 242 | ${java.version} 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | sonatype-nexus-snapshots 253 | https://oss.sonatype.org/content/repositories/snapshots 254 | 255 | 256 | sonatype-nexus-staging 257 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 258 | 259 | 260 | 261 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/model/builder/ModelBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.model.builder; 2 | 3 | import static java.util.Collections.singleton; 4 | 5 | import com.github.phantomthief.model.builder.context.BuildContext; 6 | 7 | /** 8 | * 9 | * @author w.vela 10 | */ 11 | public interface ModelBuilder { 12 | 13 | void buildMulti(Iterable sources, B buildContext); 14 | 15 | default void buildSingle(Object one, B buildContext) { 16 | buildMulti(singleton(one), buildContext); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/model/builder/context/BuildContext.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.model.builder.context; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * @author w.vela 7 | */ 8 | public interface BuildContext { 9 | 10 | default Map getData(Class type) { 11 | return getData((Object) type); 12 | } 13 | 14 | Map getData(Object namespace); 15 | 16 | void merge(BuildContext buildContext); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/model/builder/context/impl/SimpleBuildContext.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.model.builder.context.impl; 2 | 3 | import static org.apache.commons.lang3.builder.ToStringBuilder.reflectionToString; 4 | import static org.apache.commons.lang3.builder.ToStringStyle.SHORT_PREFIX_STYLE; 5 | 6 | import java.util.Map; 7 | import java.util.concurrent.ConcurrentHashMap; 8 | import java.util.concurrent.ConcurrentMap; 9 | import java.util.function.Function; 10 | 11 | import com.github.phantomthief.model.builder.context.BuildContext; 12 | import com.github.phantomthief.model.builder.util.MergeUtils; 13 | 14 | /** 15 | * 16 | * @author w.vela 17 | */ 18 | @SuppressWarnings("unchecked") 19 | public class SimpleBuildContext implements BuildContext { 20 | 21 | private final ConcurrentMap> datas; 22 | private final ConcurrentMap> lazyDatas = new ConcurrentHashMap<>(); 23 | private final ConcurrentMap>> lazyBuilders = new ConcurrentHashMap<>(); 24 | 25 | public SimpleBuildContext() { 26 | this(new ConcurrentHashMap<>()); 27 | } 28 | 29 | // for test case 30 | public SimpleBuildContext(ConcurrentMap> datas) { 31 | this.datas = datas; 32 | } 33 | 34 | @Override 35 | public Map getData(Object namespace) { 36 | Function> lazyBuilder = lazyBuilders.get(namespace); 37 | if (lazyBuilder != null) { 38 | return computeIfAbsent(lazyDatas, namespace, ns -> lazyBuilder.apply(this)); 39 | } else { 40 | return computeIfAbsent(datas, namespace, ns -> new ConcurrentHashMap<>()); 41 | } 42 | } 43 | 44 | /** 45 | * Workaround to fix ConcurrentHashMap stuck bug when call {@link ConcurrentHashMap#computeIfAbsent} recursively. 46 | * see https://bugs.openjdk.java.net/browse/JDK-8062841. 47 | */ 48 | private static Map computeIfAbsent(ConcurrentMap> map, Object key, 49 | Function> function) { 50 | Map value = map.get(key); 51 | if (value == null) { 52 | value = function.apply(key); 53 | map.put(key, value); 54 | } 55 | return (Map) value; 56 | } 57 | 58 | public void setupLazyNodeData(Object namespace, 59 | Function> lazyBuildFunction) { 60 | lazyBuilders.put(namespace, lazyBuildFunction); 61 | } 62 | 63 | @Override 64 | public void merge(BuildContext buildContext) { 65 | if (buildContext instanceof SimpleBuildContext) { 66 | SimpleBuildContext other = (SimpleBuildContext) buildContext; 67 | other.datas.forEach( 68 | (namespace, values) -> datas.merge(namespace, values, MergeUtils::merge)); 69 | other.lazyBuilders.forEach(lazyBuilders::putIfAbsent); 70 | lazyBuilders.keySet().forEach(key -> { 71 | datas.remove(key); 72 | lazyDatas.remove(key); 73 | }); 74 | } else { 75 | throw new UnsupportedOperationException(); 76 | } 77 | } 78 | 79 | @Override 80 | public String toString() { 81 | return reflectionToString(this, SHORT_PREFIX_STYLE); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/model/builder/impl/LazyBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.model.builder.impl; 2 | 3 | import java.util.Collection; 4 | import java.util.Map; 5 | import java.util.function.BiFunction; 6 | import java.util.function.Function; 7 | 8 | import com.github.phantomthief.model.builder.impl.SimpleModelBuilder.Lazy; 9 | 10 | /** 11 | * @author w.vela 12 | */ 13 | public class LazyBuilder { 14 | 15 | public static Lazy on(Object sourceNamespace, Function, Map> builder, 16 | Object targetNamespace) { 17 | return new Lazy() { 18 | 19 | @Override 20 | public Object sourceNamespace() { 21 | return sourceNamespace; 22 | } 23 | 24 | @Override 25 | public Object targetNamespace() { 26 | return targetNamespace; 27 | } 28 | 29 | @SuppressWarnings({ "unchecked", "rawtypes" }) 30 | @Override 31 | public BiFunction builder() { 32 | return (context, ids) -> ((Function) builder).apply(ids); 33 | } 34 | }; 35 | } 36 | 37 | public static Lazy on(Object sourceNamespace, 38 | BiFunction, Map> builder, Object targetNamespace) { 39 | return new Lazy() { 40 | 41 | @Override 42 | public Object sourceNamespace() { 43 | return sourceNamespace; 44 | } 45 | 46 | @Override 47 | public Object targetNamespace() { 48 | return targetNamespace; 49 | } 50 | 51 | @Override 52 | public BiFunction builder() { 53 | return builder; 54 | } 55 | }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/model/builder/impl/SimpleModelBuilder.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.model.builder.impl; 2 | 3 | import static com.google.common.collect.HashMultimap.create; 4 | import static java.util.Collections.emptyMap; 5 | import static java.util.Collections.emptySet; 6 | import static java.util.Collections.singleton; 7 | import static java.util.Collections.singletonMap; 8 | import static java.util.stream.Collectors.toSet; 9 | import static org.apache.commons.lang3.ClassUtils.getAllInterfaces; 10 | import static org.apache.commons.lang3.ClassUtils.getAllSuperclasses; 11 | import static org.apache.commons.lang3.builder.ToStringBuilder.reflectionToString; 12 | import static org.apache.commons.lang3.builder.ToStringStyle.SHORT_PREFIX_STYLE; 13 | import static org.slf4j.LoggerFactory.getLogger; 14 | 15 | import java.util.Collection; 16 | import java.util.HashMap; 17 | import java.util.HashSet; 18 | import java.util.Map; 19 | import java.util.Map.Entry; 20 | import java.util.Set; 21 | import java.util.concurrent.ConcurrentHashMap; 22 | import java.util.concurrent.ConcurrentMap; 23 | import java.util.function.BiFunction; 24 | import java.util.function.Function; 25 | 26 | import org.slf4j.Logger; 27 | 28 | import com.github.phantomthief.model.builder.ModelBuilder; 29 | import com.github.phantomthief.model.builder.context.BuildContext; 30 | import com.github.phantomthief.model.builder.context.impl.SimpleBuildContext; 31 | import com.github.phantomthief.model.builder.util.MergeUtils; 32 | import com.google.common.collect.SetMultimap; 33 | import com.google.common.collect.Sets; 34 | 35 | /** 36 | * @author w.vela 37 | */ 38 | @SuppressWarnings("unchecked") 39 | public class SimpleModelBuilder implements ModelBuilder { 40 | 41 | private static final Logger logger = getLogger(SimpleModelBuilder.class); 42 | 43 | // obj.class=>obj->(namespace,ids) 44 | private final SetMultimap, Function>>> idExtractors = create(); 45 | // obj.class=>obj->(namespace,values) 46 | private final SetMultimap, Function>>> valueExtractors = create(); 47 | // idNamespace=>(valueNamespace, ids->values) 48 | private final SetMultimap, Map>>> valueBuilders = create(); 49 | // targetNamespace=>Function 50 | private final Map>> lazyBuilders = new HashMap<>(); 51 | 52 | private final ConcurrentMap, Set>>>> cachedIdExtractors = new ConcurrentHashMap<>(); 53 | private final ConcurrentMap, Set>>>> cachedValueExtractors = new ConcurrentHashMap<>(); 54 | 55 | private volatile boolean alreadyBuilt = false; 56 | private Runnable onConflictListener; 57 | 58 | @Override 59 | public void buildMulti(Iterable sources, B buildContext) { 60 | alreadyBuilt = true; 61 | if (sources == null) { 62 | return; 63 | } 64 | if (buildContext instanceof SimpleBuildContext) { 65 | SimpleBuildContext simpleBuildContext = (SimpleBuildContext) buildContext; 66 | lazyBuilders.forEach(simpleBuildContext::setupLazyNodeData); 67 | } 68 | 69 | Set pendingForBuilding = Sets.newHashSet(sources); 70 | 71 | while (!pendingForBuilding.isEmpty()) { 72 | // namespace->ids 73 | Map> idsMap = new HashMap<>(); 74 | // namespace->values 75 | Map> valuesMap = new HashMap<>(); 76 | 77 | for (Object object : pendingForBuilding) { 78 | extract(object, buildContext, idsMap, valuesMap); 79 | } 80 | 81 | valueBuild(idsMap, valuesMap, buildContext); 82 | mergeToBuildContext(valuesMap, buildContext); 83 | 84 | 85 | Set newPendingForBuilding = new HashSet<>(); 86 | for (Map pending : valuesMap.values()) { 87 | newPendingForBuilding.addAll(pending.values()); 88 | } 89 | pendingForBuilding = newPendingForBuilding; 90 | } 91 | } 92 | 93 | public SimpleModelBuilder onConflictCheckListener(Runnable listener) { 94 | onConflictListener = listener; 95 | return this; 96 | } 97 | 98 | private void mergeToBuildContext(Map> valuesMap, B buildContext) { 99 | for (Entry> entry : valuesMap.entrySet()) { 100 | buildContext.getData(entry.getKey()).putAll(entry.getValue()); 101 | } 102 | } 103 | 104 | private void valueBuild(Map> idsMap, 105 | Map> valuesMap, B buildContext) { 106 | for (Entry> entry : idsMap.entrySet()) { 107 | for (KeyPair, Map>> valueBuilderWrapper 108 | : valueBuilders.get(entry.getKey())) { 109 | Object valueNamespace = valueBuilderWrapper.getKey(); 110 | BiFunction, Map> valueBuilder = valueBuilderWrapper 111 | .getValue(); 112 | Set needToBuildIds = filterIdSetOnBuild(entry.getValue(), buildContext, valuesMap, 113 | valueNamespace); 114 | Map values = valueBuilder.apply(buildContext, needToBuildIds); 115 | if (values != null) { 116 | valuesMap.merge(valueNamespace, values, MergeUtils::merge); 117 | } 118 | } 119 | } 120 | } 121 | 122 | private Set filterIdSetOnBuild(Set original, B buildContext, 123 | Map> valuesMap, Object valueNamespace) { 124 | Set buildContextExistIds = buildContext.getData(valueNamespace).keySet(); 125 | Set valueMapExistIds = computeIfAbsent(valuesMap, valueNamespace, i -> new HashMap<>()).keySet(); 126 | if (buildContextExistIds.isEmpty() && valueMapExistIds.isEmpty()) { 127 | return original; 128 | } 129 | 130 | Set filteredIds = new HashSet<>(original.size()); 131 | for (Object value : original) { 132 | if (!buildContextExistIds.contains(value) && !valueMapExistIds.contains(value)) { 133 | filteredIds.add(value); 134 | } 135 | } 136 | return filteredIds; 137 | } 138 | 139 | private static V computeIfAbsent(Map map, K key, Function function) { 140 | V value = map.get(key); 141 | if (value == null) { 142 | value = function.apply(key); 143 | map.put(key, value); 144 | } 145 | return value; 146 | } 147 | 148 | // return new found data. 149 | private void extract(Object obj, B buildContext, Map> idsMap, 150 | Map> valuesMap) { 151 | if (obj == null) { 152 | return; 153 | } 154 | Set>>> localValueExtractors = 155 | computeIfAbsent(cachedValueExtractors, obj.getClass(), 156 | t -> getAllSuperTypes((Class) t).stream() 157 | .flatMap(i -> this.valueExtractors.get(i).stream()).collect(toSet())); 158 | for (Function>> valueExtractor : localValueExtractors) { 159 | KeyPair> values = valueExtractor.apply(obj); 160 | Map filtered = filterValueMap(values, buildContext); 161 | idsMap.merge(values.getKey(), new HashSet<>(filtered.keySet()), 162 | MergeUtils::merge); 163 | valuesMap.merge(values.getKey(), filtered, MergeUtils::merge); 164 | } 165 | 166 | Set>>> localIdExtractors = computeIfAbsent( 167 | cachedIdExtractors, obj.getClass(), 168 | t -> getAllSuperTypes((Class) t).stream() 169 | .flatMap(i -> idExtractors.get(i).stream()).collect(toSet())); 170 | 171 | for (Function>> idExtractor : localIdExtractors) { 172 | KeyPair> ids = idExtractor.apply(obj); 173 | idsMap.merge(ids.getKey(), filterIdSet(ids, buildContext, valuesMap), 174 | MergeUtils::merge); 175 | } 176 | } 177 | 178 | private Set filterIdSet(KeyPair> keyPair, B buildContext, 179 | Map> valuesMap) { 180 | Set buildContextExistIds = buildContext.getData(keyPair.getKey()).keySet(); 181 | Set valueMapExistIds = computeIfAbsent(valuesMap, keyPair.getKey(), i -> new HashMap<>()).keySet(); 182 | 183 | if (buildContextExistIds.isEmpty() && valueMapExistIds.isEmpty()) { 184 | return new HashSet<>(keyPair.getValue()); 185 | } 186 | 187 | Set filteredIds = new HashSet<>(keyPair.getValue().size()); 188 | for (Object value : keyPair.getValue()) { 189 | if (!buildContextExistIds.contains(value) && !valueMapExistIds.contains(value)) { 190 | filteredIds.add(value); 191 | } 192 | } 193 | return filteredIds; 194 | } 195 | 196 | private Map filterValueMap(KeyPair> keyPair, 197 | B buildContext) { 198 | Map buildContextData = buildContext.getData(keyPair.getKey()); 199 | if (buildContextData.isEmpty()) { 200 | return new HashMap<>(keyPair.getValue()); 201 | } 202 | 203 | Map filteredValueMap = new HashMap<>(keyPair.getValue().size()); 204 | for (Entry entry: keyPair.value.entrySet()) { 205 | if (!buildContextData.containsKey(entry.getKey())) { 206 | filteredValueMap.put(entry.getKey(), entry.getValue()); 207 | } 208 | } 209 | return filteredValueMap; 210 | } 211 | 212 | /** 213 | * use {@link #extractId} or {@link #extractValue} 214 | */ 215 | @Deprecated 216 | public OnBuilder on(Class type) { 217 | return new OnBuilder<>(type); 218 | } 219 | 220 | /** 221 | * use {@link #valueFromSelf} 222 | */ 223 | @Deprecated 224 | public SimpleModelBuilder self(Class type, Function idExtractor) { 225 | SimpleModelBuilder.OnBuilder onBuilder = new OnBuilder<>(type); 226 | return onBuilder.new ExtractingValue(i -> i).id(idExtractor).to(type); 227 | } 228 | 229 | /** 230 | * use {@link #buildValue} or {@link #buildValueTo} 231 | */ 232 | @Deprecated 233 | public BuildingBuilder build(Object idNamespace) { 234 | return new BuildingBuilder(idNamespace); 235 | } 236 | 237 | /** 238 | * use {@link #buildValue} or {@link #buildValueTo} 239 | */ 240 | @Deprecated 241 | public SimpleModelBuilder build(Object idNamespace, 242 | BiFunction, Map> valueBuilder) { 243 | return build(idNamespace).by(valueBuilder).to(idNamespace); 244 | } 245 | 246 | /** 247 | * use {@link #buildValue} or {@link #buildValueTo} 248 | */ 249 | @Deprecated 250 | public SimpleModelBuilder build(Object idNamespace, 251 | Function, Map> valueBuilder) { 252 | return build(idNamespace).by(valueBuilder).to(idNamespace); 253 | } 254 | 255 | /** 256 | * use {@link #lazyBuild} 257 | */ 258 | @SuppressWarnings("rawtypes") 259 | @Deprecated 260 | public SimpleModelBuilder lazy(Lazy lazy) { 261 | tryCheckConflict(); 262 | lazyBuilders.put(lazy.targetNamespace(), buildContext -> (Map) ((BiFunction) lazy.builder()) 263 | .apply(buildContext, buildContext.getData(lazy.sourceNamespace()).keySet())); 264 | return this; 265 | } 266 | 267 | private void tryCheckConflict() { 268 | if (alreadyBuilt && onConflictListener != null) { 269 | onConflictListener.run(); 270 | } 271 | } 272 | 273 | public SimpleModelBuilder valueFromSelf(Class type, Function idExtractor) { 274 | self(type, idExtractor); 275 | return this; 276 | } 277 | 278 | public SimpleModelBuilder extractId(Class type, Function idExtractor, 279 | Object toIdNamespace) { 280 | on(type).id(idExtractor).to(toIdNamespace); 281 | return this; 282 | } 283 | 284 | public SimpleModelBuilder extractValue(Class type, 285 | Function valueExtractor, Function idExtractor, 286 | Object toValueNamespace) { 287 | on(type).value(valueExtractor).id(idExtractor).to(toValueNamespace); 288 | return this; 289 | } 290 | 291 | public SimpleModelBuilder buildValue(Object idNamespace, 292 | Function, Map> valueBuilder) { 293 | build(idNamespace, valueBuilder); 294 | return this; 295 | } 296 | 297 | public SimpleModelBuilder buildValue(Object idNamespace, 298 | BiFunction, Map> valueBuilder) { 299 | build(idNamespace, valueBuilder); 300 | return this; 301 | } 302 | 303 | public SimpleModelBuilder buildValueTo(Object idNamespace, 304 | Function, Map> valueBuilder, Object toValueNamespace) { 305 | build(idNamespace).by(valueBuilder).to(toValueNamespace); 306 | return this; 307 | } 308 | 309 | public SimpleModelBuilder buildValueTo(Object idNamespace, 310 | BiFunction, Map> valueBuilder, Object toValueNamespace) { 311 | build(idNamespace).by(valueBuilder).to(toValueNamespace); 312 | return this; 313 | } 314 | 315 | public SimpleModelBuilder lazyBuild(Object sourceNamespace, 316 | Function, Map> builder, Object targetNamespace) { 317 | lazy(LazyBuilder.on(sourceNamespace, builder, targetNamespace)); 318 | return this; 319 | } 320 | 321 | public SimpleModelBuilder lazyBuild(Object sourceNamespace, 322 | BiFunction, Map> builder, Object targetNamespace) { 323 | lazy(LazyBuilder.on(sourceNamespace, builder, targetNamespace)); 324 | return this; 325 | } 326 | 327 | private Set> getAllSuperTypes(Class iface) { 328 | Set> classes = new HashSet<>(); 329 | classes.add(iface); 330 | classes.addAll(getAllInterfaces(iface)); 331 | classes.addAll(getAllSuperclasses(iface)); 332 | return classes; 333 | } 334 | 335 | @Override 336 | public String toString() { 337 | return reflectionToString(this, SHORT_PREFIX_STYLE); 338 | } 339 | 340 | interface Lazy { 341 | 342 | Object sourceNamespace(); 343 | 344 | Object targetNamespace(); 345 | 346 | BiFunction builder(); 347 | } 348 | 349 | private static final class KeyPair implements Entry { 350 | 351 | private final Object key; 352 | private final V value; 353 | 354 | private KeyPair(Object key, V value) { 355 | this.key = key; 356 | this.value = value; 357 | } 358 | 359 | @Override 360 | public Object getKey() { 361 | return key; 362 | } 363 | 364 | @Override 365 | public V getValue() { 366 | return value; 367 | } 368 | 369 | @Override 370 | public V setValue(V value) { 371 | throw new UnsupportedOperationException(); 372 | } 373 | } 374 | 375 | @Deprecated 376 | public final class OnBuilder { 377 | 378 | private final Class objType; 379 | 380 | private OnBuilder(Class objType) { 381 | this.objType = objType; 382 | } 383 | 384 | public ExtractingId id(Function idExtractor) { 385 | return new ExtractingId(idExtractor); 386 | } 387 | 388 | public ExtractingValue value(Function valueExtractor) { 389 | return new ExtractingValue(valueExtractor); 390 | } 391 | 392 | public ExtractingValue value(Function> valueExtractor, 393 | Function idExtractor) { 394 | return new ExtractingValue(valueExtractor).id(idExtractor); 395 | } 396 | 397 | public final class ExtractingValue { 398 | 399 | private final Function valueExtractor; 400 | private Function idExtractor; 401 | 402 | private ExtractingValue(Function valueExtractor) { 403 | this.valueExtractor = (Function) valueExtractor; 404 | } 405 | 406 | public ExtractingValue id(Function idExtractor) { 407 | this.idExtractor = (Function) idExtractor; 408 | return this; 409 | } 410 | 411 | public SimpleModelBuilder to(Object valueNamespace) { 412 | tryCheckConflict(); 413 | valueExtractors.put(objType, obj -> { 414 | Object rawValue = valueExtractor.apply((E) obj); 415 | Map value; 416 | if (rawValue == null) { 417 | value = emptyMap(); 418 | } else { 419 | if (idExtractor != null) { 420 | if (rawValue instanceof Iterable) { 421 | if (rawValue instanceof Collection) { 422 | value = new HashMap<>(((Collection) rawValue).size()); 423 | } else { 424 | value = new HashMap<>(); 425 | } 426 | for (E e : ((Iterable) rawValue)) { 427 | value.put(idExtractor.apply(e), e); 428 | } 429 | } else { 430 | value = singletonMap(idExtractor.apply(rawValue), rawValue); 431 | } 432 | } else { 433 | if (rawValue instanceof Map) { 434 | value = (Map) rawValue; 435 | } else { 436 | logger.warn("invalid value extractor for:{}->{}", obj, rawValue); 437 | value = emptyMap(); 438 | } 439 | } 440 | } 441 | return new KeyPair<>(valueNamespace, value); 442 | }); 443 | cachedValueExtractors.clear(); 444 | return SimpleModelBuilder.this; 445 | } 446 | } 447 | 448 | public final class ExtractingId { 449 | 450 | private final Function idExtractor; 451 | 452 | private ExtractingId(Function idExtractor) { 453 | this.idExtractor = idExtractor; 454 | } 455 | 456 | public SimpleModelBuilder to(Object idNamespace) { 457 | tryCheckConflict(); 458 | idExtractors.put(objType, obj -> { 459 | Object rawId = idExtractor.apply((E) obj); 460 | Set ids; 461 | if (rawId == null) { 462 | ids = emptySet(); 463 | } else { 464 | if (rawId instanceof Iterable) { 465 | ids = Sets.newHashSet((Iterable) rawId); 466 | } else { 467 | ids = singleton(rawId); 468 | } 469 | } 470 | return new KeyPair<>(idNamespace, ids); 471 | }); 472 | cachedIdExtractors.clear(); 473 | return SimpleModelBuilder.this; 474 | } 475 | } 476 | } 477 | 478 | @Deprecated 479 | public final class BuildingBuilder { 480 | 481 | private final Object idNamespace; 482 | 483 | private BuildingBuilder(Object idNamespace) { 484 | this.idNamespace = idNamespace; 485 | } 486 | 487 | @SuppressWarnings("rawtypes") 488 | public BuildingValue by(Function, Map> valueBuilder) { 489 | return new BuildingValue<>((c, ids) -> (Map) valueBuilder.apply(ids)); 490 | } 491 | 492 | @SuppressWarnings("rawtypes") 493 | public BuildingValue by(BiFunction, Map> valueBuilder) { 494 | return new BuildingValue<>((BiFunction) valueBuilder); 495 | } 496 | 497 | public final class BuildingValue { 498 | 499 | private final BiFunction, Map> valueBuilderFunction; 500 | 501 | private BuildingValue( 502 | BiFunction, Map> valueBuilderFunction) { 503 | this.valueBuilderFunction = valueBuilderFunction; 504 | } 505 | 506 | @SuppressWarnings("rawtypes") 507 | public SimpleModelBuilder to(Object valueNamespace) { 508 | tryCheckConflict(); 509 | valueBuilders.put(idNamespace, new KeyPair(valueNamespace, valueBuilderFunction)); 510 | return SimpleModelBuilder.this; 511 | } 512 | } 513 | } 514 | } 515 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/model/builder/util/MergeUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.model.builder.util; 2 | 3 | import java.util.Map; 4 | import java.util.Set; 5 | 6 | /** 7 | * @author w.vela 8 | */ 9 | public final class MergeUtils { 10 | 11 | private MergeUtils() { 12 | throw new UnsupportedOperationException(); 13 | } 14 | 15 | public static Map merge(Map map1, Map map2) { 16 | map1.putAll(map2); 17 | return map1; 18 | } 19 | 20 | public static Set merge(Set set1, Set set2) { 21 | set1.addAll(set2); 22 | return set1; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/view/mapper/ViewMapper.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.view.mapper; 2 | 3 | import static java.util.stream.Collectors.toList; 4 | 5 | import java.util.Collection; 6 | import java.util.List; 7 | 8 | /** 9 | * @author w.vela 10 | */ 11 | public interface ViewMapper { 12 | 13 | V map(M model, B buildContext); 14 | 15 | default List map(Collection models, B buildContext) { 16 | return models.stream().map(i -> this. map(i, buildContext)).collect(toList()); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/view/mapper/impl/DefaultViewMapperImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.view.mapper.impl; 2 | 3 | import static org.apache.commons.lang3.ClassUtils.getAllInterfaces; 4 | import static org.apache.commons.lang3.ClassUtils.getAllSuperclasses; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | import java.util.concurrent.ConcurrentMap; 10 | import java.util.function.BiFunction; 11 | 12 | import com.github.phantomthief.model.builder.context.BuildContext; 13 | import com.github.phantomthief.view.mapper.ViewMapper; 14 | 15 | /** 16 | *

DefaultViewMapperImpl class.

17 | * 18 | * @author w.vela 19 | * @version $Id: $Id 20 | */ 21 | public class DefaultViewMapperImpl implements ViewMapper { 22 | 23 | private final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(getClass()); 24 | 25 | private final Map, BiFunction> mappers = new HashMap<>(); 26 | private final ConcurrentMap, BiFunction> modelTypeCache = new ConcurrentHashMap<>(); 27 | 28 | /** {@inheritDoc} */ 29 | @SuppressWarnings({ "unchecked" }) 30 | @Override 31 | public V map(M model, B buildContext) { 32 | return (V) getMapper(model.getClass()).apply(buildContext, model); 33 | } 34 | 35 | @SuppressWarnings("rawtypes") 36 | private BiFunction getMapper(Class modelType) { 37 | return modelTypeCache.computeIfAbsent(modelType, t -> { 38 | BiFunction result = mappers.get(t); 39 | if (result == null) { 40 | for (Class c : getAllInterfaces(t)) { 41 | result = mappers.get(c); 42 | if (result != null) { 43 | return result; 44 | } 45 | } 46 | for (Class c : getAllSuperclasses(t)) { 47 | result = mappers.get(c); 48 | if (result != null) { 49 | return result; 50 | } 51 | } 52 | } 53 | if (result == null) { 54 | logger.warn("cannot found model's view:{}", modelType); 55 | } 56 | return result; 57 | }); 58 | } 59 | 60 | /** 61 | *

addMapper.

62 | * 63 | * @param modelType a {@link java.lang.Class} object. 64 | * @param viewFactory a {@link java.util.function.BiFunction} object. 65 | * @param a M object. 66 | * @param a V object. 67 | * @return a {@link com.github.phantomthief.view.mapper.impl.DefaultViewMapperImpl} object. 68 | */ 69 | public DefaultViewMapperImpl addMapper(Class modelType, 70 | BiFunction viewFactory) { 71 | mappers.put(modelType, viewFactory); 72 | return this; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/view/mapper/impl/ForwardingViewMapper.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.view.mapper.impl; 2 | 3 | import com.github.phantomthief.view.mapper.ViewMapper; 4 | 5 | /** 6 | *

Abstract ForwardingViewMapper class.

7 | * 8 | * @author w.vela 9 | * @version $Id: $Id 10 | */ 11 | public abstract class ForwardingViewMapper implements ViewMapper { 12 | 13 | private final ViewMapper delegate; 14 | 15 | /** 16 | *

Constructor for ForwardingViewMapper.

17 | * 18 | * @param delegate a {@link com.github.phantomthief.view.mapper.ViewMapper} object. 19 | */ 20 | protected ForwardingViewMapper(ViewMapper delegate) { 21 | this.delegate = delegate; 22 | } 23 | 24 | /** 25 | *

map.

26 | * 27 | * @param model a M object. 28 | * @param buildContext a B object. 29 | * @param a M object. 30 | * @param a V object. 31 | * @param a B object. 32 | * @return a V object. 33 | */ 34 | public V map(M model, B buildContext) { 35 | return delegate.map(model, buildContext); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/github/phantomthief/view/mapper/impl/OverrideViewMapper.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.view.mapper.impl; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.concurrent.ConcurrentHashMap; 6 | import java.util.concurrent.ConcurrentMap; 7 | import java.util.function.BiFunction; 8 | 9 | import org.apache.commons.lang3.ClassUtils; 10 | 11 | import com.github.phantomthief.view.mapper.ViewMapper; 12 | 13 | /** 14 | * @author w.vela 15 | */ 16 | public class OverrideViewMapper extends ForwardingViewMapper { 17 | 18 | private final Map, BiFunction> overrideMappers = new HashMap<>(); 19 | private final ConcurrentMap, BiFunction> modelTypeCache = new ConcurrentHashMap<>(); 20 | 21 | public OverrideViewMapper(ViewMapper delegate) { 22 | super(delegate); 23 | } 24 | 25 | public OverrideViewMapper addMapper(Class type, BiFunction mapper) { 26 | overrideMappers.put(type, mapper); 27 | return this; 28 | } 29 | 30 | @SuppressWarnings({ "rawtypes", "unchecked" }) 31 | @Override 32 | public V map(M model, B buildContext) { 33 | BiFunction mapper = getMapper(model.getClass()); 34 | if (mapper != null) { 35 | return (V) mapper.apply(model, buildContext); 36 | } else { 37 | return super.map(model, buildContext); 38 | } 39 | } 40 | 41 | @SuppressWarnings("rawtypes") 42 | private BiFunction getMapper(Class modelType) { 43 | BiFunction biFunction = modelTypeCache.get(modelType); 44 | if (biFunction != null) { 45 | return biFunction; 46 | } 47 | return modelTypeCache.computeIfAbsent(modelType, t -> { 48 | BiFunction result = overrideMappers.get(t); 49 | if (result == null) { 50 | for (Class c : ClassUtils.getAllInterfaces(t)) { 51 | result = overrideMappers.get(c); 52 | if (result != null) { 53 | return result; 54 | } 55 | } 56 | for (Class c : ClassUtils.getAllSuperclasses(t)) { 57 | result = overrideMappers.get(c); 58 | if (result != null) { 59 | return result; 60 | } 61 | } 62 | } 63 | return result; 64 | }); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/model/builder/ModelBuilderConflictTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.model.builder; 2 | 3 | import com.github.phantomthief.model.builder.impl.SimpleModelBuilder; 4 | import com.github.phantomthief.model.builder.model.User; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Collection; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | import static com.github.phantomthief.model.builder.impl.LazyBuilder.on; 13 | import static org.junit.jupiter.api.Assertions.assertTrue; 14 | 15 | /** 16 | * @author w.vela 17 | */ 18 | class ModelBuilderConflictTest { 19 | 20 | private SimpleModelBuilder builder; 21 | 22 | @Test 23 | void testBuild1() { 24 | TestBuildContext buildContext = new TestBuildContext(1); 25 | boolean[] conflict = {false}; 26 | builder = new SimpleModelBuilder() 27 | .onConflictCheckListener(() -> conflict[0] = true) 28 | .lazy(on(User.class, 29 | (TestBuildContext context, Collection ids) -> Collections.emptyMap(), 30 | "isFans")); 31 | List sources = new ArrayList<>(); 32 | builder.buildSingle(null, buildContext); 33 | builder.lazy(on(User.class, 34 | (TestBuildContext context, Collection ids) -> Collections.emptyMap(), 35 | "isFans2")); 36 | assertTrue(conflict[0]); 37 | } 38 | 39 | @Test 40 | void testBuild2() { 41 | TestBuildContext buildContext = new TestBuildContext(1); 42 | boolean[] conflict = {false}; 43 | builder = new SimpleModelBuilder() 44 | .onConflictCheckListener(() -> conflict[0] = true) 45 | .extractId(Object.class, it -> it, String.class); 46 | List sources = new ArrayList<>(); 47 | builder.buildSingle(null, buildContext); 48 | builder.extractId(Object.class, it -> it, String.class); 49 | assertTrue(conflict[0]); 50 | } 51 | 52 | @Test 53 | void testBuild3() { 54 | TestBuildContext buildContext = new TestBuildContext(1); 55 | boolean[] conflict = {false}; 56 | builder = new SimpleModelBuilder() 57 | .onConflictCheckListener(() -> conflict[0] = true) 58 | .extractValue(Object.class, it -> it, it -> it, String.class); 59 | List sources = new ArrayList<>(); 60 | builder.buildSingle(null, buildContext); 61 | builder.extractValue(Object.class, it -> it, it -> it, String.class); 62 | assertTrue(conflict[0]); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/model/builder/ModelBuilderTest.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.model.builder; 2 | 3 | import static com.github.phantomthief.model.builder.impl.LazyBuilder.on; 4 | import static java.util.Collections.emptyMap; 5 | import static java.util.Collections.singletonList; 6 | import static java.util.function.Function.identity; 7 | import static java.util.stream.Collectors.toList; 8 | import static java.util.stream.Collectors.toMap; 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | import static org.junit.jupiter.api.Assertions.assertFalse; 11 | import static org.junit.jupiter.api.Assertions.assertNotNull; 12 | import static org.junit.jupiter.api.Assertions.assertTrue; 13 | import static org.slf4j.LoggerFactory.getLogger; 14 | 15 | import java.util.ArrayList; 16 | import java.util.Arrays; 17 | import java.util.Collection; 18 | import java.util.HashSet; 19 | import java.util.List; 20 | import java.util.Map; 21 | import java.util.Set; 22 | 23 | import org.junit.jupiter.api.BeforeEach; 24 | import org.junit.jupiter.api.Test; 25 | import org.slf4j.Logger; 26 | 27 | import com.github.phantomthief.model.builder.context.impl.SimpleBuildContext; 28 | import com.github.phantomthief.model.builder.impl.SimpleModelBuilder; 29 | import com.github.phantomthief.model.builder.model.Comment; 30 | import com.github.phantomthief.model.builder.model.Fake; 31 | import com.github.phantomthief.model.builder.model.HasUser; 32 | import com.github.phantomthief.model.builder.model.Post; 33 | import com.github.phantomthief.model.builder.model.SubUser; 34 | import com.github.phantomthief.model.builder.model.User; 35 | import com.google.common.collect.HashMultimap; 36 | import com.google.common.collect.ImmutableList; 37 | import com.google.common.collect.Maps; 38 | import com.google.common.collect.Multimap; 39 | 40 | /** 41 | * @author w.vela 42 | */ 43 | class ModelBuilderTest { 44 | 45 | private static Logger logger = getLogger(ModelBuilderTest.class); 46 | private TestDAO testDAO; 47 | private ModelBuilder builder; 48 | 49 | @BeforeEach 50 | void setup() { 51 | testDAO = new TestDAO(); 52 | builder = new SimpleModelBuilder() 53 | .self(User.class, User::getId) 54 | .self(Post.class, Post::getId) 55 | .self(Comment.class, Comment::getId) 56 | .on(Comment.class).id(Comment::getAtUserIds).to(User.class) 57 | .on(HasUser.class).id(HasUser::getUserId).to(User.class) 58 | .on(Post.class).value(Post::comments, Comment::getId).to(Comment.class) 59 | .build(User.class, testDAO::getUsers).build(Post.class, testDAO::getPosts) 60 | .build(Comment.class, testDAO::getComments).build(User.class) 61 | .by((TestBuildContext context, Collection ids) -> testDAO 62 | .isFollowing(context.getVisitorId(), ids)) 63 | .to("isFollowing") 64 | .lazy(on(User.class, 65 | (TestBuildContext context, Collection ids) -> testDAO 66 | .isFans(context.getVisitorId(), ids), 67 | "isFans")) 68 | .lazyBuild(User.class, (TestBuildContext context, Collection ids) -> { 69 | Map fans = testDAO.isFans(context.getVisitorId(), ids); 70 | logger.debug("build fans for:{}->{}, result:{}", context.getVisitorId(), ids, 71 | fans); 72 | return fans; 73 | }, "isFans3") 74 | .lazy(on(Fake.class, 75 | (TestBuildContext context, Collection ids) -> testDAO 76 | .isFans(context.getVisitorId(), ids), 77 | "unreachedLazy")) 78 | .lazy(on(Fake.class, (TestBuildContext context, Collection ids) -> { 79 | context.getData("unreachedLazy"); 80 | return testDAO.isFans(context.getVisitorId(), ids); 81 | }, "unreachedLazy2")) 82 | ; 83 | System.out.println("builder===>"); 84 | System.out.println(builder); 85 | } 86 | 87 | @Test 88 | void testBuild() { 89 | TestBuildContext buildContext = new TestBuildContext(1); 90 | List sources = new ArrayList<>(); 91 | Collection posts = testDAO.getPosts(Arrays.asList(1L, 2L, 3L)).values(); 92 | posts.forEach(post -> post.setComments( 93 | testDAO.getComments(post.getCommentIds()).values().stream().collect(toList()))); 94 | sources.addAll(posts); 95 | sources.addAll(testDAO.getComments(singletonList(3L)).values()); 96 | sources.add(new SubUser(98)); 97 | logger.info("sources===>"); 98 | sources.forEach(o -> logger.info("{}", o)); 99 | testDAO.assertOn(); 100 | builder.buildMulti(sources, buildContext); 101 | logger.info("buildContext===>"); 102 | logger.info("{}", buildContext); 103 | 104 | assertTrue(testDAO.retrievedFansUserIds.isEmpty()); 105 | 106 | Map isFans = buildContext.getData("isFans"); 107 | logger.info("isFans:{}", isFans); 108 | isFans.forEach((userId, value) -> assertEquals( 109 | testDAO.fansMap.get(buildContext.getVisitorId()).contains(userId), value)); 110 | assertFalse(testDAO.retrievedFansUserIds.isEmpty()); 111 | logger.info("retry fans"); 112 | buildContext.getData("isFans"); 113 | logger.info("doing merge"); 114 | buildContext.merge(new SimpleBuildContext()); 115 | testDAO.retrievedFansUserIds.clear(); 116 | logger.info("isFans:{}", buildContext.getData("isFans")); 117 | 118 | // try assert 119 | for (Object obj : sources) { 120 | if (obj instanceof Post) { 121 | Post post = (Post) obj; 122 | assertEquals(buildContext.getData(Post.class).get(post.getId()), obj); 123 | assertEquals(post.getUserId(), 124 | buildContext.getData(User.class).get(post.getUserId()).getId()); 125 | for (Comment cmt : post.comments()) { 126 | assertCmt(buildContext, cmt); 127 | } 128 | } 129 | if (obj instanceof Comment) { 130 | Comment cmt = (Comment) obj; 131 | assertCmt(buildContext, cmt); 132 | } 133 | if (obj instanceof User) { 134 | User user = (User) obj; 135 | assertUser(buildContext, user); 136 | } 137 | } 138 | 139 | Map unreachedLazy = buildContext.getData("unreachedLazy2"); 140 | assertTrue(unreachedLazy.isEmpty()); 141 | assertFalse(unreachedLazy.getOrDefault(1L, false)); 142 | 143 | logger.info("checking nodes."); 144 | buildContext.getData(User.class).values().forEach(user -> assertUser(buildContext, user)); 145 | logger.info("fin."); 146 | } 147 | 148 | @Test 149 | void testNullBuild() { 150 | TestBuildContext buildContext = new TestBuildContext(1); 151 | builder.buildSingle(null, buildContext); 152 | buildContext.getData("t").put("a", "c"); 153 | System.out.println("checking..."); 154 | Map isFans = buildContext.getData("isFans3"); 155 | assertFalse(isFans.getOrDefault(1, false)); 156 | System.out.println("fin."); 157 | } 158 | 159 | @Test 160 | void testMerge() { 161 | TestBuildContext buildContext = new TestBuildContext(1); 162 | List users = new ArrayList<>(testDAO.getUsers(ImmutableList.of(1, 2, 3)).values()); 163 | builder.buildMulti(users, buildContext); 164 | Map isFans = buildContext.getData("isFans3"); 165 | System.out.println("isFans:" + isFans); 166 | users.forEach(user -> assertNotNull(isFans.get(user.getId()))); 167 | 168 | TestBuildContext other = new TestBuildContext(1); 169 | List users2 = new ArrayList<>(testDAO.getUsers(ImmutableList.of(3, 4, 5)).values()); 170 | builder.buildMulti(users2, other); 171 | Map isFans2 = other.getData("isFans3"); 172 | System.out.println("isFans2:" + isFans2); 173 | users2.forEach(user -> assertNotNull(isFans2.get(user.getId()))); 174 | 175 | buildContext.merge(other); 176 | System.out.println("after merged."); 177 | System.out.println("users:" + buildContext.getData(User.class)); 178 | 179 | Map merged = buildContext.getData("isFans3"); 180 | System.out.println("merged:" + merged); 181 | for (int i = 1; i <= 5; i++) { 182 | assertNotNull(merged.get(i)); 183 | } 184 | System.out.println("fin."); 185 | } 186 | 187 | @Test 188 | void testDuplicateMerge() { 189 | TestBuildContext mainBuildContext = new TestBuildContext(1); 190 | 191 | TestBuildContext buildContext = new TestBuildContext(1); 192 | builder.buildMulti(emptyMap().values(), buildContext); 193 | mainBuildContext.merge(buildContext); 194 | 195 | TestBuildContext buildContext2 = new TestBuildContext(1); 196 | Map byIdsFailFast = testDAO.getUsers(ImmutableList.of(1, 2)); 197 | builder.buildMulti(byIdsFailFast.values(), buildContext2); 198 | Map isFans3 = buildContext2.getData("isFans3"); 199 | System.out.println("[test] " + isFans3); 200 | assertFalse(isFans3.isEmpty()); 201 | 202 | mainBuildContext.merge(buildContext2); 203 | 204 | isFans3 = mainBuildContext.getData("isFans3"); 205 | System.out.println("[test] " + isFans3); 206 | assertFalse(isFans3.isEmpty()); 207 | } 208 | 209 | private void assertUser(TestBuildContext buildContext, User user) { 210 | assertNotNull(buildContext.getData("isFollowing").get(user.getId())); 211 | } 212 | 213 | private void assertCmt(TestBuildContext buildContext, Comment cmt) { 214 | assertEquals(buildContext.getData(Comment.class).get(cmt.getId()), cmt); 215 | assertEquals(cmt.getUserId(), 216 | buildContext.getData(User.class).get(cmt.getUserId()).getId()); 217 | if (cmt.getAtUserIds() != null) { 218 | for (Integer atUserId : cmt.getAtUserIds()) { 219 | assertEquals(atUserId, buildContext.getData(User.class).get(atUserId).getId()); 220 | } 221 | } 222 | } 223 | 224 | private class TestDAO { 225 | 226 | private static final int USER_MAX = 100; 227 | private final Map posts = ImmutableList 228 | .of(new Post(1, 1, null), 229 | new Post(2, 1, Arrays.asList(1L, 2L, 3L)), 230 | new Post(3, 2, Arrays.asList(4L, 5L))) 231 | .stream().collect(toMap(Post::getId, identity())); 232 | 233 | private final Map cmts = ImmutableList 234 | .of(new Comment(1, 1, null), new Comment(2, 2, null), new Comment(3, 1, null), 235 | new Comment(4, 2, Arrays.asList(2, 3)), 236 | new Comment(5, 11, Arrays.asList(2, 99))) 237 | .stream().collect(toMap(Comment::getId, identity())); 238 | 239 | private final Multimap followingMap = HashMultimap.create(); 240 | private final Multimap fansMap = HashMultimap.create(); 241 | private Set retreievedUserIds; 242 | private Set retreievedPostIds; 243 | private Set retreievedCommentIds; 244 | private Set retrievedFollowUserIds; 245 | private Set retrievedFansUserIds; 246 | 247 | { 248 | followingMap.put(1, 5); 249 | followingMap.put(1, 2); 250 | } 251 | 252 | { 253 | fansMap.put(1, 5); 254 | fansMap.put(1, 99); 255 | } 256 | 257 | Map getUsers(Collection ids) { 258 | if (retreievedUserIds != null) { 259 | logger.info("try to get users:{}", ids); 260 | for (Integer id : ids) { 261 | assertTrue(retreievedUserIds.add(id)); 262 | } 263 | } 264 | return ids.stream().filter(i -> i <= USER_MAX).collect(toMap(identity(), User::new)); 265 | } 266 | 267 | Map getPosts(Collection ids) { 268 | if (retreievedPostIds != null) { 269 | logger.info("try to get posts:{}", ids); 270 | for (Long id : ids) { 271 | assertTrue(retreievedPostIds.add(id)); 272 | } 273 | } 274 | return Maps.filterKeys(posts, ids::contains); 275 | } 276 | 277 | Map getComments(Collection ids) { 278 | if (ids == null) { 279 | return emptyMap(); 280 | } 281 | if (retreievedCommentIds != null) { 282 | logger.info("try to get cmts:{}", ids); 283 | for (Long id : ids) { 284 | assertTrue(retreievedCommentIds.add(id)); 285 | } 286 | } 287 | return Maps.filterKeys(cmts, ids::contains); 288 | } 289 | 290 | Map isFollowing(int fromUserId, Collection ids) { 291 | if (retrievedFollowUserIds != null) { 292 | logger.info("try to get followings:{}->{}", fromUserId, ids); 293 | for (Integer id : ids) { 294 | assertTrue(retrievedFollowUserIds.add(id)); 295 | } 296 | } 297 | Collection followings = followingMap.get(fromUserId); 298 | return ids.stream().collect(toMap(identity(), followings::contains)); 299 | } 300 | 301 | Map isFans(int fromUserId, Collection ids) { 302 | if (retrievedFansUserIds != null) { 303 | logger.info("try to get fans:{}->{}", fromUserId, ids); 304 | for (Integer id : ids) { 305 | assertTrue(retrievedFansUserIds.add(id)); 306 | } 307 | } 308 | Collection fans = fansMap.get(fromUserId); 309 | return ids.stream().collect(toMap(identity(), fans::contains)); 310 | } 311 | 312 | void assertOn() { 313 | logger.info("assert on."); 314 | retreievedUserIds = new HashSet<>(); 315 | retreievedPostIds = new HashSet<>(); 316 | retreievedCommentIds = new HashSet<>(); 317 | retrievedFollowUserIds = new HashSet<>(); 318 | retrievedFansUserIds = new HashSet<>(); 319 | } 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/model/builder/TestBuildContext.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.model.builder; 2 | 3 | import java.util.concurrent.ConcurrentHashMap; 4 | 5 | import com.github.phantomthief.model.builder.context.impl.SimpleBuildContext; 6 | import com.github.phantomthief.model.builder.util.ToStringUtils; 7 | 8 | /** 9 | * @author w.vela 10 | */ 11 | public class TestBuildContext extends SimpleBuildContext { 12 | 13 | private final int visitorId; 14 | 15 | TestBuildContext(int visitorId) { 16 | super(new ConcurrentHashMap<>(1, 0.75F, 2)); 17 | this.visitorId = visitorId; 18 | } 19 | 20 | public int getVisitorId() { 21 | return visitorId; 22 | } 23 | 24 | @Override 25 | public String toString() { 26 | return ToStringUtils.toString(this); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/model/builder/model/Comment.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.model.builder.model; 2 | 3 | import java.util.List; 4 | 5 | import com.github.phantomthief.model.builder.util.ToStringUtils; 6 | 7 | /** 8 | * @author w.vela 9 | */ 10 | public class Comment implements HasId, HasUser { 11 | 12 | private final long id; 13 | private final int userId; 14 | private final List atUserIds; 15 | 16 | public Comment(long id, int userId, List atUserIds) { 17 | this.id = id; 18 | this.userId = userId; 19 | this.atUserIds = atUserIds; 20 | } 21 | 22 | @Override 23 | public Integer getUserId() { 24 | return userId; 25 | } 26 | 27 | @Override 28 | public Long getId() { 29 | return id; 30 | } 31 | 32 | public List getAtUserIds() { 33 | return atUserIds; 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | return ToStringUtils.toString(this); 39 | } 40 | 41 | @Override 42 | public int hashCode() { 43 | final int prime = 31; 44 | int result = 1; 45 | result = prime * result + (int) (id ^ (id >>> 32)); 46 | return result; 47 | } 48 | 49 | @Override 50 | public boolean equals(Object obj) { 51 | if (this == obj) { 52 | return true; 53 | } 54 | if (obj == null) { 55 | return false; 56 | } 57 | if (!(obj instanceof Comment)) { 58 | return false; 59 | } 60 | Comment other = (Comment) obj; 61 | return id == other.id; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/model/builder/model/Fake.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.model.builder.model; 2 | 3 | /** 4 | * @author w.vela 5 | * Created on 16/3/21. 6 | */ 7 | public class Fake { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/model/builder/model/HasId.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.model.builder.model; 2 | 3 | /** 4 | * @author w.vela 5 | */ 6 | public interface HasId { 7 | 8 | T getId(); 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/model/builder/model/HasUser.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.model.builder.model; 2 | 3 | /** 4 | * @author w.vela 5 | */ 6 | public interface HasUser { 7 | 8 | Integer getUserId(); 9 | } 10 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/model/builder/model/Post.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.model.builder.model; 2 | 3 | import java.util.List; 4 | 5 | import com.fasterxml.jackson.annotation.JsonIgnore; 6 | import com.github.phantomthief.model.builder.util.ToStringUtils; 7 | 8 | /** 9 | * @author w.vela 10 | */ 11 | public class Post implements HasUser, HasId { 12 | 13 | private final long id; 14 | private final int userId; 15 | private final List commentIds; 16 | @JsonIgnore 17 | private List comments; 18 | 19 | public Post(long id, int userId, List commentIds) { 20 | this.id = id; 21 | this.userId = userId; 22 | this.commentIds = commentIds; 23 | } 24 | 25 | @Override 26 | public Long getId() { 27 | return id; 28 | } 29 | 30 | @Override 31 | public Integer getUserId() { 32 | return userId; 33 | } 34 | 35 | public List comments() { 36 | return comments; 37 | } 38 | 39 | public List getCommentIds() { 40 | return commentIds; 41 | } 42 | 43 | public void setComments(List comments) { 44 | this.comments = comments; 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | return ToStringUtils.toString(this); 50 | } 51 | 52 | @Override 53 | public int hashCode() { 54 | final int prime = 31; 55 | int result = 1; 56 | result = prime * result + (int) (id ^ (id >>> 32)); 57 | return result; 58 | } 59 | 60 | @Override 61 | public boolean equals(Object obj) { 62 | if (this == obj) { 63 | return true; 64 | } 65 | if (obj == null) { 66 | return false; 67 | } 68 | if (!(obj instanceof Post)) { 69 | return false; 70 | } 71 | Post other = (Post) obj; 72 | return id == other.id; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/model/builder/model/SubUser.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.model.builder.model; 2 | 3 | /** 4 | * @author w.vela 5 | */ 6 | public class SubUser extends User { 7 | 8 | public SubUser(int id) { 9 | super(id); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/model/builder/model/User.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.model.builder.model; 2 | 3 | import com.github.phantomthief.model.builder.util.ToStringUtils; 4 | 5 | /** 6 | * @author w.vela 7 | */ 8 | public class User implements HasId { 9 | 10 | private final int id; 11 | 12 | public User(int id) { 13 | this.id = id; 14 | } 15 | 16 | @Override 17 | public Integer getId() { 18 | return id; 19 | } 20 | 21 | @Override 22 | public String toString() { 23 | return ToStringUtils.toString(this); 24 | } 25 | 26 | @Override 27 | public int hashCode() { 28 | final int prime = 31; 29 | int result = 1; 30 | result = prime * result + id; 31 | return result; 32 | } 33 | 34 | @Override 35 | public boolean equals(Object obj) { 36 | if (this == obj) { 37 | return true; 38 | } 39 | if (obj == null) { 40 | return false; 41 | } 42 | if (!(obj instanceof User)) { 43 | return false; 44 | } 45 | User other = (User) obj; 46 | return id == other.id; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/com/github/phantomthief/model/builder/util/ToStringUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.phantomthief.model.builder.util; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.databind.SerializationFeature; 6 | 7 | /** 8 | * @author w.vela 9 | */ 10 | public class ToStringUtils { 11 | 12 | private static ObjectMapper objectMapper = new ObjectMapper(); 13 | 14 | static { 15 | objectMapper.enable(SerializationFeature.INDENT_OUTPUT); 16 | } 17 | 18 | public static String toString(Object obj) { 19 | try { 20 | return obj.getClass().getSimpleName() + "=>" + objectMapper.writeValueAsString(obj); 21 | } catch (JsonProcessingException e) { 22 | throw new RuntimeException(e); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | --------------------------------------------------------------------------------