├── .gitignore ├── LICENSE ├── README.md ├── pom.xml ├── rahub-composite-service ├── .gitignore ├── pom.xml └── src │ └── main │ ├── java │ └── net │ │ └── jotorren │ │ └── microservices │ │ ├── RahubCompositeServiceApplication.java │ │ ├── composite │ │ ├── controller │ │ │ └── CompositeController.java │ │ ├── domain │ │ │ ├── CompositeData.java │ │ │ └── CompositeForum.java │ │ └── service │ │ │ └── CompositeService.java │ │ ├── configuration │ │ ├── CompositeTransactionConfiguration.java │ │ └── WebServicesConfiguration.java │ │ └── tcc │ │ ├── TccRestCoordinator.java │ │ └── TccRestInitializer.java │ └── resources │ ├── application.properties │ └── static │ └── swagger-tcc.json ├── rahub-cross-cutting-concerns ├── .gitignore ├── pom.xml └── src │ └── main │ └── java │ └── net │ └── jotorren │ └── microservices │ ├── context │ ├── SpringContext.java │ ├── SpringContextProvider.java │ └── ThreadLocalContext.java │ ├── rs │ └── ExceptionRestHandler.java │ └── tx │ ├── AbstractEntityCommandJsonSerializer.java │ ├── ChangeStateJpaListener.java │ ├── CompositeTransaction.java │ ├── CompositeTransactionException.java │ ├── CompositeTransactionManager.java │ ├── CompositeTransactionParticipantController.java │ ├── CompositeTransactionParticipantDao.java │ ├── CompositeTransactionParticipantService.java │ ├── EntityCommand.java │ ├── Serializer.java │ └── impl │ ├── CompositeTransactionManagerKafkaImpl.java │ └── CompositeTransactionManagerMemoryImpl.java ├── rahub-forum-service ├── .gitignore ├── pom.xml └── src │ └── main │ ├── java │ └── net │ │ └── jotorren │ │ └── microservices │ │ └── forum │ │ ├── RahubForumServiceApplication.java │ │ ├── configuration │ │ ├── CompositeTransactionConfiguration.java │ │ ├── H2DatabaseConfiguration.java │ │ └── WebServicesConfiguration.java │ │ ├── controller │ │ └── ForumController.java │ │ ├── dao │ │ ├── ForumDao.java │ │ └── ForumTransactionAwareDao.java │ │ ├── domain │ │ ├── Forum.java │ │ └── ForumSerializer.java │ │ └── service │ │ └── ForumService.java │ └── resources │ ├── META-INF │ └── orm.xml │ ├── application.properties │ └── static │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── index.html │ ├── oauth2-redirect.html │ ├── swagger-ui-bundle.js │ ├── swagger-ui-bundle.js.map │ ├── swagger-ui-standalone-preset.js │ ├── swagger-ui-standalone-preset.js.map │ ├── swagger-ui.css │ ├── swagger-ui.css.map │ ├── swagger-ui.js │ └── swagger-ui.js.map └── rahub-source-code-service ├── .gitignore ├── pom.xml └── src └── main ├── java └── net │ └── jotorren │ └── microservices │ └── content │ ├── RahubContentServiceApplication.java │ ├── configuration │ ├── CompositeTransactionConfiguration.java │ ├── H2DatabaseConfiguration.java │ └── WebServicesConfiguration.java │ ├── controller │ └── ContentController.java │ ├── dao │ ├── ContentDao.java │ └── ContentTransactionAwareDao.java │ ├── domain │ ├── SourceCodeItem.java │ └── SourceCodeItemSerializer.java │ └── service │ └── ContentService.java └── resources ├── META-INF └── orm.xml ├── application.properties └── static ├── favicon-16x16.png ├── favicon-32x32.png ├── index.html ├── oauth2-redirect.html ├── swagger-ui-bundle.js ├── swagger-ui-bundle.js.map ├── swagger-ui-standalone-preset.js ├── swagger-ui-standalone-preset.js.map ├── swagger-ui.css ├── swagger-ui.css.map ├── swagger-ui.js └── swagger-ui.js.map /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | tmp 4 | target 5 | 6 | .settings 7 | .classpath 8 | .project 9 | 10 | .vscode 11 | 12 | # Ignore Test reporters 13 | ####################### 14 | test 15 | coverage 16 | report 17 | doc 18 | -------------------------------------------------------------------------------- /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, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microservices and data consistency (I) 2 | 3 | As you can read in [Christian Posta's excellent article](http://blog.christianposta.com/microservices/the-hardest-part-about-microservices-data/), when designing a microservices-based solution **our first choice to solve consistency between bounded contexts will be to communicate boundaries with immutable point in time events** (by means of a messaging queue/listener, a dedicated event store/publish-subscribe topic or a database/replicated log/event processor). 4 | 5 | ¿But how to deal with situations where, inevitably, we must update data from different contexts in a single transaction either across a single database or multiple databases? A combination of JPA 2.1 unsynchronized persistence contexts, JPA Entity listeners, Kafka and [Atomikos TCC](https://www.atomikos.com/Blog/TransactionManagementAPIForRESTTCC) could fit like a glove ;-) 6 | 7 | Let's describe that approach. We will start by introducing all the actors: 8 | 9 | - **Domain Services**. Each of the stateless and autonomous pieces that the whole system has been divided into. 10 | - **Composite Services**. Coarse-grained service operations which are composed by many calls to one or more domain services. 11 | - **Command**. Data describing a persistence operation performed by a domain service: "*an operation on a given entity within certain context*" 12 | - **Composite transaction**. Set of commands that must be grouped and carried out together. 13 | - **Coordinator**. Service to manage composite transactions lifecycle, deciding whether or not changes (commands) must be applied to the corresponding underlying repositories. 14 | - **TCC Service**. *Try*-*Cancel*/*Confirm* protocol implementation. It handles all TCC remote calls verifying no transaction timeout has been exceeded. 15 | - **Distributed, replicated event log**. Distributed store of composite transactions accessible by any service instance (domain, composite or coordinator) 16 | 17 | I would like to point out that Domain, Composite, Coordinator and TCC services have no 2PC/XA support and they can be dynamically allocated/destroyed. 18 | 19 | 20 | 21 | Regarding the sequence of actions: 22 | 23 | 1. A client makes a remote call to a composite service 24 | 2. The composite service knows which domain services needs to invoke and passes that information to the coordinator 25 | 3. The coordinator creates a composite transaction or, in other words, a persistent topic for each domain service involved in the operation. Every topic will be uniquely identified by a string that can be interpreted as a *partial transaction id* (partial because a topic will store only commands for instances of a single domain service) 26 | 4. The composite service calls each domain service using its respective *partial transaction id* 27 | 5. A domain service performs persistence operations through a JPA unsynchronized persistence context and publishes appropriate commands to the topic identified by the given *partial transaction id* 28 | 29 | ![producers](https://cloud.githubusercontent.com/assets/22961359/26069317/baa16904-39a0-11e7-91bd-b2d3bd75cf32.png) 30 | 31 | 32 | 33 | 1. If all domain services calls succeed, the composite service signals the coordinator to commit the changes 34 | - The coordinator calls the confirm operation on the TCC service 35 | - The TCC service calls the confirm operation on each domain service passing the correct *partial transaction id* 36 | - Each domain service reads all commands from the given topic, executes them through a JPA unsynchronized persistence context and finally applies the derived changes to the underlying repository. 37 | - If all commit calls succeed the business operation ends successfully, otherwise the operation ends with an heuristic failure 38 | 2. If a domain service call fails, the composite service signals the coordinator to rollback the changes 39 | - The coordinator calls the cancel operation on the TCC service 40 | - The TCC service calls the cancel operation on each domain service passing the correct *partial transaction id* 41 | - The business operation ends with error 42 | 43 | ![consumers](https://cloud.githubusercontent.com/assets/22961359/26069329/c3b944da-39a0-11e7-8916-a29e4df2e124.png) 44 | 45 | 46 | 47 | ## Build 48 | 49 | ```shell 50 | # clone this repo 51 | # --depth 1 removes all but one .git commit history 52 | 53 | git clone --depth 1 https://github.com/jotorren/microservices-transactions-tcc.git my-project 54 | 55 | # change directory to your project 56 | cd my-project 57 | 58 | # build artifacts 59 | mvn clean install 60 | ``` 61 | 62 | 63 | 64 | ## Run 65 | 66 | First of all you must download and install Zookeeper & Kafka servers. Please follow guidelines described in: 67 | 68 | - https://zookeeper.apache.org/doc/r3.1.2/zookeeperStarted.html 69 | - https://kafka.apache.org/quickstart 70 | 71 | Once both servers are up and running you can start all services: 72 | 73 | - Composite service to create source code items and discussion boards + TCC Service 74 | 75 | ```shell 76 | # inside your project home folder 77 | cd rahub-composite-service 78 | mvn spring-boot:run 79 | # default port 8090 80 | ``` 81 | 82 | - Domain service to create/query pieces of source code 83 | 84 | 85 | ```shell 86 | # inside your project home folder 87 | cd rahub-source-code-service 88 | mvn spring-boot:run 89 | # default port 8091 90 | ``` 91 | 92 | - Domain service to create/query discussion boards about source code items 93 | 94 | ```shell 95 | # inside your project home folder 96 | cd rahub-forum-service 97 | mvn spring-boot:run 98 | # default port 8092 99 | ``` 100 | 101 | 102 | 103 | ## Available services 104 | 105 | - `/api`: http://localhost:8090/api/api-docs?url=/api/swagger.json 106 | 107 | ![composite65](https://cloud.githubusercontent.com/assets/22961359/26103358/4ccbd47a-3a39-11e7-9eb9-8810d4efe123.png) 108 | 109 | 110 | 111 | - `/api/coordinator`: http://localhost:8090/api/api-docs?url=/swagger-tcc.json 112 | 113 | In the current example TCC service runs on the same JAX-RS container as the composite does, but it will be preferable to deploy it on its own instance. 114 | 115 | ![tcc-ops65](https://cloud.githubusercontent.com/assets/22961359/26151969/5c16e894-3b05-11e7-9e33-519ea8c3d9a8.png) 116 | 117 | 118 | 119 | - `/content`: http://localhost:8091/index.html?url=/content/swagger.json 120 | 121 | 122 | ![sourcecode65](https://cloud.githubusercontent.com/assets/22961359/26103359/4cce7978-3a39-11e7-82c3-baa7f9024696.png) 123 | 124 | 125 | 126 | - `/forum`: http://localhost:8092/index.html?url=/forum/swagger.json 127 | 128 | 129 | ![forum65](https://cloud.githubusercontent.com/assets/22961359/26103360/4cd31258-3a39-11e7-9624-c100d0622a5c.png) 130 | 131 | 132 | 133 | ## Considerations 134 | 135 | #### REST implementation 136 | 137 | In the example we use Jersey for Domain Services whilst Composite and TCC services rely on CXF. With regard to swagger ui, the former contain required static resources inside `src/main/resources/static` while the latter only depend on a [webjar](http://www.webjars.org/) and have an empty static folder. 138 | 139 | #### Repositories 140 | 141 | Our sample Domain Services use an embedded H2 file based database. You can check the configuration looking at their respective `src/main/resources/application.properties`. By default, both data models are initialized on startup, but that behavior can be disabled by uncommenting the following lines: 142 | 143 | ```properties 144 | #spring.jpa.generate-ddl: false 145 | #spring.jpa.hibernate.ddl-auto: none 146 | ``` 147 | 148 | Additionally, H2 web console is enabled in both cases and can be accessed through the URI `/h2/console`. 149 | 150 | 151 | 152 | ## Components 153 | 154 | ![Core classes](https://cloud.githubusercontent.com/assets/22961359/26158987/ae0acd88-3b1d-11e7-85a1-68ba872a3867.png) 155 | 156 | Pink classes are provided by [Atomikos](https://www.atomikos.com/Blog/TransactionManagementAPIForRESTTCC) and contain the TCC protocol implementation. Green ones are generic and reusable components to isolate and hide the complexity of composite transactions management. 157 | 158 | 159 | 160 | ## Implementation key aspects 161 | 162 | #### 1. Transactional persistence operations: unsynchronized persistence contexts 163 | 164 | Persistence operations executed inside a Composite Transaction are delegated to *unsynchronized entity manager*s: you can create, change and delete entities without doing any change to the repository until you force the `EntityManager` to join an existent `LOCAL/JTA` transaction (note the `@Transactional` annotation present in the `commit()` method ). 165 | 166 | ```java 167 | @Repository 168 | @Scope("prototype") 169 | public class CompositeTransactionParticipantDao { 170 | 171 | @PersistenceContext(type = PersistenceContextType.EXTENDED, 172 | synchronization = SynchronizationType.UNSYNCHRONIZED) 173 | private EntityManager em; 174 | 175 | @Transactional(readOnly=false) 176 | public void commit() { 177 | em.joinTransaction(); 178 | } 179 | 180 | public void save(Object entity) { 181 | em.persist(entity); 182 | } 183 | 184 | public T saveOrUpdate(T entity) { 185 | return em.merge(entity); 186 | } 187 | 188 | public void remove(Object entity) { 189 | em.remove(entity); 190 | } 191 | 192 | public T findOne(Class entityClass, Object pk){ 193 | return getEntityManager().find(entityClass, pk); 194 | } 195 | } 196 | ``` 197 | 198 | As stated in [Spring ORM documentation](http://docs.spring.io/spring/docs/current/spring-framework-reference/html/orm.html): 199 | 200 | > `PersistenceContextType.EXTENDED` is a completely different affair: This results in a so-called extended EntityManager, which is *not thread-safe* and hence must not be used in a concurrently accessed component such as a Spring-managed singleton bean 201 | 202 | This is the reason why we set `prototype` as scope for any `DAO` with an *unsynchronized persistence context* injected into it. 203 | 204 | And some final aspects to be aware of: 205 | 206 | Any call to the `executeUpdate()` method of a `Query` created through an *unsynchronized* `EntityManager` will fail reporting `javax.persistence.TransactionRequiredException: Executing an update/delete query`. Consequently, bulk update/delete operations are not supported. 207 | 208 | On the other hand, it is possible to create/execute a `Query` to look for data but, in that case, only already persisted (committed) entries are searchable. If you want to retrieve entities that have not yet been saved (committed) you must use `EntityManager` `find()` methods. 209 | 210 | Keep in mind that any repository constraint will be checked only when the `EntityManager` joins the transaction (that is during the *commit* phase). Therefore it will be preferable to implement as many validations as possible out of the repositories. In doing so, we can detect potential problems in a very early stage, increasing the overall performance and consistency of the system. 211 | 212 | 213 | 214 | #### 2. From persistence operation to Command: JPA entity listeners and callback methods 215 | 216 | *Default entity listeners* are listeners that should be applied to all entity classes. Currently, they can only be specified in a mapping XML that can be found in `src/main/resources/META-INF/orm.xml` 217 | 218 | *Callback* methods are user defined methods that are attached to entity lifecycle events and are invoked automatically by JPA when these events occur: 219 | 220 | - `@PrePersist` - before a new entity is persisted (added to the `EntityManager`). 221 | - `@PostPersist` - after storing a new entity in the database (during *commit* or *flush*). 222 | - `@PostLoad` - after an entity has been retrieved from the database. 223 | - `@PreUpdate` - when an entity is identified as modified by the `EntityManager`. 224 | - `@PostUpdate` - after updating an entity in the database (during *commit* or *flush*). 225 | - `@PreRemove` - when an entity is marked for removal in the `EntityManager`. 226 | - `@PostRemove` - after deleting an entity from the database (during *commit* or *flush*). 227 | 228 | (For further details see http://www.objectdb.com/java/jpa/persistence/event) 229 | 230 | If we want to find out which entities have been created, updated or removed through an *unsynchronized entity manager*, we only need *@Pre\* callback* methods: 231 | 232 | ```java 233 | public class ChangeStateJpaListener { 234 | 235 | @PrePersist 236 | void onPrePersist(Object o) { 237 | enlist(o, EntityCommand.Action.INSERT); 238 | } 239 | 240 | @PreUpdate 241 | void onPreUpdate(Object o) { 242 | enlist(o, EntityCommand.Action.UPDATE); 243 | } 244 | 245 | @PreRemove 246 | void onPreRemove(Object o) { 247 | enlist(o, EntityCommand.Action.DELETE); 248 | } 249 | 250 | private void enlist(Object entity, EntityCommand.Action action){ 251 | EntityCommand command = new EntityCommand(); 252 | command.setEntity(entity); 253 | command.setAction(action); 254 | command.setTimestamp(System.currentTimeMillis()); 255 | // send command to some store/queue 256 | } 257 | } 258 | ``` 259 | 260 | 261 | 262 | #### 3. Commands persistence and distribution 263 | 264 | At this point we know how persistence operations executed by a service are translated into Commands, but once instantiated we need to save and distribute them to all service instances. This is accomplished by using Kafka persistent topics. Let's have a deeper look at the proposed mechanism: 265 | 266 | When a Composite Service asks the Coordinator (`TccRestCoordinator`) to open a new Composite Transaction, the first thing the latter does is to generate an UUID to uniquely identify that transaction. Then it creates as many topics as different Domain Services must be coordinated, assigning them a name that results from concatenating the UUID and an internal sequence number (building the so-called *partial transaction id*). Once all resources have been allocated, it returns to the Composite Service a `CompositeTransaction` object that includes the transaction global UUID and all partial ids. From this moment on, any call dispatched by the Composite Service to a Domain Service will always include the corresponding partial transaction id (as an extra `@PathParam`) 267 | 268 | Furthermore, the JPA entity listener responsible for generating Commands (see point #2) requires the name of the topic to use for publishing them (after a proper serialization process has been applied to the Command). How can that standard JPA class obtain a value available inside an `Spring` bean? `ThreadLocal` variables come to the rescue: just before the first call to a `DAO`, the Domain Service adds its partial transaction id to a `ThreadLocal` variable. Because of JPA listeners run in the same thread as the `EntityManager` operation, they have access to any `ThreadLocal` variable created by the service and can retrieve the partial transaction id from it. Finally, a `org.springframework.kafka.core.KafkaTemplate` instance is used to send the `JSON` representation of the Command to the appropriate topic. 269 | 270 | 271 | 272 | #### 4. From Command to persistence operation: inherited method from `CompositeTransactionParticipantDao` 273 | 274 | Because an `EntityCommand` object contains the entity to create/update/delete and the action to apply to it, it's very straightforward to find out which persistence operation must be executed by a given `EntityManager`; this is as simple as adding an special method to the generic `CompositeTransactionParticipantDao` where the`EntityManager` is injected: 275 | 276 | ```java 277 | public void apply(List> transactionOperations) { 278 | if (null == transactionOperations) { 279 | return; 280 | } 281 | 282 | for (EntityCommand command : transactionOperations) { 283 | switch (command.getAction().ordinal()) { 284 | case 0: 285 | save(command.getEntity()); 286 | break; 287 | case 1: 288 | saveOrUpdate(command.getEntity()); 289 | break; 290 | case 2: 291 | remove(command.getEntity()); 292 | break; 293 | } 294 | } 295 | } 296 | ``` 297 | 298 | 299 | 300 | #### 5. Composite Transaction lifecycle 301 | 302 | [01] A Composite Service asks the Coordinator (`TccRestCoordinator`) to open a new Composite Transaction. The call arguments include the maximum amount of time (in milliseconds) to complete the transaction and the URL of each participant (Domain Service) to be used when cancelling/confirming its operations (as specified by the TCC protocol). 303 | 304 | ```java 305 | CompositeTransaction transaction = tccRestCoordinator.open(transactionTimeout, featureAbcTccUrl, 306 | featureXyzTccUrl); 307 | ``` 308 | 309 | [02] The Coordinator generates the Composite Transaction UUID. Then, for each participant it computes the partial transaction id and uses a `CompositeTransactionManager` (instance provided by Spring container) to initialize the transaction persistence/distribution (with the Kafka-based implementation a persistent topic is created for each Domain Service) 310 | 311 | [03] The Composite Service starts calling each Domain Service and processes their responses 312 | 313 | [04] When a Domain Service receives a call, it extracts the transaction partial id from the URI 314 | 315 | ```java 316 | public Response txedOperation(@Context UriInfo uriInfo, @PathParam("txid") String txid, Feature data) 317 | ``` 318 | 319 | [05] Defines a `ThreadLocal` variable and sets its value to the transaction partial id 320 | 321 | ```java 322 | ThreadLocalContext.put(CURRENT_TRANSACTION_KEY, txId); 323 | ``` 324 | 325 | [06] Asks Spring container to return a **NEW** instance of a `DAO` with an *unsynchronized* `EntityManager` injected into it. Makes some calls to `DAO` methods 326 | 327 | [07] The `DAO` translates each method call to a set of persistence operations, delegating their execution to its `EntityManager` 328 | 329 | [08] For every persistence operation, the JPA container executes the global entity listener (in the same thread as the `EntityManager` operation) 330 | 331 | [09] The JPA listener checks if a partial transaction id has been informed by the service and in case of unavailability it does nothing. Otherwise (when a partial id can be positively found) it creates a new `EntityCommand` instance grouping the entity, the type of operation, the partial transaction id and a timestamp. After that, it uses the `CompositeTransactionManager` (instance provided by Spring container) to "enlist" the Command. 332 | 333 | ```java 334 | private void enlist(Object entity, EntityCommand.Action action, String txId){ 335 | 336 | EntityCommand command = new EntityCommand(); 337 | command.setEntity(entity); 338 | command.setAction(action); 339 | command.setTransactionId(txId); 340 | command.setTimestamp(System.currentTimeMillis()); 341 | 342 | CompositeTransactionManager txManager = 343 | SpringContext.getBean(CompositeTransactionManager.class); 344 | txManager.enlist(txId, command); 345 | } 346 | ``` 347 | 348 | [10] With the Kafka-based implementation of `CompositeTransactionManager`, the `EntityCommand` object is serialized to a `JSON` string prior to storing it in a topic. 349 | 350 | ------ 351 | 352 | 353 | 354 | So far, we have completed the *Try* part of the *Try*-*Cancel*/*Confirm* protocol. What about the *Cancel*/*Confirm* one? Let's start with *Confirm* 355 | 356 | [11] Once the Composite Service ends calling Domain Services, it invokes the `commit()` method on the Coordinator (`TccRestCoordinator`) 357 | 358 | [12] The coordinator sends a PUT request to the "confirm URI" of the TCC Service, adding the Composite Transaction data as the request content 359 | 360 | [13] The TCC Service iterates over the transaction participants list and, for each of them, sends a PUT request to their respective "TCC confirm URI" (computed during the Composite Transaction creation) 361 | 362 | [14] When a Domain Service receives the confirm call, it extracts the transaction partial id from the URI 363 | 364 | ```java 365 | public void confirm(@PathParam("txid") String txid) 366 | ``` 367 | 368 | [15] Uses the `CompositeTransactionManager` instance provided by Spring container to get all the Commands "enlisted" in that (partial) transaction 369 | 370 | [16] Asks the Spring container to return a **NEW** instance of a `DAO` with an *unsynchronized* `EntityManager` injected into it. 371 | 372 | [17] Invokes the `apply()` method on the `DAO` to translate Commands to persistence operations. Because of we're applying already persisted commands, we must disable the JPA global entity listener. This can be easily done by ensuring no `ThreadLocal` variable with the partial id has been defined. 373 | 374 | [18] Forces the `EntityManager` to join a `LOCAL/JTA` transaction and, thus, all persistence operations are applied to the underlying repository. 375 | 376 | [19] If a Domain Service fails to process the confirm call, a 404 response is returned. When the TCC Service receives it, the confirmation process is stopped and a 409 response is sent back to the Coordinator which in turn propagates that value to the Composite Service. 377 | 378 | [20] If all confirm calls succeed (all return 204) the TCC Service also responds with a 204 to the Coordinator which in turn propagates that value to the Composite Service. 379 | 380 | 381 | 382 | ------ 383 | 384 | And finally the *Cancel* branch: 385 | 386 | [11] If Composite Service detects some error condition, it can abort the Composite Transaction by invoking the `rollback()` method on the Coordinator (`TccRestCoordinator`) 387 | 388 | [12] In that case, the coordinator sends a PUT request to the "cancel URI" of the TCC Service, adding the Composite Transaction data as the request content 389 | 390 | [13] The TCC Service iterates over the transaction participants list and, for each of them, sends a DELETE request to their respective "TCC cancel URI" (computed during the Composite Transaction creation) 391 | 392 | [14] When a Domain Service receives the cancel call, it extracts the transaction partial id from the URI 393 | 394 | ```java 395 | public void cancel(@PathParam("txid") String txid) 396 | ``` 397 | 398 | [15] In the current implementation the Domain Service does nothing. Perhaps a valid action could be to "close" the partial transaction (with the Kafka-based implementation of the `CompositeTransactionManager` that could trigger a topic removal) 399 | 400 | [16] If a Domain Service fails to process the cancel call, a 404 response is returned. When the TCC Service receives it, a log trace is written and the cancellation process goes on. After the last call finishes, the TCC Service returns a 204 response to the Coordinator which in turn propagates that value to the Composite Service. 401 | 402 | [17] If all cancel calls succeed (all return 204) the TCC Service also responds with a 204 to the Coordinator which in turn propagates that value to the Composite Service. -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | net.jotorren.microservices 7 | rahub 8 | 1.0.0 9 | pom 10 | 11 | rahub 12 | Spring Boot raHub example 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 1.5.3.RELEASE 18 | 19 | 20 | 21 | 22 | rahub-cross-cutting-concerns 23 | rahub-composite-service 24 | rahub-source-code-service 25 | rahub-forum-service 26 | 27 | 28 | 29 | UTF-8 30 | UTF-8 31 | 1.8 32 | 33 | 1.0.0 34 | 1.0.0.Final 35 | 0.10 36 | 0.10.2.1 37 | 4.0.4 38 | 39 | 40 | 41 | 42 | 43 | net.jotorren.microservices 44 | rahub-cross-cutting-concerns 45 | ${net.jotorren.microservices.version} 46 | 47 | 48 | 49 | org.hibernate.javax.persistence 50 | hibernate-jpa-2.1-api 51 | ${org.hibernate.javax.persistence.version} 52 | 53 | 54 | 55 | com.101tec 56 | zkclient 57 | ${com.101tec.version} 58 | 59 | 60 | org.apache.kafka 61 | kafka_2.11 62 | ${org.apache.kafka.version} 63 | 64 | 65 | 66 | com.atomikos 67 | transactions 68 | ${com.atomikos.version} 69 | 70 | 71 | com.atomikos 72 | transactions-api 73 | ${com.atomikos.version} 74 | 75 | 76 | com.atomikos 77 | transactions-tcc-rest 78 | ${com.atomikos.version} 79 | 80 | 81 | com.atomikos 82 | transactions-tcc-rest-api 83 | ${com.atomikos.version} 84 | 85 | 86 | com.atomikos 87 | atomikos-util 88 | ${com.atomikos.version} 89 | 90 | 91 | 92 | 93 | org.apache.cxf 94 | cxf-rt-rs-service-description-swagger 95 | 3.1.11 96 | 97 | 98 | io.swagger 99 | swagger-jersey2-jaxrs 100 | 1.5.13 101 | 102 | 103 | org.webjars 104 | swagger-ui 105 | 3.0.8 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | org.springframework.boot 114 | spring-boot-maven-plugin 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /rahub-composite-service/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | tmp 4 | target 5 | 6 | .settings 7 | .classpath 8 | .project 9 | 10 | # Ignore Test reporters 11 | ####################### 12 | test 13 | coverage 14 | report 15 | doc 16 | -------------------------------------------------------------------------------- /rahub-composite-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | net.jotorren.microservices 8 | rahub 9 | 1.0.0 10 | 11 | 12 | rahub-composite-service 13 | rahub-composite-service 14 | Spring Boot raHub Composite Service 15 | 16 | 17 | net.jotorren.microservices.RahubCompositeServiceApplication 18 | 19 | 20 | 21 | 22 | org.apache.cxf 23 | cxf-spring-boot-starter-jaxrs 24 | 3.1.11 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-web 29 | 30 | 31 | 32 | net.jotorren.microservices 33 | rahub-cross-cutting-concerns 34 | 35 | 36 | 37 | com.fasterxml.jackson.jaxrs 38 | jackson-jaxrs-json-provider 39 | 40 | 41 | com.atomikos 42 | transactions 43 | 44 | 45 | com.atomikos 46 | transactions-api 47 | 48 | 49 | com.atomikos 50 | transactions-tcc-rest 51 | 52 | 53 | com.atomikos 54 | transactions-tcc-rest-api 55 | 56 | 57 | com.atomikos 58 | atomikos-util 59 | 60 | 61 | 62 | com.101tec 63 | zkclient 64 | 65 | 66 | org.apache.kafka 67 | kafka_2.11 68 | 69 | 70 | 71 | org.apache.cxf 72 | cxf-rt-rs-service-description-swagger 73 | 74 | 75 | org.webjars 76 | swagger-ui 77 | 78 | 79 | 80 | org.springframework.boot 81 | spring-boot-starter-test 82 | test 83 | 84 | 85 | org.springframework.restdocs 86 | spring-restdocs-mockmvc 87 | test 88 | 89 | 90 | -------------------------------------------------------------------------------- /rahub-composite-service/src/main/java/net/jotorren/microservices/RahubCompositeServiceApplication.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class RahubCompositeServiceApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(RahubCompositeServiceApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /rahub-composite-service/src/main/java/net/jotorren/microservices/composite/controller/CompositeController.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.composite.controller; 2 | 3 | import java.net.URI; 4 | import java.util.List; 5 | import java.util.Map.Entry; 6 | 7 | import javax.ws.rs.POST; 8 | import javax.ws.rs.Path; 9 | import javax.ws.rs.Produces; 10 | import javax.ws.rs.core.Context; 11 | import javax.ws.rs.core.Response; 12 | import javax.ws.rs.core.UriInfo; 13 | 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.web.bind.annotation.RestController; 16 | 17 | import io.swagger.annotations.Api; 18 | import io.swagger.annotations.ApiOperation; 19 | import io.swagger.annotations.ApiParam; 20 | import io.swagger.annotations.ApiResponse; 21 | import io.swagger.annotations.ApiResponses; 22 | import io.swagger.annotations.ResponseHeader; 23 | import net.jotorren.microservices.composite.domain.CompositeData; 24 | import net.jotorren.microservices.composite.service.CompositeService; 25 | import net.jotorren.microservices.rs.ExceptionRestHandler.ErrorDetails; 26 | import net.jotorren.microservices.tx.CompositeTransactionException; 27 | 28 | @RestController 29 | @Path("/") 30 | @Api(value = "API Services") 31 | public class CompositeController { 32 | 33 | @Autowired 34 | private CompositeService service; 35 | 36 | @POST 37 | @Path("files") 38 | @Produces ( "application/json" ) 39 | @ApiOperation( 40 | code = 201, 41 | value = "Save new content while creating its discussion board", 42 | notes = "Both operations (new content and forum) must succeed within a given time interval, otherwise both will be canceled or rolled back. " 43 | + "The newly created resource(s) can be referenced by the URI(s) returned in the entity of the response, with the URI for the " 44 | + "distributed transaction given by the Location header field", 45 | response = String.class, 46 | responseContainer = "List", 47 | responseHeaders = { 48 | @ResponseHeader(name = "Location", description = "The distributed transaction URI", response = String.class) 49 | } 50 | ) 51 | @ApiResponses(value = { 52 | @ApiResponse(code=500, message="Error processing request", response = ErrorDetails.class) 53 | }) 54 | public Response save(@Context UriInfo uriInfo, 55 | @ApiParam(value = "Data to pass to server", required = true) CompositeData data 56 | ) throws CompositeTransactionException { 57 | 58 | Entry> txEntities = service.saveAllEntities(data); 59 | 60 | URI location = uriInfo.getAbsolutePathBuilder().path("{id}") 61 | .resolveTemplate("id", txEntities.getKey()).build(); 62 | 63 | return Response.created(location).entity(txEntities.getValue()).build(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /rahub-composite-service/src/main/java/net/jotorren/microservices/composite/domain/CompositeData.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.composite.domain; 2 | 3 | import java.io.Serializable; 4 | 5 | public class CompositeData implements Serializable { 6 | private static final long serialVersionUID = 3182123368747451614L; 7 | 8 | private String fileName; 9 | private String fileLocation; 10 | private String fileContent; 11 | private String fileDescription; 12 | private String fileOwner; 13 | 14 | private String topicName; 15 | private String topicCategory; 16 | 17 | public String getFileName() { 18 | return fileName; 19 | } 20 | 21 | public void setFileName(String fileName) { 22 | this.fileName = fileName; 23 | } 24 | 25 | public String getFileLocation() { 26 | return fileLocation; 27 | } 28 | 29 | public void setFileLocation(String fileLocation) { 30 | this.fileLocation = fileLocation; 31 | } 32 | 33 | public String getFileContent() { 34 | return fileContent; 35 | } 36 | 37 | public void setFileContent(String fileContent) { 38 | this.fileContent = fileContent; 39 | } 40 | 41 | public String getFileDescription() { 42 | return fileDescription; 43 | } 44 | 45 | public void setFileDescription(String fileDescription) { 46 | this.fileDescription = fileDescription; 47 | } 48 | 49 | public String getFileOwner() { 50 | return fileOwner; 51 | } 52 | 53 | public void setFileOwner(String fileOwner) { 54 | this.fileOwner = fileOwner; 55 | } 56 | 57 | public String getTopicName() { 58 | return topicName; 59 | } 60 | 61 | public void setTopicName(String topicName) { 62 | this.topicName = topicName; 63 | } 64 | 65 | public String getTopicCategory() { 66 | return topicCategory; 67 | } 68 | 69 | public void setTopicCategory(String topicCategory) { 70 | this.topicCategory = topicCategory; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /rahub-composite-service/src/main/java/net/jotorren/microservices/composite/domain/CompositeForum.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.composite.domain; 2 | 3 | import java.io.Serializable; 4 | 5 | public class CompositeForum implements Serializable { 6 | private static final long serialVersionUID = 6059100903956824262L; 7 | 8 | private String forumId; 9 | private String topicName; 10 | private String topicCategory; 11 | private String subjectId; 12 | 13 | public String getForumId() { 14 | return forumId; 15 | } 16 | 17 | public void setForumId(String forumId) { 18 | this.forumId = forumId; 19 | } 20 | 21 | public String getTopicName() { 22 | return topicName; 23 | } 24 | 25 | public void setTopicName(String topicName) { 26 | this.topicName = topicName; 27 | } 28 | 29 | public String getTopicCategory() { 30 | return topicCategory; 31 | } 32 | 33 | public void setTopicCategory(String topicCategory) { 34 | this.topicCategory = topicCategory; 35 | } 36 | 37 | public String getSubjectId() { 38 | return subjectId; 39 | } 40 | 41 | public void setSubjectId(String subjectId) { 42 | this.subjectId = subjectId; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /rahub-composite-service/src/main/java/net/jotorren/microservices/composite/service/CompositeService.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.composite.service; 2 | 3 | import java.net.URI; 4 | import java.util.AbstractMap; 5 | import java.util.Arrays; 6 | import java.util.List; 7 | import java.util.Map.Entry; 8 | 9 | import javax.ws.rs.core.Response; 10 | 11 | import net.jotorren.microservices.composite.domain.CompositeData; 12 | import net.jotorren.microservices.composite.domain.CompositeForum; 13 | import net.jotorren.microservices.tcc.TccRestCoordinator; 14 | import net.jotorren.microservices.tx.CompositeTransaction; 15 | import net.jotorren.microservices.tx.CompositeTransactionException; 16 | 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | import org.springframework.beans.factory.annotation.Autowired; 20 | import org.springframework.beans.factory.annotation.Value; 21 | import org.springframework.stereotype.Service; 22 | import org.springframework.web.client.RestTemplate; 23 | 24 | @Service 25 | public class CompositeService { 26 | 27 | private static final Logger LOG = LoggerFactory.getLogger(CompositeService.class); 28 | 29 | @Value("${tcc.transaction.timeout}") 30 | private long transactionTimeout; 31 | 32 | @Autowired 33 | private TccRestCoordinator tccRestCoordinator; 34 | 35 | @Value("${content.service.url}") 36 | private String contentServiceUrl; 37 | 38 | @Value("${content.service.tcc.url}") 39 | private String contentServiceTccUrl; 40 | 41 | @Value("${forum.service.url}") 42 | private String forumServiceUrl; 43 | 44 | @Value("${forum.service.tcc.url}") 45 | private String forumServiceTccUrl; 46 | 47 | @Autowired 48 | private RestTemplate restTemplate; 49 | 50 | private String getIdFromURI(URI uri){ 51 | String[] segments = uri.getPath().split("/"); 52 | return segments[segments.length-1]; 53 | } 54 | 55 | private String removeTransactionIdFromURI(URI uri){ 56 | String[] segments = uri.getPath().split("/"); 57 | String txId = segments[segments.length-2]; 58 | 59 | return uri.toString().replace("/"+txId, ""); 60 | } 61 | 62 | public Entry> saveAllEntities(CompositeData data) throws CompositeTransactionException { 63 | Entry> newObjectsUris = null; 64 | 65 | CompositeTransaction transaction = tccRestCoordinator.open(System.currentTimeMillis() + transactionTimeout, 66 | contentServiceTccUrl, forumServiceTccUrl); 67 | 68 | Response commitResponse; 69 | try{ 70 | // first service call 71 | String contentServiceUrlWithTransaction = contentServiceUrl + 72 | (contentServiceUrl.endsWith("/")? "" : "/") + transaction.getPartialTransactionId(0); 73 | LOG.info("Step 1: calling [{}]", contentServiceUrlWithTransaction); 74 | URI contentUriWithTransaction = restTemplate.postForLocation(contentServiceUrlWithTransaction, data); 75 | LOG.info("Step 1: content created [{}]", contentUriWithTransaction); 76 | 77 | // second service call preparation (using data received from the first one) 78 | CompositeForum forumData = new CompositeForum(); 79 | forumData.setTopicName(data.getTopicName()); 80 | forumData.setTopicCategory(data.getTopicCategory()); 81 | forumData.setSubjectId(getIdFromURI(contentUriWithTransaction)); 82 | 83 | // second service call 84 | String forumServiceUrlWithTransaction = forumServiceUrl + 85 | (forumServiceUrl.endsWith("/")? "" : "/") + transaction.getPartialTransactionId(1); 86 | LOG.info("Step 2: calling [{}]", forumServiceUrlWithTransaction); 87 | URI forumUriWithTransaction = restTemplate.postForLocation(forumServiceUrlWithTransaction, forumData); 88 | LOG.info("Step 2: forum discussion created [{}]", forumUriWithTransaction); 89 | 90 | // commit 91 | commitResponse = tccRestCoordinator.commit(transaction); 92 | 93 | // everything seems to be fine 94 | newObjectsUris = new AbstractMap.SimpleEntry>( 95 | transaction.getId(), Arrays.asList( 96 | removeTransactionIdFromURI(contentUriWithTransaction), 97 | removeTransactionIdFromURI(forumUriWithTransaction) 98 | )); 99 | 100 | } catch (Exception e){ 101 | // oops! something went wrong 102 | tccRestCoordinator.rollback(transaction); 103 | throw new CompositeTransactionException(e); 104 | } 105 | 106 | // check for potential heuristic exceptions (some commit calls failed) 107 | if (Response.Status.NO_CONTENT.getStatusCode() != commitResponse.getStatus()) { 108 | throw new CompositeTransactionException(commitResponse.readEntity(String.class)); 109 | } 110 | 111 | return newObjectsUris; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /rahub-composite-service/src/main/java/net/jotorren/microservices/configuration/CompositeTransactionConfiguration.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.configuration; 2 | 3 | import javax.ws.rs.client.Client; 4 | import javax.ws.rs.client.ClientBuilder; 5 | import javax.ws.rs.client.WebTarget; 6 | 7 | import kafka.utils.ZKStringSerializer$; 8 | import kafka.utils.ZkUtils; 9 | import net.jotorren.microservices.tx.CompositeTransactionManager; 10 | import net.jotorren.microservices.tx.impl.CompositeTransactionManagerKafkaImpl; 11 | 12 | import org.I0Itec.zkclient.ZkClient; 13 | import org.I0Itec.zkclient.ZkConnection; 14 | import org.springframework.beans.factory.annotation.Value; 15 | import org.springframework.context.annotation.Bean; 16 | import org.springframework.context.annotation.Configuration; 17 | 18 | import com.atomikos.icatch.tcc.rest.CoordinatorImp; 19 | import com.atomikos.icatch.tcc.rest.TransactionProvider; 20 | import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; 21 | 22 | @Configuration 23 | public class CompositeTransactionConfiguration { 24 | 25 | @Value("${tcc.service.base.url}") 26 | private String tccCoordinatorBaseUrl; 27 | 28 | 29 | @Value("${zookeeper.servers}") 30 | private String zooServers; 31 | 32 | @Value("${zookeeper.session.timeout}") 33 | private int zooSessionTimeout; 34 | 35 | @Value("${zookeeper.connection.timeout}") 36 | private int zooConnectionTimeout; 37 | 38 | 39 | @Bean 40 | public CoordinatorImp tccCoordinatorService() { 41 | return new CoordinatorImp(); 42 | } 43 | 44 | @Bean 45 | public WebTarget tccCoordinatorClient() { 46 | Client client = ClientBuilder.newClient(); 47 | client.register(new JacksonJaxbJsonProvider()); 48 | client.register(new TransactionProvider()); 49 | WebTarget target = client.target(tccCoordinatorBaseUrl); 50 | return target.path("/coordinator"); 51 | } 52 | 53 | @Bean 54 | public CompositeTransactionManager compositeTransactionManager() { 55 | return new CompositeTransactionManagerKafkaImpl(); 56 | } 57 | 58 | @Bean 59 | public ZkClient zkClient() { 60 | return new ZkClient(zooServers, zooSessionTimeout, zooConnectionTimeout, ZKStringSerializer$.MODULE$); 61 | } 62 | 63 | @Bean 64 | public ZkUtils zkUtils(ZkClient zkClient) { 65 | // Security for Kafka was added in Kafka 0.9.0.0 66 | boolean isSecureKafkaCluster = false; 67 | return new ZkUtils(zkClient, new ZkConnection(zooServers), isSecureKafkaCluster); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /rahub-composite-service/src/main/java/net/jotorren/microservices/configuration/WebServicesConfiguration.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.configuration; 2 | 3 | import java.util.Arrays; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | 7 | import net.jotorren.microservices.composite.controller.CompositeController; 8 | import net.jotorren.microservices.rs.ExceptionRestHandler; 9 | 10 | import org.apache.cxf.Bus; 11 | import org.apache.cxf.endpoint.Server; 12 | import org.apache.cxf.feature.LoggingFeature; 13 | import org.apache.cxf.jaxrs.JAXRSServerFactoryBean; 14 | import org.apache.cxf.jaxrs.swagger.Swagger2Feature; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.beans.factory.annotation.Value; 17 | import org.springframework.boot.web.client.RestTemplateBuilder; 18 | import org.springframework.context.annotation.Bean; 19 | import org.springframework.context.annotation.Configuration; 20 | import org.springframework.web.client.RestTemplate; 21 | 22 | import com.atomikos.icatch.tcc.rest.CoordinatorImp; 23 | import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider; 24 | 25 | @Configuration 26 | public class WebServicesConfiguration { 27 | 28 | @Value("${swagger.title}") 29 | private String title; 30 | 31 | @Value("${swagger.description}") 32 | private String description; 33 | 34 | @Value("${swagger.version}") 35 | private String version; 36 | 37 | @Value("${swagger.contact}") 38 | private String contact; 39 | 40 | @Value("${swagger.schemes}") 41 | private String schemes; 42 | 43 | @Value("${swagger.basePath}") 44 | private String basePath; 45 | 46 | @Value("${swagger.resourcePackage}") 47 | private String resourcePackage; 48 | 49 | @Value("${swagger.prettyPrint}") 50 | private boolean prettyPrint; 51 | 52 | @Value("${swagger.scan}") 53 | private boolean scan; 54 | 55 | @Autowired 56 | private Bus bus; 57 | 58 | @Autowired 59 | private CoordinatorImp tccCoordinatorService; 60 | 61 | @Autowired 62 | private CompositeController compositeController; 63 | 64 | @Bean 65 | public Server rsServer() { 66 | JAXRSServerFactoryBean endpoint = new JAXRSServerFactoryBean(); 67 | endpoint.setBus(bus); 68 | endpoint.setAddress("/"); 69 | endpoint.setProviders(Arrays.asList(new JacksonJsonProvider(), new ExceptionRestHandler())); 70 | 71 | Map mappings = new HashMap(); 72 | mappings.put("json", "application/json"); 73 | endpoint.setExtensionMappings(mappings); 74 | 75 | Swagger2Feature swagger2Feature = new Swagger2Feature(); 76 | swagger2Feature.setTitle(title); 77 | swagger2Feature.setDescription(description); 78 | swagger2Feature.setVersion(version); 79 | swagger2Feature.setContact(contact); 80 | swagger2Feature.setSchemes(schemes.split(",")); 81 | swagger2Feature.setBasePath(basePath); 82 | swagger2Feature.setResourcePackage(resourcePackage); 83 | swagger2Feature.setPrettyPrint(prettyPrint); 84 | swagger2Feature.setScan(scan); 85 | 86 | endpoint.setFeatures(Arrays.asList(new LoggingFeature(), swagger2Feature)); 87 | endpoint.setServiceBeans(Arrays.asList(tccCoordinatorService, compositeController)); 88 | 89 | return endpoint.create(); 90 | } 91 | 92 | @Bean 93 | public RestTemplate restTemplate(RestTemplateBuilder builder) { 94 | return builder.build(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /rahub-composite-service/src/main/java/net/jotorren/microservices/tcc/TccRestCoordinator.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.tcc; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.UUID; 6 | 7 | import javax.ws.rs.client.Entity; 8 | import javax.ws.rs.client.WebTarget; 9 | import javax.ws.rs.core.Response; 10 | 11 | import net.jotorren.microservices.tx.CompositeTransaction; 12 | import net.jotorren.microservices.tx.CompositeTransactionManager; 13 | 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.beans.factory.annotation.Qualifier; 16 | import org.springframework.stereotype.Service; 17 | 18 | import com.atomikos.tcc.rest.ParticipantLink; 19 | import com.atomikos.tcc.rest.Transaction; 20 | 21 | @Service 22 | public class TccRestCoordinator { 23 | 24 | @Autowired 25 | @Qualifier("tccCoordinatorClient") 26 | private WebTarget tccCoordinatorClient; 27 | 28 | @Autowired 29 | private CompositeTransactionManager txManager; 30 | 31 | public CompositeTransaction open(long timestamp, String... participantUris){ 32 | String txId = UUID.randomUUID().toString(); 33 | 34 | int i = 0; 35 | String participantUri; 36 | String partialTxId; 37 | List participants = new ArrayList(); 38 | for (String uri : participantUris) { 39 | partialTxId = txId + "-" + String.format("%03d", i++); 40 | txManager.open(partialTxId); 41 | 42 | participantUri = uri + (uri.endsWith("/")? "" : "/") + partialTxId; 43 | ParticipantLink participantLink = new ParticipantLink(participantUri, timestamp); 44 | participants.add(participantLink); 45 | } 46 | 47 | CompositeTransaction transaction = new CompositeTransaction(); 48 | transaction.setId(txId); 49 | transaction.getParticipantLinks().addAll(participants); 50 | return transaction; 51 | } 52 | 53 | public Response commit(Transaction transaction){ 54 | return tccCoordinatorClient.path("/confirm").request().put(Entity.entity(transaction, "application/tcc+json")); 55 | } 56 | 57 | public Response rollback(Transaction transaction){ 58 | return tccCoordinatorClient.path("/cancel").request().put(Entity.entity(transaction, "application/tcc+json")); 59 | } 60 | 61 | public void close(CompositeTransaction transaction){ 62 | txManager.close(transaction.getId()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /rahub-composite-service/src/main/java/net/jotorren/microservices/tcc/TccRestInitializer.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.tcc; 2 | 3 | import org.springframework.context.ApplicationListener; 4 | import org.springframework.context.event.ContextRefreshedEvent; 5 | import org.springframework.stereotype.Component; 6 | 7 | import com.atomikos.icatch.config.Configuration; 8 | 9 | @Component 10 | public class TccRestInitializer implements ApplicationListener{ 11 | 12 | @Override 13 | public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) { 14 | Configuration.init(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /rahub-composite-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port = 8090 2 | cxf.path = /api 3 | 4 | # http://localhost:8090/api/swagger.json 5 | # http://localhost:8090/api/api-docs?url=/api/swagger.json 6 | swagger.title = rahub-composite-service 7 | swagger.description = Spring Boot raHub Composite Service 8 | swagger.version = 1.0.0 9 | swagger.contact = jotorren 10 | #swagger.schemes = http,https 11 | swagger.schemes = http 12 | swagger.basePath = /api 13 | swagger.resourcePackage = net.jotorren.microservices.composite.controller 14 | swagger.prettyPrint = true 15 | swagger.scan = true 16 | 17 | zookeeper.servers = localhost:2181 18 | zookeeper.session.timeout = 10000 19 | zookeeper.connection.timeout = 8000 20 | zookeeper.partitions = 1 21 | zookeeper.replication = 1 22 | 23 | content.service.url = http://localhost:8091/content 24 | content.service.tcc.url = http://localhost:8091/content/tcc 25 | 26 | forum.service.url = http://localhost:8092/forum 27 | forum.service.tcc.url = http://localhost:8092/forum/tcc 28 | 29 | tcc.service.base.url = http://localhost:8090/api 30 | tcc.transaction.timeout = 30000 -------------------------------------------------------------------------------- /rahub-composite-service/src/main/resources/static/swagger-tcc.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger" : "2.0", 3 | "info" : { 4 | "description" : "Spring Boot raHub TCC Service", 5 | "version" : "1.0.0", 6 | "title" : "rahub-composite-service", 7 | "contact" : { 8 | "name" : "jotorren" 9 | }, 10 | "license" : { 11 | "name" : "Apache 2.0 License", 12 | "url" : "http://www.apache.org/licenses/LICENSE-2.0.html" 13 | } 14 | }, 15 | "basePath" : "/api/coordinator", 16 | "tags" : [ { 17 | "name" : "TCC Services" 18 | } ], 19 | "schemes" : [ "http" ], 20 | "paths" : { 21 | "/cancel" : { 22 | "put" : { 23 | "tags" : [ "TCC Services" ], 24 | "summary" : "Cancel a composite transaction", 25 | "description" : "Iterates over the list of transaction participants calling the TCC cancel method for each of them", 26 | "operationId" : "cancel", 27 | "consumes" : [ "application/tcc+json" ], 28 | "produces" : [ "*/*" ], 29 | "parameters" : [ { 30 | "in" : "body", 31 | "name" : "body", 32 | "description" : "Transaction data", 33 | "required" : true, 34 | "schema" : { 35 | "$ref" : "#/definitions/Transaction" 36 | } 37 | } ], 38 | "responses" : { 39 | "204" : { 40 | "description" : "successful operation" 41 | }, 42 | "400" : { 43 | "description" : "invalid request: transaction must not be null, each participantLink must have an 'expires', invalid date format for participantLink 'expires', each participantLink must have a value for 'uri'" 44 | }, 45 | "500" : { 46 | "description" : "unexpected error" 47 | } 48 | } 49 | } 50 | }, 51 | "/confirm" : { 52 | "put" : { 53 | "tags" : [ "TCC Services" ], 54 | "summary" : "Confirm a composite transaction", 55 | "description" : "Iterates over the list the transaction participants calling the TCC confirm method for each of them", 56 | "operationId" : "confirm", 57 | "consumes" : [ "application/tcc+json" ], 58 | "produces" : [ "*/*" ], 59 | "parameters" : [ { 60 | "in" : "body", 61 | "name" : "body", 62 | "description" : "Transaction data", 63 | "required" : true, 64 | "schema" : { 65 | "$ref" : "#/definitions/Transaction" 66 | } 67 | } ], 68 | "responses" : { 69 | "204" : { 70 | "description" : "successful operation" 71 | }, 72 | "400" : { 73 | "description" : "invalid request: transaction must not be null, each participantLink must have an 'expires', invalid date format for participantLink 'expires', each participantLink must have a value for 'uri'" 74 | }, 75 | "404" : { 76 | "description" : "transaction has timed out and was cancelled" 77 | }, 78 | "409" : { 79 | "description" : "partial confirmation - check each participant for details" 80 | }, 81 | "500" : { 82 | "description" : "unexpected error" 83 | } 84 | } 85 | } 86 | } 87 | }, 88 | "definitions" : { 89 | "Transaction" : { 90 | "type" : "object", 91 | "properties" : { 92 | "participantLinks" : { 93 | "type" : "array", 94 | "items": { 95 | "$ref" : "#/definitions/ParticipantLink" 96 | } 97 | } 98 | } 99 | }, 100 | "ParticipantLink" : { 101 | "type" : "object", 102 | "properties" : { 103 | "uri" : { 104 | "type" : "string" 105 | }, 106 | "expires" : { 107 | "type" : "string" 108 | } 109 | } 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /rahub-cross-cutting-concerns/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | tmp 4 | target 5 | 6 | .settings 7 | .classpath 8 | .project 9 | 10 | # Ignore Test reporters 11 | ####################### 12 | test 13 | coverage 14 | report 15 | doc 16 | -------------------------------------------------------------------------------- /rahub-cross-cutting-concerns/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | net.jotorren.microservices 8 | rahub 9 | 1.0.0 10 | 11 | 12 | rahub-cross-cutting-concerns 13 | rahub-cross-cutting-concerns 14 | raHub cross cutting classes 15 | jar 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-web 21 | 22 | 23 | org.springframework.kafka 24 | spring-kafka 25 | 26 | 27 | 28 | org.hibernate.javax.persistence 29 | hibernate-jpa-2.1-api 30 | 31 | 32 | org.springframework 33 | spring-tx 34 | 35 | 36 | 37 | com.atomikos 38 | transactions-tcc-rest-api 39 | 40 | 41 | 42 | com.101tec 43 | zkclient 44 | 45 | 46 | org.apache.kafka 47 | kafka_2.11 48 | 49 | 50 | 51 | io.swagger 52 | swagger-jersey2-jaxrs 53 | 54 | 55 | 56 | 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-maven-plugin 61 | 62 | true 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /rahub-cross-cutting-concerns/src/main/java/net/jotorren/microservices/context/SpringContext.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.context; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | 7 | public final class SpringContext { 8 | 9 | private static final Logger LOG = LoggerFactory.getLogger(SpringContext.class); 10 | 11 | private SpringContext() { 12 | } 13 | 14 | public static T getBean(Class clazz) { 15 | 16 | if (null == SpringContextProvider.getApplicationContext()) { 17 | LOG.warn("No application context available"); 18 | return null; 19 | } else { 20 | return SpringContextProvider.getApplicationContext().getBean(clazz); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /rahub-cross-cutting-concerns/src/main/java/net/jotorren/microservices/context/SpringContextProvider.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.context; 2 | 3 | import org.springframework.context.ApplicationContext; 4 | import org.springframework.context.ApplicationContextAware; 5 | 6 | // To be defined as spring component inside target project 7 | public class SpringContextProvider implements ApplicationContextAware { 8 | private static ApplicationContext ctx = null; 9 | 10 | public static ApplicationContext getApplicationContext() { 11 | return ctx; 12 | } 13 | 14 | public void setApplicationContext(ApplicationContext ctx) { 15 | SpringContextProvider.ctx = ctx; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /rahub-cross-cutting-concerns/src/main/java/net/jotorren/microservices/context/ThreadLocalContext.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.context; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | public final class ThreadLocalContext { 9 | 10 | private static final Logger LOG = LoggerFactory.getLogger(ThreadLocalContext.class); 11 | 12 | /** Initial size of the map */ 13 | private static final int HT_SIZE = 5; 14 | 15 | /** Thread local map variable to store any shared object */ 16 | private static final ThreadLocal> THREAD_LOCAL_HOLDER = new ThreadLocal>() { 17 | 18 | protected java.util.Map initialValue() { 19 | return new HashMap(HT_SIZE); 20 | }; 21 | }; 22 | 23 | private ThreadLocalContext() { 24 | } 25 | 26 | public static void put(String key, Object payload) { 27 | if (null == key) { 28 | LOG.error("Unable to put a null key"); 29 | return; 30 | } 31 | 32 | if (null == payload) { 33 | LOG.error("Unable to put a null value"); 34 | return; 35 | } 36 | 37 | THREAD_LOCAL_HOLDER.get().put(key, payload); 38 | } 39 | 40 | public static Object get(String key) { 41 | if (null == key) { 42 | LOG.error("Unable to get a null key"); 43 | return null; 44 | } 45 | 46 | return THREAD_LOCAL_HOLDER.get().get(key); 47 | } 48 | 49 | @SuppressWarnings("unchecked") 50 | public static T get(String key, Class requiredType) { 51 | return (T) get(key); 52 | } 53 | 54 | public static Object remove(String key) { 55 | if (null == key) { 56 | LOG.error("Unable to remove a null key"); 57 | return null; 58 | } 59 | 60 | return THREAD_LOCAL_HOLDER.get().remove(key); 61 | } 62 | 63 | public static void cleanup() { 64 | THREAD_LOCAL_HOLDER.get().clear(); 65 | THREAD_LOCAL_HOLDER.remove(); 66 | } 67 | 68 | public static Map exportContext() { 69 | return THREAD_LOCAL_HOLDER.get(); 70 | } 71 | 72 | public static void importContext(Map context) { 73 | THREAD_LOCAL_HOLDER.get().putAll(context); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /rahub-cross-cutting-concerns/src/main/java/net/jotorren/microservices/rs/ExceptionRestHandler.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.rs; 2 | 3 | import java.io.PrintWriter; 4 | import java.io.StringWriter; 5 | 6 | import javax.ws.rs.core.Response; 7 | import javax.ws.rs.ext.ExceptionMapper; 8 | 9 | public class ExceptionRestHandler implements ExceptionMapper{ 10 | 11 | public class ErrorDetails { 12 | private int code; 13 | private String message; 14 | private String stack; 15 | 16 | public ErrorDetails(int code, String message, String stack){ 17 | this.code = code; 18 | this.message = message; 19 | this.stack = stack; 20 | } 21 | 22 | public int getCode() { 23 | return code; 24 | } 25 | 26 | public String getMessage() { 27 | return message; 28 | } 29 | 30 | public String getStack() { 31 | return stack; 32 | } 33 | } 34 | 35 | @Override 36 | public Response toResponse(Exception exception) { 37 | return toResponse(exception, Response.Status.INTERNAL_SERVER_ERROR); 38 | } 39 | 40 | public Response toResponse(Exception exception, Response.Status status) { 41 | return Response.status(status).entity( 42 | new ErrorDetails(status.getStatusCode(), exception.getMessage(), toString(exception))).build(); 43 | } 44 | 45 | public String toString(Exception exception) { 46 | StringWriter sw = new StringWriter(); 47 | PrintWriter pw = new PrintWriter(sw); 48 | exception.printStackTrace(pw); 49 | String stack = sw.toString(); 50 | pw.close(); 51 | 52 | return stack; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /rahub-cross-cutting-concerns/src/main/java/net/jotorren/microservices/tx/AbstractEntityCommandJsonSerializer.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.tx; 2 | 3 | import org.springframework.core.serializer.support.SerializationFailedException; 4 | 5 | import com.fasterxml.jackson.core.JsonProcessingException; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | 8 | public abstract class AbstractEntityCommandJsonSerializer implements Serializer> { 9 | 10 | private ObjectMapper jacksonMapper = new ObjectMapper(); 11 | 12 | public ObjectMapper getJacksonMapper(){ 13 | return jacksonMapper; 14 | } 15 | 16 | @Override 17 | public byte[] write(EntityCommand object) throws SerializationFailedException { 18 | return writeToString(object).getBytes(); 19 | } 20 | 21 | @Override 22 | public String writeToString(EntityCommand object) throws SerializationFailedException { 23 | try { 24 | return jacksonMapper.writeValueAsString(object); 25 | } catch (JsonProcessingException e) { 26 | throw new SerializationFailedException("Error performing EntityCommand serialization", e); 27 | } 28 | } 29 | 30 | @Override 31 | public EntityCommand read(byte[] bytes) throws SerializationFailedException { 32 | return readFromString(new String(bytes)); 33 | } 34 | 35 | @Override 36 | public abstract EntityCommand readFromString(String chars) throws SerializationFailedException; 37 | 38 | } 39 | -------------------------------------------------------------------------------- /rahub-cross-cutting-concerns/src/main/java/net/jotorren/microservices/tx/ChangeStateJpaListener.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.tx; 2 | 3 | import javax.persistence.PrePersist; 4 | import javax.persistence.PreRemove; 5 | import javax.persistence.PreUpdate; 6 | 7 | import net.jotorren.microservices.context.SpringContext; 8 | import net.jotorren.microservices.context.ThreadLocalContext; 9 | 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | public class ChangeStateJpaListener { 14 | 15 | private static final Logger LOG = LoggerFactory.getLogger(ChangeStateJpaListener.class); 16 | 17 | private void enlist(Object entity, EntityCommand.Action action, String txId){ 18 | 19 | EntityCommand command = new EntityCommand(); 20 | command.setEntity(entity); 21 | command.setAction(action); 22 | command.setTransactionId(txId); 23 | command.setTimestamp(System.currentTimeMillis()); 24 | 25 | CompositeTransactionManager txManager = SpringContext.getBean(CompositeTransactionManager.class); 26 | txManager.enlist(txId, command); 27 | } 28 | 29 | @PrePersist 30 | void onPrePersist(Object o) { 31 | String txId = (String)ThreadLocalContext.get(CompositeTransactionParticipantService.CURRENT_TRANSACTION_KEY); 32 | if (null == txId){ 33 | LOG.info("onPrePersist outside any transaction"); 34 | } else { 35 | LOG.info("onPrePersist inside transaction [{}]", txId); 36 | enlist(o, EntityCommand.Action.INSERT, txId); 37 | } 38 | } 39 | 40 | @PreUpdate 41 | void onPreUpdate(Object o) { 42 | String txId = (String)ThreadLocalContext.get(CompositeTransactionParticipantService.CURRENT_TRANSACTION_KEY); 43 | if (null == txId){ 44 | LOG.info("onPreUpdate outside any transaction"); 45 | } else { 46 | LOG.info("onPreUpdate inside transaction [{}]", txId); 47 | enlist(o, EntityCommand.Action.UPDATE, txId); 48 | } 49 | } 50 | 51 | @PreRemove 52 | void onPreRemove(Object o) { 53 | String txId = (String)ThreadLocalContext.get(CompositeTransactionParticipantService.CURRENT_TRANSACTION_KEY); 54 | if (null == txId){ 55 | LOG.info("onPreRemove outside any transaction"); 56 | } else { 57 | LOG.info("onPreRemove inside transaction [{}]", txId); 58 | enlist(o, EntityCommand.Action.DELETE, txId); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /rahub-cross-cutting-concerns/src/main/java/net/jotorren/microservices/tx/CompositeTransaction.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.tx; 2 | 3 | import com.atomikos.tcc.rest.Transaction; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | 6 | public class CompositeTransaction extends Transaction { 7 | 8 | @JsonIgnore 9 | private String id; 10 | 11 | public String getId() { 12 | return id; 13 | } 14 | 15 | public void setId(String id) { 16 | this.id = id; 17 | } 18 | 19 | public String getPartialTransactionId(int index) { 20 | if (index < 0 || null == participantLinks || index >= participantLinks.size()){ 21 | return null; 22 | } 23 | 24 | String participantUri = participantLinks.get(index).getUri(); 25 | return participantUri.substring(participantUri.lastIndexOf("/") + 1); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /rahub-cross-cutting-concerns/src/main/java/net/jotorren/microservices/tx/CompositeTransactionException.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.tx; 2 | 3 | public class CompositeTransactionException extends Exception { 4 | private static final long serialVersionUID = 3758500701912008969L; 5 | 6 | public CompositeTransactionException() { 7 | } 8 | 9 | public CompositeTransactionException(String message) { 10 | super(message); 11 | } 12 | 13 | public CompositeTransactionException(Throwable cause) { 14 | super(cause); 15 | } 16 | 17 | public CompositeTransactionException(String message, Throwable cause) { 18 | super(message, cause); 19 | } 20 | 21 | public CompositeTransactionException(String message, Throwable cause, 22 | boolean enableSuppression, boolean writableStackTrace) { 23 | super(message, cause, enableSuppression, writableStackTrace); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /rahub-cross-cutting-concerns/src/main/java/net/jotorren/microservices/tx/CompositeTransactionManager.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.tx; 2 | 3 | import java.util.List; 4 | 5 | public interface CompositeTransactionManager { 6 | 7 | void open(String txId); 8 | void enlist(String txId, EntityCommand ec); 9 | List> fetch(String txId); 10 | void close(String txId); 11 | } 12 | -------------------------------------------------------------------------------- /rahub-cross-cutting-concerns/src/main/java/net/jotorren/microservices/tx/CompositeTransactionParticipantController.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.tx; 2 | 3 | import io.swagger.annotations.ApiOperation; 4 | import io.swagger.annotations.ApiParam; 5 | import io.swagger.annotations.ApiResponse; 6 | import io.swagger.annotations.ApiResponses; 7 | 8 | import javax.ws.rs.Consumes; 9 | import javax.ws.rs.DELETE; 10 | import javax.ws.rs.PUT; 11 | import javax.ws.rs.Path; 12 | import javax.ws.rs.PathParam; 13 | import javax.ws.rs.WebApplicationException; 14 | import javax.ws.rs.core.MediaType; 15 | import javax.ws.rs.core.Response; 16 | 17 | import net.jotorren.microservices.rs.ExceptionRestHandler; 18 | 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | 22 | public abstract class CompositeTransactionParticipantController { 23 | 24 | private static final Logger LOG = LoggerFactory.getLogger(CompositeTransactionParticipantController.class); 25 | 26 | @DELETE 27 | @Path("/tcc/{txid}") 28 | @Consumes("application/tcc") 29 | @ApiOperation( 30 | code = 204, 31 | response = String.class, 32 | value = "Rollback a given composite transaction", 33 | notes = "See https://www.atomikos.com/Blog/TransactionManagementAPIForRESTTCC", 34 | consumes = "application/tcc" 35 | ) 36 | public void cancel( 37 | @ApiParam(value = "Id of the composite transaction to rollback", required = true) @PathParam("txid") String txid 38 | ){ 39 | LOG.info("Trying to rollback transaction [{}]", txid); 40 | 41 | getCompositeTransactionParticipantService().cancel(txid); 42 | 43 | LOG.info("Transaction [{}] rolled back", txid); 44 | } 45 | 46 | @PUT 47 | @Path("/tcc/{txid}") 48 | @Consumes("application/tcc") 49 | @ApiOperation( 50 | code = 204, 51 | response = String.class, 52 | value = "Commit a given composite transaction", 53 | notes = "See https://www.atomikos.com/Blog/TransactionManagementAPIForRESTTCC", 54 | consumes = "application/tcc" 55 | ) 56 | @ApiResponses(value = { 57 | @ApiResponse(code=404, message="Error committing transaction", response = String.class) 58 | }) 59 | public void confirm( 60 | @ApiParam(value = "Id of the composite transaction to commit", required = true) @PathParam("txid") String txid 61 | ){ 62 | LOG.info("Trying to commit transaction [{}]", txid); 63 | 64 | try { 65 | getCompositeTransactionParticipantService().confirm(txid); 66 | 67 | LOG.info("Transaction [{}] committed", txid); 68 | } catch(Exception e){ 69 | // See com.atomikos.icatch.tcc.rest.ParticipantAdapterImp.callConfirmOnJaxrsClient() 70 | Response response = Response.status(Response.Status.NOT_FOUND) 71 | .entity(new ExceptionRestHandler().toString(e)).type(MediaType.TEXT_PLAIN).build(); 72 | throw new WebApplicationException(response); 73 | } 74 | } 75 | 76 | public abstract CompositeTransactionParticipantService getCompositeTransactionParticipantService(); 77 | } 78 | -------------------------------------------------------------------------------- /rahub-cross-cutting-concerns/src/main/java/net/jotorren/microservices/tx/CompositeTransactionParticipantDao.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.tx; 2 | 3 | import java.util.List; 4 | 5 | import javax.persistence.EntityManager; 6 | import javax.persistence.PersistenceContext; 7 | import javax.persistence.PersistenceContextType; 8 | import javax.persistence.SynchronizationType; 9 | 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | public class CompositeTransactionParticipantDao { 13 | 14 | @PersistenceContext(type = PersistenceContextType.EXTENDED, synchronization = SynchronizationType.UNSYNCHRONIZED) 15 | private EntityManager em; 16 | 17 | public EntityManager getEntityManager() { 18 | return em; 19 | } 20 | 21 | public void save(Object entity) { 22 | em.persist(entity); 23 | } 24 | 25 | public T saveOrUpdate(T entity) { 26 | return em.merge(entity); 27 | } 28 | 29 | public void remove(Object entity) { 30 | em.remove(entity); 31 | } 32 | 33 | @Transactional(readOnly=false) 34 | public void commit() { 35 | em.joinTransaction(); 36 | } 37 | 38 | public void apply(List> transactionOperations) { 39 | if (null == transactionOperations) { 40 | return; 41 | } 42 | 43 | for (EntityCommand command : transactionOperations) { 44 | switch (command.getAction().ordinal()) { 45 | case 0: 46 | save(command.getEntity()); 47 | break; 48 | case 1: 49 | saveOrUpdate(command.getEntity()); 50 | break; 51 | case 2: 52 | remove(command.getEntity()); 53 | break; 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /rahub-cross-cutting-concerns/src/main/java/net/jotorren/microservices/tx/CompositeTransactionParticipantService.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.tx; 2 | 3 | import java.util.List; 4 | 5 | import net.jotorren.microservices.context.ThreadLocalContext; 6 | 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | 11 | public abstract class CompositeTransactionParticipantService { 12 | 13 | private static final Logger LOG = LoggerFactory.getLogger(CompositeTransactionParticipantService.class); 14 | 15 | public static final String CURRENT_TRANSACTION_KEY = "current.opened.tx"; 16 | 17 | @Autowired 18 | private CompositeTransactionManager txManager; 19 | 20 | public CompositeTransactionManager getCompositeTransactionManager(){ 21 | return txManager; 22 | } 23 | 24 | public void cancel(String txId) { 25 | ThreadLocalContext.remove(CURRENT_TRANSACTION_KEY); 26 | 27 | LOG.info("Rolling back transaction [{}]", txId); 28 | } 29 | 30 | public void confirm(String txId) { 31 | ThreadLocalContext.remove(CURRENT_TRANSACTION_KEY); 32 | 33 | LOG.info("Looking for transaction [{}]", txId); 34 | List> transactionOperations = txManager.fetch(txId); 35 | if (null == transactionOperations){ 36 | LOG.warn("Transaction [{}] does not exist. Ignoring commit call", txId); 37 | return; 38 | } 39 | CompositeTransactionParticipantDao unsynchronizedDao = getCompositeTransactionDao(); 40 | unsynchronizedDao.apply(transactionOperations); 41 | 42 | LOG.info("Committing transaction [{}]", txId); 43 | unsynchronizedDao.commit(); 44 | } 45 | 46 | public abstract CompositeTransactionParticipantDao getCompositeTransactionDao(); 47 | } 48 | -------------------------------------------------------------------------------- /rahub-cross-cutting-concerns/src/main/java/net/jotorren/microservices/tx/EntityCommand.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.tx; 2 | 3 | import java.io.Serializable; 4 | 5 | public class EntityCommand implements Serializable{ 6 | private static final long serialVersionUID = -3927305961526601453L; 7 | 8 | public enum Action { 9 | INSERT, UPDATE, DELETE, QUERY 10 | } 11 | 12 | private T entity; 13 | private Action action; 14 | private String transactionId; 15 | private long timestamp; 16 | 17 | public T getEntity() { 18 | return entity; 19 | } 20 | 21 | public void setEntity(T entity) { 22 | this.entity = entity; 23 | } 24 | 25 | public Action getAction() { 26 | return action; 27 | } 28 | 29 | public void setAction(Action action) { 30 | this.action = action; 31 | } 32 | 33 | public String getTransactionId() { 34 | return transactionId; 35 | } 36 | 37 | public void setTransactionId(String transactionId) { 38 | this.transactionId = transactionId; 39 | } 40 | 41 | public long getTimestamp() { 42 | return timestamp; 43 | } 44 | 45 | public void setTimestamp(long timestamp) { 46 | this.timestamp = timestamp; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /rahub-cross-cutting-concerns/src/main/java/net/jotorren/microservices/tx/Serializer.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.tx; 2 | 3 | import org.springframework.core.serializer.support.SerializationFailedException; 4 | 5 | public interface Serializer { 6 | 7 | byte[] write(T object) throws SerializationFailedException; 8 | String writeToString(T object) throws SerializationFailedException; 9 | 10 | T read(byte[] bytes) throws SerializationFailedException; 11 | T readFromString(String chars) throws SerializationFailedException; 12 | } 13 | -------------------------------------------------------------------------------- /rahub-cross-cutting-concerns/src/main/java/net/jotorren/microservices/tx/impl/CompositeTransactionManagerKafkaImpl.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.tx.impl; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.Properties; 8 | import java.util.UUID; 9 | 10 | import kafka.admin.AdminUtils; 11 | import kafka.admin.RackAwareMode; 12 | import kafka.utils.ZkUtils; 13 | import net.jotorren.microservices.tx.CompositeTransactionManager; 14 | import net.jotorren.microservices.tx.EntityCommand; 15 | import net.jotorren.microservices.tx.Serializer; 16 | 17 | import org.apache.kafka.clients.consumer.ConsumerConfig; 18 | import org.apache.kafka.clients.consumer.ConsumerRecord; 19 | import org.apache.kafka.clients.consumer.ConsumerRecords; 20 | import org.apache.kafka.clients.consumer.KafkaConsumer; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | import org.springframework.beans.factory.annotation.Autowired; 24 | import org.springframework.beans.factory.annotation.Qualifier; 25 | import org.springframework.beans.factory.annotation.Value; 26 | import org.springframework.core.serializer.support.SerializationFailedException; 27 | import org.springframework.kafka.core.KafkaTemplate; 28 | 29 | public class CompositeTransactionManagerKafkaImpl implements CompositeTransactionManager { 30 | 31 | private static final Logger LOG = LoggerFactory.getLogger(CompositeTransactionManager.class); 32 | 33 | @Value("${zookeeper.partitions:0}") 34 | private int zooPartitions; 35 | 36 | @Value("${zookeeper.replication:0}") 37 | private int zooReplication; 38 | 39 | @Value("${kafka.consumer.poll.timeout:0}") 40 | private long kafkaConsumerPollTimeout; 41 | 42 | 43 | @Autowired(required = false) 44 | private ZkUtils zkUtils; 45 | 46 | @Autowired(required = false) 47 | private KafkaTemplate kafkaTemplate; 48 | 49 | @Autowired(required = false) 50 | @Qualifier("kafkaConsumerConfiguration") 51 | private Map configuration; 52 | 53 | @Autowired(required = false) 54 | private Serializer> serializer; 55 | 56 | @Override 57 | public void open(String txId) { 58 | // Add topic configuration here 59 | Properties topicConfig = new Properties(); 60 | 61 | AdminUtils.createTopic(zkUtils, txId, zooPartitions, zooReplication, topicConfig, RackAwareMode.Enforced$.MODULE$); 62 | } 63 | 64 | @Override 65 | public void enlist(String txId, EntityCommand command) { 66 | kafkaTemplate.send(txId, serializer.writeToString(command)); 67 | } 68 | 69 | @SuppressWarnings("unchecked") 70 | @Override 71 | public List> fetch(String txId) { 72 | List> transactionOperations = new ArrayList>(); 73 | 74 | Map consumerConfigs = (Map)configuration.get("kafkaConsumerConfiguration"); 75 | consumerConfigs.put(ConsumerConfig.GROUP_ID_CONFIG, UUID.randomUUID().toString()); 76 | 77 | KafkaConsumer kafkaConsumer = new KafkaConsumer(consumerConfigs); 78 | kafkaConsumer.subscribe(Arrays.asList(txId)); 79 | 80 | ConsumerRecords records = kafkaConsumer.poll(kafkaConsumerPollTimeout); 81 | for (ConsumerRecord record : records){ 82 | LOG.info("offset = {}, key = {}, value = {}", record.offset(), record.key(), record.value()); 83 | try { 84 | transactionOperations.add(serializer.readFromString(record.value())); 85 | } catch (SerializationFailedException e) { 86 | LOG.error("Unable to deserialize [{}] because of: {}", record.value(), e.getMessage()); 87 | } 88 | } 89 | 90 | kafkaConsumer.close(); 91 | 92 | return transactionOperations; 93 | } 94 | 95 | @Override 96 | public void close(String txId) { 97 | AdminUtils.deleteTopic(zkUtils, txId); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /rahub-cross-cutting-concerns/src/main/java/net/jotorren/microservices/tx/impl/CompositeTransactionManagerMemoryImpl.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.tx.impl; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.concurrent.ConcurrentHashMap; 7 | 8 | import net.jotorren.microservices.tx.CompositeTransactionManager; 9 | import net.jotorren.microservices.tx.EntityCommand; 10 | 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | public class CompositeTransactionManagerMemoryImpl implements CompositeTransactionManager { 15 | 16 | private static final Logger LOG = LoggerFactory.getLogger(CompositeTransactionManager.class); 17 | 18 | private Map>> transactions = new ConcurrentHashMap>>(); 19 | 20 | @Override 21 | public void open(String txId) { 22 | if (transactions.containsKey(txId)) { 23 | throw new IllegalArgumentException("The composite transaction ["+ txId + "] already exists"); 24 | } 25 | transactions.put(txId, new ArrayList>()); 26 | } 27 | 28 | @Override 29 | public void enlist(String txId, EntityCommand ec) { 30 | if (!transactions.containsKey(txId)) { 31 | throw new IllegalArgumentException("The composite transaction ["+ txId + "] does not exist"); 32 | } 33 | 34 | List> transactionOperations = transactions.get(txId); 35 | transactionOperations.add(ec); 36 | } 37 | 38 | @Override 39 | public List> fetch(String txId) { 40 | List> transactionOperations = transactions.get(txId); 41 | if (null == transactionOperations) { 42 | throw new IllegalArgumentException("The composite transaction ["+ txId + "] does not exist"); 43 | } 44 | 45 | return transactionOperations; 46 | } 47 | 48 | @Override 49 | public void close(String txId) { 50 | List> transactionOperations = transactions.remove(txId); 51 | if (null == transactionOperations) { 52 | LOG.warn("The composite transaction [{}] does not exist", txId); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /rahub-forum-service/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | tmp 4 | target 5 | 6 | .settings 7 | .classpath 8 | .project 9 | 10 | # Ignore Test reporters 11 | ####################### 12 | test 13 | coverage 14 | report 15 | doc 16 | -------------------------------------------------------------------------------- /rahub-forum-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | net.jotorren.microservices 8 | rahub 9 | 1.0.0 10 | 11 | 12 | rahub-forum-service 13 | rahub-forum-service 14 | Spring Boot raHub Forum Service 15 | 16 | 17 | net.jotorren.microservices.forum.RahubForumServiceApplication 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-data-jpa 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-jersey 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-web 32 | 33 | 34 | org.springframework.kafka 35 | spring-kafka 36 | 37 | 38 | 39 | net.jotorren.microservices 40 | rahub-cross-cutting-concerns 41 | 42 | 43 | 44 | com.h2database 45 | h2 46 | 47 | 48 | 49 | 50 | 51 | com.101tec 52 | zkclient 53 | 54 | 55 | org.apache.kafka 56 | kafka_2.11 57 | 58 | 59 | 60 | io.swagger 61 | swagger-jersey2-jaxrs 62 | 63 | 64 | 65 | org.springframework.boot 66 | spring-boot-starter-test 67 | test 68 | 69 | 70 | org.springframework.restdocs 71 | spring-restdocs-mockmvc 72 | test 73 | 74 | 75 | -------------------------------------------------------------------------------- /rahub-forum-service/src/main/java/net/jotorren/microservices/forum/RahubForumServiceApplication.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.forum; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class RahubForumServiceApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(RahubForumServiceApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /rahub-forum-service/src/main/java/net/jotorren/microservices/forum/configuration/CompositeTransactionConfiguration.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.forum.configuration; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import net.jotorren.microservices.context.SpringContextProvider; 7 | import net.jotorren.microservices.tx.CompositeTransactionManager; 8 | import net.jotorren.microservices.tx.impl.CompositeTransactionManagerKafkaImpl; 9 | 10 | import org.apache.kafka.clients.consumer.ConsumerConfig; 11 | import org.apache.kafka.clients.producer.ProducerConfig; 12 | import org.apache.kafka.common.serialization.StringDeserializer; 13 | import org.apache.kafka.common.serialization.StringSerializer; 14 | import org.springframework.beans.factory.annotation.Value; 15 | import org.springframework.context.annotation.Bean; 16 | import org.springframework.context.annotation.Configuration; 17 | import org.springframework.kafka.core.DefaultKafkaProducerFactory; 18 | import org.springframework.kafka.core.KafkaTemplate; 19 | import org.springframework.kafka.core.ProducerFactory; 20 | 21 | @Configuration 22 | public class CompositeTransactionConfiguration { 23 | 24 | @Value("${kafka.bootstrap-servers}") 25 | private String kafkaBootstrapServers; 26 | 27 | @Value("${kafka.consumer.enable.auto.commit}") 28 | private String kafkaEnableAutoCommit; 29 | 30 | @Value("${kafka.consumer.auto.commit.interval}") 31 | private String kafkaAutoCommitInterval; 32 | 33 | @Value("${kafka.consumer.session.timeout}") 34 | private String kafkaSessionTimeout; 35 | 36 | @Value("${kafka.consumer.auto.offset.reset}") 37 | private String kafkaAutoOffsetReset; 38 | 39 | @Bean 40 | public SpringContextProvider springContextProvider(){ 41 | return new SpringContextProvider(); 42 | } 43 | 44 | @Bean 45 | public CompositeTransactionManager compositeTransactionManager() { 46 | return new CompositeTransactionManagerKafkaImpl(); 47 | } 48 | 49 | @Bean 50 | public Map kafkaConsumerConfiguration() { 51 | Map propsMap = new HashMap<>(); 52 | // list of host:port pairs used for establishing the initial connections to the Kakfa cluster 53 | propsMap.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaBootstrapServers); 54 | propsMap.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, kafkaEnableAutoCommit); 55 | propsMap.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, kafkaAutoCommitInterval); 56 | propsMap.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, kafkaSessionTimeout); 57 | propsMap.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); 58 | propsMap.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); 59 | propsMap.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, kafkaAutoOffsetReset); 60 | return propsMap; 61 | } 62 | 63 | @Bean 64 | public Map kafkaProducerConfiguration() { 65 | Map props = new HashMap<>(); 66 | // list of host:port pairs used for establishing the initial connections to the Kakfa cluster 67 | props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaBootstrapServers); 68 | props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); 69 | props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); 70 | 71 | return props; 72 | } 73 | 74 | @Bean 75 | public ProducerFactory kafkaProducerFactory() { 76 | return new DefaultKafkaProducerFactory<>(kafkaProducerConfiguration()); 77 | } 78 | 79 | @Bean 80 | public KafkaTemplate kafkaTemplate() { 81 | return new KafkaTemplate(kafkaProducerFactory()); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /rahub-forum-service/src/main/java/net/jotorren/microservices/forum/configuration/H2DatabaseConfiguration.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.forum.configuration; 2 | 3 | import org.h2.server.web.WebServlet; 4 | import org.springframework.boot.web.servlet.ServletRegistrationBean; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | public class H2DatabaseConfiguration { 10 | 11 | @Bean 12 | ServletRegistrationBean h2servletRegistration() { 13 | ServletRegistrationBean registrationBean = new ServletRegistrationBean(new WebServlet()); 14 | registrationBean.addUrlMappings("/h2/console/*"); 15 | return registrationBean; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /rahub-forum-service/src/main/java/net/jotorren/microservices/forum/configuration/WebServicesConfiguration.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.forum.configuration; 2 | 3 | import io.swagger.jaxrs.config.BeanConfig; 4 | import io.swagger.jaxrs.listing.ApiListingResource; 5 | import io.swagger.jaxrs.listing.SwaggerSerializers; 6 | 7 | import java.util.logging.Level; 8 | import java.util.logging.Logger; 9 | 10 | import javax.annotation.PostConstruct; 11 | 12 | import net.jotorren.microservices.forum.controller.ForumController; 13 | 14 | import org.glassfish.jersey.logging.LoggingFeature; 15 | import org.glassfish.jersey.server.ResourceConfig; 16 | import org.springframework.beans.factory.annotation.Value; 17 | import org.springframework.context.annotation.Configuration; 18 | 19 | @Configuration 20 | public class WebServicesConfiguration extends ResourceConfig { 21 | 22 | /** 23 | * maximum number of entity bytes to be logged (and buffered) - if the entity is larger, 24 | * logging filter will print (and buffer in memory) only the specified number of bytes 25 | * and print "...more..." string at the end. Negative values are interpreted as zero. 26 | */ 27 | @Value("${spring.jersey.log.entity.size:2048}") 28 | private int maxlog; 29 | 30 | @Value("${swagger.title}") 31 | private String title; 32 | 33 | @Value("${swagger.description}") 34 | private String description; 35 | 36 | @Value("${swagger.version}") 37 | private String version; 38 | 39 | @Value("${swagger.contact}") 40 | private String contact; 41 | 42 | @Value("${swagger.schemes}") 43 | private String schemes; 44 | 45 | @Value("${swagger.basePath}") 46 | private String basePath; 47 | 48 | @Value("${swagger.resourcePackage}") 49 | private String resourcePackage; 50 | 51 | @Value("${swagger.prettyPrint}") 52 | private boolean prettyPrint; 53 | 54 | @Value("${swagger.scan}") 55 | private boolean scan; 56 | 57 | private void configureSwagger() { 58 | // Available at localhost:port/swagger.json 59 | this.register(ApiListingResource.class); 60 | this.register(SwaggerSerializers.class); 61 | 62 | BeanConfig config = new BeanConfig(); 63 | // config.setConfigId(title); 64 | config.setTitle(title); 65 | config.setDescription(description); 66 | config.setVersion(version); 67 | config.setContact(contact); 68 | config.setSchemes(schemes.split(",")); 69 | config.setBasePath(basePath); 70 | config.setResourcePackage(resourcePackage); 71 | config.setPrettyPrint(prettyPrint); 72 | config.setScan(scan); 73 | } 74 | 75 | public WebServicesConfiguration() { 76 | register(ForumController.class); 77 | register(new LoggingFeature( 78 | Logger.getLogger(LoggingFeature.DEFAULT_LOGGER_NAME), 79 | Level.SEVERE, 80 | LoggingFeature.Verbosity.PAYLOAD_ANY, 81 | maxlog)); 82 | } 83 | 84 | @PostConstruct 85 | public void init() { 86 | // Register components where DI is needed 87 | this.configureSwagger(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /rahub-forum-service/src/main/java/net/jotorren/microservices/forum/controller/ForumController.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.forum.controller; 2 | 3 | import java.net.URI; 4 | 5 | import javax.ws.rs.GET; 6 | import javax.ws.rs.POST; 7 | import javax.ws.rs.Path; 8 | import javax.ws.rs.PathParam; 9 | import javax.ws.rs.Produces; 10 | import javax.ws.rs.WebApplicationException; 11 | import javax.ws.rs.core.Context; 12 | import javax.ws.rs.core.MediaType; 13 | import javax.ws.rs.core.Response; 14 | import javax.ws.rs.core.UriInfo; 15 | 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.web.bind.annotation.RestController; 20 | 21 | import io.swagger.annotations.Api; 22 | import io.swagger.annotations.ApiOperation; 23 | import io.swagger.annotations.ApiParam; 24 | import io.swagger.annotations.ApiResponse; 25 | import io.swagger.annotations.ApiResponses; 26 | import io.swagger.annotations.ResponseHeader; 27 | import net.jotorren.microservices.forum.domain.Forum; 28 | import net.jotorren.microservices.forum.service.ForumService; 29 | import net.jotorren.microservices.rs.ExceptionRestHandler; 30 | import net.jotorren.microservices.tx.CompositeTransactionParticipantController; 31 | import net.jotorren.microservices.tx.CompositeTransactionParticipantService; 32 | 33 | @RestController 34 | @Path("/") 35 | @Api(value = "Discussion board services") 36 | public class ForumController extends CompositeTransactionParticipantController { 37 | 38 | private static final Logger LOG = LoggerFactory.getLogger(ForumController.class); 39 | 40 | @Autowired 41 | private ForumService service; 42 | 43 | @Override 44 | public CompositeTransactionParticipantService getCompositeTransactionParticipantService() { 45 | return service; 46 | } 47 | 48 | @GET 49 | @Path("{id}") 50 | @Produces("application/json") 51 | @ApiOperation( 52 | code = 200, 53 | value = "Find a discussion board by its Id", 54 | notes = "Queries data previously persisted in the database", 55 | response = Forum.class, 56 | produces = "application/json" 57 | ) 58 | public Forum get( 59 | @ApiParam(value = "Id of the discussion board to retrieve", required = true) @PathParam("id") String id 60 | ) { 61 | LOG.info("Trying to get discussion board [{}] outside any transaction", id); 62 | 63 | return service.getForum(id); 64 | } 65 | 66 | @POST 67 | @ApiOperation( 68 | code = 201, 69 | value = "Save a new discussion board in the database", 70 | notes = "The newly created resource can be referenced by the URI returned in the the Location header field", 71 | response = String.class, 72 | responseHeaders = { 73 | @ResponseHeader(name = "Location", description = "The URI of the saved discussion board", response = String.class) 74 | } 75 | ) 76 | @ApiResponses(value = { 77 | @ApiResponse(code=500, message="Error saving the given discussion board", response = String.class) 78 | }) 79 | public Response save(@Context UriInfo uriInfo, 80 | @ApiParam(value = "Data of the discussion board, including the Id of the referred source code item", required = true) Forum content 81 | ) { 82 | try { 83 | LOG.info("Trying to save discussion board outside any transaction"); 84 | 85 | String id = service.addNewForum(content); 86 | LOG.info("New discussion board id set to [{}]", id); 87 | 88 | URI location = uriInfo.getAbsolutePathBuilder().path("{id}") 89 | .resolveTemplate("id", id).build(); 90 | LOG.info("New discussion board uri [{}]", location); 91 | 92 | return Response.created(location).build(); 93 | } catch (Exception e){ 94 | Response response = Response.status(Response.Status.INTERNAL_SERVER_ERROR) 95 | .entity(new ExceptionRestHandler().toString(e)).type(MediaType.TEXT_PLAIN).build(); 96 | throw new WebApplicationException(response); 97 | } 98 | } 99 | 100 | // Composite Transaction methods 101 | 102 | @GET 103 | @Path("{txid}/{id}") 104 | @Produces("application/json") 105 | @ApiOperation( 106 | code = 200, 107 | value = "Find a discussion board by its Id", 108 | notes = "Queries the transaction uncommitted data in addition to the one previously persisted in the database", 109 | response = Forum.class, 110 | produces = "application/json" 111 | ) 112 | public Forum getTxAware( 113 | @ApiParam(value = "Id of a composite transaction", required = true) @PathParam("txid") String txid, 114 | @ApiParam(value = "Id of the discussion board to retrieve", required = true) @PathParam("id") String id 115 | ) { 116 | LOG.info("Trying to get discussion board [{}] inside transaction [{}]", id, txid); 117 | 118 | return service.getForum(txid, id); 119 | } 120 | 121 | @POST 122 | @Path("{txid}") 123 | @ApiOperation( 124 | code = 201, 125 | value = "Save a new discussion board enlisting the operation in a composite transaction", 126 | notes = "No data will be persisted in the database until the transaction is explicitly committed. " 127 | + "The newly created resource can be referenced by the URI returned in the the Location header field", 128 | response = String.class, 129 | responseHeaders = { 130 | @ResponseHeader(name = "Location", description = "The URI of the saved discussion board", response = String.class) 131 | } 132 | ) 133 | @ApiResponses(value = { 134 | @ApiResponse(code=500, message="Error saving the given discussion board", response = String.class) 135 | }) 136 | public Response saveTxAware(@Context UriInfo uriInfo, 137 | @ApiParam(value = "Id of the composite transaction where the operation must be enlisted", required = true) @PathParam("txid") String txid, 138 | @ApiParam(value = "Data of the discussion board, including the Id of the referred source code item", required = true) Forum content) { 139 | try{ 140 | LOG.info("Trying to save discussion board inside transaction [{}]", txid); 141 | 142 | String id = service.addNewForum(txid, content); 143 | LOG.info("New discussion board id set to [{}]", id); 144 | 145 | URI location = uriInfo.getAbsolutePathBuilder().path("{id}") 146 | .resolveTemplate("id", id).build(); 147 | LOG.info("New discussion board uri [{}]", location); 148 | 149 | return Response.created(location).build(); 150 | } catch (Exception e){ 151 | Response response = Response.status(Response.Status.INTERNAL_SERVER_ERROR) 152 | .entity(new ExceptionRestHandler().toString(e)).type(MediaType.TEXT_PLAIN).build(); 153 | throw new WebApplicationException(response); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /rahub-forum-service/src/main/java/net/jotorren/microservices/forum/dao/ForumDao.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.forum.dao; 2 | 3 | import net.jotorren.microservices.forum.domain.Forum; 4 | 5 | import org.springframework.data.repository.CrudRepository; 6 | 7 | public interface ForumDao extends CrudRepository { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /rahub-forum-service/src/main/java/net/jotorren/microservices/forum/dao/ForumTransactionAwareDao.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.forum.dao; 2 | 3 | import net.jotorren.microservices.forum.domain.Forum; 4 | import net.jotorren.microservices.tx.CompositeTransactionParticipantDao; 5 | 6 | import org.springframework.context.annotation.Scope; 7 | import org.springframework.stereotype.Repository; 8 | 9 | @Repository 10 | @Scope("prototype") 11 | public class ForumTransactionAwareDao extends CompositeTransactionParticipantDao{ 12 | 13 | public Forum findOne(String pk){ 14 | return getEntityManager().find(Forum.class, pk); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /rahub-forum-service/src/main/java/net/jotorren/microservices/forum/domain/Forum.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.forum.domain; 2 | 3 | import java.io.Serializable; 4 | 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.Id; 8 | 9 | @Entity 10 | public class Forum implements Serializable { 11 | private static final long serialVersionUID = -8739470216693425165L; 12 | 13 | @Id 14 | @Column(nullable=false) 15 | private String forumId; 16 | 17 | @Column(nullable=false) 18 | private String topicName; 19 | 20 | @Column 21 | private String topicCategory; 22 | 23 | @Column(nullable=false) 24 | private String subjectId; 25 | 26 | public String getForumId() { 27 | return forumId; 28 | } 29 | 30 | public void setForumId(String forumId) { 31 | this.forumId = forumId; 32 | } 33 | 34 | public String getTopicName() { 35 | return topicName; 36 | } 37 | 38 | public void setTopicName(String topicName) { 39 | this.topicName = topicName; 40 | } 41 | 42 | public String getTopicCategory() { 43 | return topicCategory; 44 | } 45 | 46 | public void setTopicCategory(String topicCategory) { 47 | this.topicCategory = topicCategory; 48 | } 49 | 50 | public String getSubjectId() { 51 | return subjectId; 52 | } 53 | 54 | public void setSubjectId(String subjectId) { 55 | this.subjectId = subjectId; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /rahub-forum-service/src/main/java/net/jotorren/microservices/forum/domain/ForumSerializer.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.forum.domain; 2 | 3 | import java.io.IOException; 4 | 5 | import net.jotorren.microservices.tx.AbstractEntityCommandJsonSerializer; 6 | import net.jotorren.microservices.tx.EntityCommand; 7 | 8 | import org.springframework.core.serializer.support.SerializationFailedException; 9 | import org.springframework.stereotype.Component; 10 | 11 | import com.fasterxml.jackson.core.JsonParseException; 12 | import com.fasterxml.jackson.databind.JsonMappingException; 13 | import com.fasterxml.jackson.databind.JsonNode; 14 | 15 | @Component 16 | public class ForumSerializer extends AbstractEntityCommandJsonSerializer{ 17 | 18 | @Override 19 | public EntityCommand readFromString(String chars) 20 | throws SerializationFailedException { 21 | try { 22 | EntityCommand command = new EntityCommand(); 23 | 24 | JsonNode node = getJacksonMapper().readValue(chars, JsonNode.class); 25 | command.setAction(EntityCommand.Action.valueOf(node.get("action").asText())); 26 | command.setTransactionId(node.get("transactionId").asText()); 27 | command.setTimestamp(node.get("timestamp").asLong()); 28 | command.setEntity(getJacksonMapper().readValue(node.get("entity").toString(), Forum.class)); 29 | 30 | return command; 31 | } catch (JsonParseException e) { 32 | throw new SerializationFailedException(e.getMessage()); 33 | } catch (JsonMappingException e) { 34 | throw new SerializationFailedException(e.getMessage()); 35 | } catch (IOException e) { 36 | throw new SerializationFailedException(e.getMessage()); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /rahub-forum-service/src/main/java/net/jotorren/microservices/forum/service/ForumService.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.forum.service; 2 | 3 | import java.util.List; 4 | import java.util.UUID; 5 | 6 | import net.jotorren.microservices.context.ThreadLocalContext; 7 | import net.jotorren.microservices.forum.dao.ForumDao; 8 | import net.jotorren.microservices.forum.dao.ForumTransactionAwareDao; 9 | import net.jotorren.microservices.forum.domain.Forum; 10 | import net.jotorren.microservices.tx.CompositeTransactionParticipantDao; 11 | import net.jotorren.microservices.tx.CompositeTransactionParticipantService; 12 | import net.jotorren.microservices.tx.EntityCommand; 13 | 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.context.ApplicationContext; 18 | import org.springframework.stereotype.Service; 19 | 20 | @Service 21 | public class ForumService extends CompositeTransactionParticipantService { 22 | 23 | private static final Logger LOG = LoggerFactory.getLogger(ForumService.class); 24 | 25 | @Autowired 26 | private ApplicationContext context; 27 | 28 | @Autowired 29 | private ForumDao dao; 30 | 31 | public String addNewForum(Forum Forum) { 32 | String uuid = UUID.randomUUID().toString(); 33 | Forum.setForumId(uuid); 34 | 35 | Forum saved = dao.save(Forum); 36 | return saved.getForumId(); 37 | } 38 | 39 | public Forum getForum(String pk) { 40 | return dao.findOne(pk); 41 | } 42 | 43 | // Composite Transaction methods 44 | 45 | @Override 46 | public CompositeTransactionParticipantDao getCompositeTransactionDao() { 47 | return context.getBean(ForumTransactionAwareDao.class); 48 | } 49 | 50 | public String addNewForum(String txId, Forum Forum) { 51 | ThreadLocalContext.put(CURRENT_TRANSACTION_KEY, txId); 52 | 53 | String uuid = UUID.randomUUID().toString(); 54 | Forum.setForumId(uuid); 55 | 56 | LOG.info("Creating transaction [{}]", txId); 57 | Forum saved = getCompositeTransactionDao().saveOrUpdate(Forum); 58 | 59 | return saved.getForumId(); 60 | } 61 | 62 | public Forum getForum(String txId, String pk) { 63 | ThreadLocalContext.remove(CURRENT_TRANSACTION_KEY); 64 | 65 | LOG.warn("Looking for transaction [{}]", txId); 66 | List> transactionOperations = getCompositeTransactionManager().fetch(txId); 67 | if (null == transactionOperations){ 68 | LOG.error("Transaction [{}] does not exist", txId); 69 | return null; 70 | } 71 | ForumTransactionAwareDao unsynchronizedDao = (ForumTransactionAwareDao)getCompositeTransactionDao(); 72 | unsynchronizedDao.apply(transactionOperations); 73 | 74 | return unsynchronizedDao.findOne(pk); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /rahub-forum-service/src/main/resources/META-INF/orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /rahub-forum-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port = 8092 2 | 3 | spring.datasource.url = jdbc:h2:file:~/rahub-forum-db 4 | spring.datasource.username = sa 5 | spring.datasource.password = 6 | spring.datasource.driverClassName = org.h2.Driver 7 | 8 | #spring.jpa.generate-ddl: false 9 | #spring.jpa.hibernate.ddl-auto: none 10 | spring.jpa.show-sql: true 11 | spring.jpa.properties.hibernate.format_sql=true 12 | 13 | # Spring MVC dispatcher servlet path. Needs to be different than Jersey's to enable/disable Actuator endpoints access (/info, /health, ...) 14 | server.servlet-path: / 15 | # Jersey dispatcher servlet 16 | spring.jersey.application-path: /forum 17 | spring.jersey.log.entity.size = 4096 18 | 19 | # http://localhost:8092/forum/swagger.json 20 | # http://localhost:8092/index.html?url=/forum/swagger.json 21 | swagger.title = rahub-forum-service 22 | swagger.description = Spring Boot raHub Forum Service 23 | swagger.version = 1.0.0 24 | swagger.contact = jotorren 25 | #swagger.schemes = http,https 26 | swagger.schemes = http 27 | swagger.basePath = /forum 28 | swagger.resourcePackage = net.jotorren.microservices.forum.controller 29 | swagger.prettyPrint = true 30 | swagger.scan = true 31 | 32 | zookeeper.servers = localhost:2181 33 | zookeeper.session.timeout = 10000 34 | zookeeper.connection.timeout = 8000 35 | 36 | kafka.bootstrap-servers = localhost:9092 37 | kafka.consumer.enable.auto.commit = false 38 | kafka.consumer.auto.commit.interval = 1000 39 | kafka.consumer.session.timeout = 15000 40 | kafka.consumer.auto.offset.reset = earliest 41 | kafka.consumer.poll.timeout = 1000 -------------------------------------------------------------------------------- /rahub-forum-service/src/main/resources/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jotorren/microservices-transactions-tcc/8e35e840376084987577dc8e73daa28b999b40dc/rahub-forum-service/src/main/resources/static/favicon-16x16.png -------------------------------------------------------------------------------- /rahub-forum-service/src/main/resources/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jotorren/microservices-transactions-tcc/8e35e840376084987577dc8e73daa28b999b40dc/rahub-forum-service/src/main/resources/static/favicon-32x32.png -------------------------------------------------------------------------------- /rahub-forum-service/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger UI 7 | 8 | 9 | 10 | 11 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |
69 | 70 | 71 | 72 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /rahub-forum-service/src/main/resources/static/oauth2-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 54 | -------------------------------------------------------------------------------- /rahub-forum-service/src/main/resources/static/swagger-ui-bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"swagger-ui-bundle.js","sources":["webpack:///swagger-ui-bundle.js"],"mappings":"AAAA;AAu/FA;AA6+FA;;;;;;;;;;;;;;;;;;;;;;;;;;AAoTA;;;;;;AAoIA;AAi7FA;AAmtCA;AAi0IA;AA2pJA;AA+uFA;AA2rGA;AAgiFA;AA0rFA;AAk9CA;AA2hDA;AA4rCA;AAg6EA;;;;;AA4gCA;AA02JA;;;;;;;;;;;;;;AAuyEA;AA4mIA;AAquJA;AAwsHA;AA2mGA;AAiiEA;AAq4DA;AA+2DA;AAmfA;;;;;;AAmrFA;AA62FA;;;;;AAy3CA;AA2qFA;AAw2CA;AAqkCA;AA++CA;AAsiFA;AAk2FA;;;;;;;;;AAm/CA;AA2zIA;AAk4DA;AAolDA","sourceRoot":""} -------------------------------------------------------------------------------- /rahub-forum-service/src/main/resources/static/swagger-ui-standalone-preset.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"swagger-ui-standalone-preset.js","sources":["webpack:///swagger-ui-standalone-preset.js"],"mappings":"AAAA;;;;;AA8QA;AAitGA;AA+vFA;;;;;;AA6eA;AAkvFA;AAu+CA;AAo+CA;AAgrCA;AAgyEA","sourceRoot":""} -------------------------------------------------------------------------------- /rahub-forum-service/src/main/resources/static/swagger-ui.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";.swagger-ui html{box-sizing:border-box}.swagger-ui *,.swagger-ui :after,.swagger-ui :before{box-sizing:inherit}.swagger-ui body{margin:0;background:#fafafa}.swagger-ui .wrapper{width:100%;max-width:1460px;margin:0 auto;padding:0 20px}.swagger-ui .opblock-tag-section{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.swagger-ui .opblock-tag{display:-webkit-box;display:-ms-flexbox;display:flex;padding:10px 20px 10px 10px;cursor:pointer;transition:all .2s;border-bottom:1px solid rgba(59,65,81,.3);-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .opblock-tag:hover{background:rgba(0,0,0,.02)}.swagger-ui .opblock-tag{font-size:24px;margin:0 0 5px;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .opblock-tag.no-desc span{-webkit-box-flex:1;-ms-flex:1;flex:1}.swagger-ui .opblock-tag svg{transition:all .4s}.swagger-ui .opblock-tag small{font-size:14px;font-weight:400;padding:0 10px;-webkit-box-flex:1;-ms-flex:1;flex:1;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .parаmeter__type{font-size:12px;padding:5px 0;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .view-line-link{position:relative;top:3px;width:20px;margin:0 5px;cursor:pointer;transition:all .5s}.swagger-ui .opblock{margin:0 0 15px;border:1px solid #000;border-radius:4px;box-shadow:0 0 3px rgba(0,0,0,.19)}.swagger-ui .opblock.is-open .opblock-summary{border-bottom:1px solid #000}.swagger-ui .opblock .opblock-section-header{padding:8px 20px;background:hsla(0,0%,100%,.8);box-shadow:0 1px 2px rgba(0,0,0,.1)}.swagger-ui .opblock .opblock-section-header,.swagger-ui .opblock .opblock-section-header label{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .opblock .opblock-section-header label{font-size:12px;font-weight:700;margin:0;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .opblock .opblock-section-header label span{padding:0 10px 0 0}.swagger-ui .opblock .opblock-section-header h4{font-size:14px;margin:0;-webkit-box-flex:1;-ms-flex:1;flex:1;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .opblock .opblock-summary-method{font-size:14px;font-weight:700;min-width:80px;padding:6px 15px;text-align:center;border-radius:3px;background:#000;text-shadow:0 1px 0 rgba(0,0,0,.1);font-family:Titillium Web,sans-serif;color:#fff}.swagger-ui .opblock .opblock-summary-path,.swagger-ui .opblock .opblock-summary-path__deprecated{font-size:16px;display:-webkit-box;display:-ms-flexbox;display:flex;padding:0 10px;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .opblock .opblock-summary-path .view-line-link,.swagger-ui .opblock .opblock-summary-path__deprecated .view-line-link{position:relative;top:2px;width:0;margin:0;cursor:pointer;transition:all .5s}.swagger-ui .opblock .opblock-summary-path:hover .view-line-link,.swagger-ui .opblock .opblock-summary-path__deprecated:hover .view-line-link{width:18px;margin:0 5px}.swagger-ui .opblock .opblock-summary-path__deprecated{text-decoration:line-through}.swagger-ui .opblock .opblock-summary-description{font-size:13px;-webkit-box-flex:1;-ms-flex:1;flex:1;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .opblock .opblock-summary{display:-webkit-box;display:-ms-flexbox;display:flex;padding:5px;cursor:pointer;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .opblock.opblock-post{border-color:#49cc90;background:rgba(73,204,144,.1)}.swagger-ui .opblock.opblock-post .opblock-summary-method{background:#49cc90}.swagger-ui .opblock.opblock-post .opblock-summary{border-color:#49cc90}.swagger-ui .opblock.opblock-put{border-color:#fca130;background:rgba(252,161,48,.1)}.swagger-ui .opblock.opblock-put .opblock-summary-method{background:#fca130}.swagger-ui .opblock.opblock-put .opblock-summary{border-color:#fca130}.swagger-ui .opblock.opblock-delete{border-color:#f93e3e;background:rgba(249,62,62,.1)}.swagger-ui .opblock.opblock-delete .opblock-summary-method{background:#f93e3e}.swagger-ui .opblock.opblock-delete .opblock-summary{border-color:#f93e3e}.swagger-ui .opblock.opblock-get{border-color:#61affe;background:rgba(97,175,254,.1)}.swagger-ui .opblock.opblock-get .opblock-summary-method{background:#61affe}.swagger-ui .opblock.opblock-get .opblock-summary{border-color:#61affe}.swagger-ui .opblock.opblock-patch{border-color:#50e3c2;background:rgba(80,227,194,.1)}.swagger-ui .opblock.opblock-patch .opblock-summary-method{background:#50e3c2}.swagger-ui .opblock.opblock-patch .opblock-summary{border-color:#50e3c2}.swagger-ui .opblock.opblock-head{border-color:#9012fe;background:rgba(144,18,254,.1)}.swagger-ui .opblock.opblock-head .opblock-summary-method{background:#9012fe}.swagger-ui .opblock.opblock-head .opblock-summary{border-color:#9012fe}.swagger-ui .opblock.opblock-options{border-color:#0d5aa7;background:rgba(13,90,167,.1)}.swagger-ui .opblock.opblock-options .opblock-summary-method{background:#0d5aa7}.swagger-ui .opblock.opblock-options .opblock-summary{border-color:#0d5aa7}.swagger-ui .opblock.opblock-deprecated{opacity:.6;border-color:#ebebeb;background:hsla(0,0%,92%,.1)}.swagger-ui .opblock.opblock-deprecated .opblock-summary-method{background:#ebebeb}.swagger-ui .opblock.opblock-deprecated .opblock-summary{border-color:#ebebeb}.swagger-ui .opblock .opblock-schemes{padding:8px 20px}.swagger-ui .opblock .opblock-schemes .schemes-title{padding:0 10px 0 0}.swagger-ui .tab{display:-webkit-box;display:-ms-flexbox;display:flex;margin:20px 0 10px;padding:0;list-style:none}.swagger-ui .tab li{font-size:12px;min-width:100px;min-width:90px;padding:0;cursor:pointer;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .tab li:first-of-type{position:relative;padding-left:0}.swagger-ui .tab li:first-of-type:after{position:absolute;top:0;right:6px;width:1px;height:100%;content:"";background:rgba(0,0,0,.2)}.swagger-ui .tab li.active{font-weight:700}.swagger-ui .opblock-description-wrapper,.swagger-ui .opblock-title_normal{padding:15px 20px}.swagger-ui .opblock-description-wrapper,.swagger-ui .opblock-description-wrapper h4,.swagger-ui .opblock-title_normal,.swagger-ui .opblock-title_normal h4{font-size:12px;margin:0 0 5px;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .opblock-description-wrapper p,.swagger-ui .opblock-title_normal p{font-size:14px;margin:0;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .execute-wrapper{padding:20px;text-align:right}.swagger-ui .execute-wrapper .btn{width:100%;padding:8px 40px}.swagger-ui .body-param-options{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.swagger-ui .body-param-options .body-param-edit{padding:10px 0}.swagger-ui .body-param-options label{padding:8px 0}.swagger-ui .body-param-options label select{margin:3px 0 0}.swagger-ui .responses-inner{padding:20px}.swagger-ui .responses-inner h4,.swagger-ui .responses-inner h5{font-size:12px;margin:10px 0 5px;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .response-col_status{font-size:14px;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .response-col_status .response-undocumented{font-size:11px;font-family:Source Code Pro,monospace;font-weight:600;color:#999}.swagger-ui .response-col_description__inner span{font-size:12px;font-style:italic;display:block;margin:10px 0;padding:10px;border-radius:4px;background:#41444e;font-family:Source Code Pro,monospace;font-weight:600;color:#fff}.swagger-ui .response-col_description__inner span p{margin:0}.swagger-ui .opblock-body pre{font-size:12px;margin:0;padding:10px;white-space:pre-wrap;border-radius:4px;background:#41444e;font-family:Source Code Pro,monospace;font-weight:600;color:#fff}.swagger-ui .opblock-body pre span{color:#fff!important}.swagger-ui .opblock-body pre .headerline{display:block}.swagger-ui .scheme-container{margin:0 0 20px;padding:30px 0;background:#fff;box-shadow:0 1px 2px 0 rgba(0,0,0,.15)}.swagger-ui .scheme-container .schemes{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .scheme-container .schemes>label{font-size:12px;font-weight:700;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:-20px 15px 0 0;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .scheme-container .schemes>label select{min-width:130px;text-transform:uppercase}.swagger-ui .loading-container{padding:40px 0 60px}.swagger-ui .loading-container .loading{position:relative}.swagger-ui .loading-container .loading:after{font-size:10px;font-weight:700;position:absolute;top:50%;left:50%;content:"loading";-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);text-transform:uppercase;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .loading-container .loading:before{position:absolute;top:50%;left:50%;display:block;width:60px;height:60px;margin:-30px;content:"";-webkit-animation:rotation 1s infinite linear,opacity .5s;animation:rotation 1s infinite linear,opacity .5s;opacity:1;border:2px solid rgba(85,85,85,.1);border-top-color:rgba(0,0,0,.6);border-radius:100%;-webkit-backface-visibility:hidden;backface-visibility:hidden}@-webkit-keyframes rotation{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes rotation{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@-webkit-keyframes blinker{50%{opacity:0}}@keyframes blinker{50%{opacity:0}}.swagger-ui .btn{font-size:14px;font-weight:700;padding:5px 23px;transition:all .3s;border:2px solid #888;border-radius:4px;background:transparent;box-shadow:0 1px 2px rgba(0,0,0,.1);font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .btn[disabled]{cursor:not-allowed;opacity:.3}.swagger-ui .btn:hover{box-shadow:0 0 5px rgba(0,0,0,.3)}.swagger-ui .btn.cancel{border-color:#ff6060;font-family:Titillium Web,sans-serif;color:#ff6060}.swagger-ui .btn.authorize{line-height:1;display:inline;color:#49cc90;border-color:#49cc90}.swagger-ui .btn.authorize span{float:left;padding:4px 20px 0 0}.swagger-ui .btn.authorize svg{fill:#49cc90}.swagger-ui .btn.execute{-webkit-animation:pulse 2s infinite;animation:pulse 2s infinite;color:#fff;border-color:#4990e2}@-webkit-keyframes pulse{0%{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,.8)}70%{box-shadow:0 0 0 5px rgba(73,144,226,0)}to{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,0)}}@keyframes pulse{0%{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,.8)}70%{box-shadow:0 0 0 5px rgba(73,144,226,0)}to{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,0)}}.swagger-ui .btn-group{display:-webkit-box;display:-ms-flexbox;display:flex;padding:30px}.swagger-ui .btn-group .btn{-webkit-box-flex:1;-ms-flex:1;flex:1}.swagger-ui .btn-group .btn:first-child{border-radius:4px 0 0 4px}.swagger-ui .btn-group .btn:last-child{border-radius:0 4px 4px 0}.swagger-ui .authorization__btn{padding:0 10px;border:none;background:none}.swagger-ui .authorization__btn.locked{opacity:1}.swagger-ui .authorization__btn.unlocked{opacity:.4}.swagger-ui .expand-methods,.swagger-ui .expand-operation{border:none;background:none}.swagger-ui .expand-methods svg,.swagger-ui .expand-operation svg{width:20px;height:20px}.swagger-ui .expand-methods{padding:0 10px}.swagger-ui .expand-methods:hover svg{fill:#444}.swagger-ui .expand-methods svg{transition:all .3s;fill:#777}.swagger-ui button{cursor:pointer;outline:none}.swagger-ui select{font-size:14px;font-weight:700;padding:5px 40px 5px 10px;border:2px solid #41444e;border-radius:4px;background:#f7f7f7 url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCI+ICAgIDxwYXRoIGQ9Ik0xMy40MTggNy44NTljLjI3MS0uMjY4LjcwOS0uMjY4Ljk3OCAwIC4yNy4yNjguMjcyLjcwMSAwIC45NjlsLTMuOTA4IDMuODNjLS4yNy4yNjgtLjcwNy4yNjgtLjk3OSAwbC0zLjkwOC0zLjgzYy0uMjctLjI2Ny0uMjctLjcwMSAwLS45NjkuMjcxLS4yNjguNzA5LS4yNjguOTc4IDBMMTAgMTFsMy40MTgtMy4xNDF6Ii8+PC9zdmc+) right 10px center no-repeat;background-size:20px;box-shadow:0 1px 2px 0 rgba(0,0,0,.25);font-family:Titillium Web,sans-serif;color:#3b4151;-webkit-appearance:none;-moz-appearance:none;appearance:none}.swagger-ui select[multiple]{margin:5px 0;padding:5px;background:#f7f7f7}.swagger-ui .opblock-body select{min-width:230px}.swagger-ui label{font-size:12px;font-weight:700;margin:0 0 5px;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui input[type=email],.swagger-ui input[type=password],.swagger-ui input[type=search],.swagger-ui input[type=text]{min-width:100px;margin:5px 0;padding:8px 10px;border:1px solid #d9d9d9;border-radius:4px;background:#fff}.swagger-ui input[type=email].invalid,.swagger-ui input[type=password].invalid,.swagger-ui input[type=search].invalid,.swagger-ui input[type=text].invalid{-webkit-animation:shake .4s 1;animation:shake .4s 1;border-color:#f93e3e;background:#feebeb}@-webkit-keyframes shake{10%,90%{-webkit-transform:translate3d(-1px,0,0);transform:translate3d(-1px,0,0)}20%,80%{-webkit-transform:translate3d(2px,0,0);transform:translate3d(2px,0,0)}30%,50%,70%{-webkit-transform:translate3d(-4px,0,0);transform:translate3d(-4px,0,0)}40%,60%{-webkit-transform:translate3d(4px,0,0);transform:translate3d(4px,0,0)}}@keyframes shake{10%,90%{-webkit-transform:translate3d(-1px,0,0);transform:translate3d(-1px,0,0)}20%,80%{-webkit-transform:translate3d(2px,0,0);transform:translate3d(2px,0,0)}30%,50%,70%{-webkit-transform:translate3d(-4px,0,0);transform:translate3d(-4px,0,0)}40%,60%{-webkit-transform:translate3d(4px,0,0);transform:translate3d(4px,0,0)}}.swagger-ui textarea{font-size:12px;width:100%;min-height:280px;padding:10px;border:none;border-radius:4px;outline:none;background:hsla(0,0%,100%,.8);font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui textarea:focus{border:2px solid #61affe}.swagger-ui textarea.curl{font-size:12px;min-height:100px;margin:0;padding:10px;resize:none;border-radius:4px;background:#41444e;font-family:Source Code Pro,monospace;font-weight:600;color:#fff}.swagger-ui .checkbox{padding:5px 0 10px;transition:opacity .5s;color:#333}.swagger-ui .checkbox label{display:-webkit-box;display:-ms-flexbox;display:flex}.swagger-ui .checkbox p{font-weight:400!important;font-style:italic;margin:0!important;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .checkbox input[type=checkbox]{display:none}.swagger-ui .checkbox input[type=checkbox]+label>.item{position:relative;top:3px;display:inline-block;width:16px;height:16px;margin:0 8px 0 0;padding:5px;cursor:pointer;border-radius:1px;background:#e8e8e8;box-shadow:0 0 0 2px #e8e8e8;-webkit-box-flex:0;-ms-flex:none;flex:none}.swagger-ui .checkbox input[type=checkbox]+label>.item:active{-webkit-transform:scale(.9);transform:scale(.9)}.swagger-ui .checkbox input[type=checkbox]:checked+label>.item{background:#e8e8e8 url("data:image/svg+xml;charset=utf-8,%3Csvg width='10' height='8' viewBox='3 7 10 8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%2341474E' fill-rule='evenodd' d='M6.333 15L3 11.667l1.333-1.334 2 2L11.667 7 13 8.333z'/%3E%3C/svg%3E") 50% no-repeat}.swagger-ui .dialog-ux{position:fixed;z-index:9999;top:0;right:0;bottom:0;left:0}.swagger-ui .dialog-ux .backdrop-ux{position:fixed;top:0;right:0;bottom:0;left:0;background:rgba(0,0,0,.8)}.swagger-ui .dialog-ux .modal-ux{position:absolute;z-index:9999;top:50%;left:50%;width:100%;min-width:300px;max-width:650px;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);border:1px solid #ebebeb;border-radius:4px;background:#fff;box-shadow:0 10px 30px 0 rgba(0,0,0,.2)}.swagger-ui .dialog-ux .modal-ux-content{overflow-y:auto;max-height:540px;padding:20px}.swagger-ui .dialog-ux .modal-ux-content p{font-size:12px;margin:0 0 5px;color:#41444e;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .dialog-ux .modal-ux-content h4{font-size:18px;font-weight:600;margin:15px 0 0;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .dialog-ux .modal-ux-header{display:-webkit-box;display:-ms-flexbox;display:flex;padding:12px 0;border-bottom:1px solid #ebebeb;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .dialog-ux .modal-ux-header .close-modal{padding:0 10px;border:none;background:none;-webkit-appearance:none;-moz-appearance:none;appearance:none}.swagger-ui .dialog-ux .modal-ux-header h3{font-size:20px;font-weight:600;margin:0;padding:0 20px;-webkit-box-flex:1;-ms-flex:1;flex:1;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .model{font-size:12px;font-weight:300;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .model-toggle{font-size:10px;position:relative;top:6px;display:inline-block;margin:auto .3em;cursor:pointer;transition:-webkit-transform .15s ease-in;transition:transform .15s ease-in;transition:transform .15s ease-in,-webkit-transform .15s ease-in;-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:50% 50%;transform-origin:50% 50%}.swagger-ui .model-toggle.collapsed{-webkit-transform:rotate(0deg);transform:rotate(0deg)}.swagger-ui .model-toggle:after{display:block;width:20px;height:20px;content:"";background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z'/%3E%3C/svg%3E") 50% no-repeat;background-size:100%}.swagger-ui .model-jump-to-path{position:relative;cursor:pointer}.swagger-ui .model-jump-to-path .view-line-link{position:absolute;top:-.4em;cursor:pointer}.swagger-ui .model-title{position:relative}.swagger-ui .model-title:hover .model-hint{visibility:visible}.swagger-ui .model-hint{position:absolute;top:-1.8em;visibility:hidden;padding:.1em .5em;white-space:nowrap;color:#ebebeb;border-radius:4px;background:rgba(0,0,0,.7)}.swagger-ui section.models{margin:30px 0;border:1px solid rgba(59,65,81,.3);border-radius:4px}.swagger-ui section.models.is-open{padding:0 0 20px}.swagger-ui section.models.is-open h4{margin:0 0 5px;border-bottom:1px solid rgba(59,65,81,.3)}.swagger-ui section.models.is-open h4 svg{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.swagger-ui section.models h4{font-size:16px;display:-webkit-box;display:-ms-flexbox;display:flex;margin:0;padding:10px 20px 10px 10px;cursor:pointer;transition:all .2s;font-family:Titillium Web,sans-serif;color:#777;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui section.models h4 svg{transition:all .4s}.swagger-ui section.models h4 span{-webkit-box-flex:1;-ms-flex:1;flex:1}.swagger-ui section.models h4:hover{background:rgba(0,0,0,.02)}.swagger-ui section.models h5{font-size:16px;margin:0 0 10px;font-family:Titillium Web,sans-serif;color:#777}.swagger-ui section.models .model-jump-to-path{position:relative;top:5px}.swagger-ui section.models .model-container{margin:0 20px 15px;transition:all .5s;border-radius:4px;background:rgba(0,0,0,.05)}.swagger-ui section.models .model-container:hover{background:rgba(0,0,0,.07)}.swagger-ui section.models .model-container:first-of-type{margin:20px}.swagger-ui section.models .model-container:last-of-type{margin:0 20px}.swagger-ui section.models .model-box{background:none}.swagger-ui .model-box{padding:10px;border-radius:4px;background:rgba(0,0,0,.1)}.swagger-ui .model-box .model-jump-to-path{position:relative;top:4px}.swagger-ui .model-title{font-size:16px;font-family:Titillium Web,sans-serif;color:#555}.swagger-ui span>span.model,.swagger-ui span>span.model .brace-close{padding:0 0 0 10px}.swagger-ui .prop-type{color:#55a}.swagger-ui .prop-enum{display:block}.swagger-ui .prop-format{color:#999}.swagger-ui table{width:100%;padding:0 10px;border-collapse:collapse}.swagger-ui table.model tbody tr td{padding:0;vertical-align:top}.swagger-ui table.model tbody tr td:first-of-type{width:100px;padding:0}.swagger-ui table.headers td{font-size:12px;font-weight:300;vertical-align:middle;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui table tbody tr td{padding:10px 0 0;vertical-align:top}.swagger-ui table tbody tr td:first-of-type{width:20%;padding:10px 0}.swagger-ui table thead tr td,.swagger-ui table thead tr th{font-size:12px;font-weight:700;padding:12px 0;text-align:left;border-bottom:1px solid rgba(59,65,81,.2);font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .parameters-col_description p{font-size:14px;margin:0;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .parameters-col_description input[type=text]{width:100%;max-width:340px}.swagger-ui .parameter__name{font-size:16px;font-weight:400;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .parameter__name.required{font-weight:700}.swagger-ui .parameter__name.required:after{font-size:10px;position:relative;top:-6px;padding:5px;content:"required";color:rgba(255,0,0,.6)}.swagger-ui .parameter__in{font-size:12px;font-style:italic;font-family:Source Code Pro,monospace;font-weight:600;color:#888}.swagger-ui .table-container{padding:20px}.swagger-ui .topbar{padding:8px 30px;background-color:#89bf04}.swagger-ui .topbar .topbar-wrapper{-ms-flex-align:center}.swagger-ui .topbar .topbar-wrapper,.swagger-ui .topbar a{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;align-items:center}.swagger-ui .topbar a{font-size:1.5em;font-weight:700;max-width:300px;text-decoration:none;-webkit-box-flex:1;-ms-flex:1;flex:1;-ms-flex-align:center;font-family:Titillium Web,sans-serif;color:#fff}.swagger-ui .topbar a span{margin:0;padding:0 10px}.swagger-ui .topbar .download-url-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:3;-ms-flex:3;flex:3}.swagger-ui .topbar .download-url-wrapper input[type=text]{width:100%;min-width:350px;margin:0;border:2px solid #547f00;border-radius:4px 0 0 4px;outline:none}.swagger-ui .topbar .download-url-wrapper .download-url-button{font-size:16px;font-weight:700;padding:4px 40px;border:none;border-radius:0 4px 4px 0;background:#547f00;font-family:Titillium Web,sans-serif;color:#fff}.swagger-ui .info{margin:50px 0}.swagger-ui .info hgroup.main{margin:0 0 20px}.swagger-ui .info hgroup.main a{font-size:12px}.swagger-ui .info p{font-size:14px;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .info code{padding:3px 5px;border-radius:4px;background:rgba(0,0,0,.05);font-family:Source Code Pro,monospace;font-weight:600;color:#9012fe}.swagger-ui .info a{font-size:14px;transition:all .4s;font-family:Open Sans,sans-serif;color:#4990e2}.swagger-ui .info a:hover{color:#1f69c0}.swagger-ui .info>div{margin:0 0 5px}.swagger-ui .info .base-url{font-size:12px;font-weight:300!important;margin:0;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .info .title{font-size:36px;margin:0;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .info .title small{font-size:10px;position:relative;top:-5px;display:inline-block;margin:0 0 0 5px;padding:2px 4px;vertical-align:super;border-radius:57px;background:#7d8492}.swagger-ui .info .title small pre{margin:0;font-family:Titillium Web,sans-serif;color:#fff}.swagger-ui .auth-btn-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;padding:10px 0;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.swagger-ui .auth-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.swagger-ui .auth-wrapper .authorize{padding-right:20px}.swagger-ui .auth-container{margin:0 0 10px;padding:10px 20px;border-bottom:1px solid #ebebeb}.swagger-ui .auth-container:last-of-type{margin:0;padding:10px 20px;border:0}.swagger-ui .auth-container h4{margin:5px 0 15px!important}.swagger-ui .auth-container .wrapper{margin:0;padding:0}.swagger-ui .auth-container input[type=password],.swagger-ui .auth-container input[type=text]{min-width:230px}.swagger-ui .auth-container .errors{font-size:12px;padding:10px;border-radius:4px;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .scopes h2{font-size:14px;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .scope-def{padding:0 0 20px}.swagger-ui .errors-wrapper{margin:20px;padding:10px 20px;-webkit-animation:scaleUp .5s;animation:scaleUp .5s;border:2px solid #f93e3e;border-radius:4px;background:rgba(249,62,62,.1)}.swagger-ui .errors-wrapper .error-wrapper{margin:0 0 10px}.swagger-ui .errors-wrapper .errors h4{font-size:14px;margin:0;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .errors-wrapper .errors small{color:#666}.swagger-ui .errors-wrapper hgroup{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .errors-wrapper hgroup h4{font-size:20px;margin:0;-webkit-box-flex:1;-ms-flex:1;flex:1;font-family:Titillium Web,sans-serif;color:#3b4151}@-webkit-keyframes scaleUp{0%{-webkit-transform:scale(.8);transform:scale(.8);opacity:0}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes scaleUp{0%{-webkit-transform:scale(.8);transform:scale(.8);opacity:0}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.swagger-ui .Resizer.vertical.disabled{display:none} 2 | /*# sourceMappingURL=swagger-ui.css.map*/ -------------------------------------------------------------------------------- /rahub-forum-service/src/main/resources/static/swagger-ui.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"swagger-ui.css","sources":[],"mappings":"","sourceRoot":""} -------------------------------------------------------------------------------- /rahub-forum-service/src/main/resources/static/swagger-ui.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"swagger-ui.js","sources":["webpack:///swagger-ui.js"],"mappings":"AAAA;;;;;;AAyxCA;AAoyHA;AAuxHA;AAy4FA;AA2sCA;AAmgCA;AA0iCA;AA64BA","sourceRoot":""} -------------------------------------------------------------------------------- /rahub-source-code-service/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | tmp 4 | target 5 | 6 | .settings 7 | .classpath 8 | .project 9 | 10 | # Ignore Test reporters 11 | ####################### 12 | test 13 | coverage 14 | report 15 | doc 16 | -------------------------------------------------------------------------------- /rahub-source-code-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | net.jotorren.microservices 8 | rahub 9 | 1.0.0 10 | 11 | 12 | rahub-source-code-service 13 | rahub-source-code-service 14 | Spring Boot raHub Source Code Service 15 | 16 | 17 | net.jotorren.microservices.content.RahubContentServiceApplication 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-data-jpa 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-jersey 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-web 32 | 33 | 34 | org.springframework.kafka 35 | spring-kafka 36 | 37 | 38 | 39 | net.jotorren.microservices 40 | rahub-cross-cutting-concerns 41 | 42 | 43 | 44 | com.h2database 45 | h2 46 | 47 | 48 | 49 | 50 | 51 | com.101tec 52 | zkclient 53 | 54 | 55 | org.apache.kafka 56 | kafka_2.11 57 | 58 | 59 | 60 | io.swagger 61 | swagger-jersey2-jaxrs 62 | 63 | 64 | 65 | org.springframework.boot 66 | spring-boot-starter-test 67 | test 68 | 69 | 70 | org.springframework.restdocs 71 | spring-restdocs-mockmvc 72 | test 73 | 74 | 75 | -------------------------------------------------------------------------------- /rahub-source-code-service/src/main/java/net/jotorren/microservices/content/RahubContentServiceApplication.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.content; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class RahubContentServiceApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(RahubContentServiceApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /rahub-source-code-service/src/main/java/net/jotorren/microservices/content/configuration/CompositeTransactionConfiguration.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.content.configuration; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import net.jotorren.microservices.context.SpringContextProvider; 7 | import net.jotorren.microservices.tx.CompositeTransactionManager; 8 | import net.jotorren.microservices.tx.impl.CompositeTransactionManagerKafkaImpl; 9 | 10 | import org.apache.kafka.clients.consumer.ConsumerConfig; 11 | import org.apache.kafka.clients.producer.ProducerConfig; 12 | import org.apache.kafka.common.serialization.StringDeserializer; 13 | import org.apache.kafka.common.serialization.StringSerializer; 14 | import org.springframework.beans.factory.annotation.Value; 15 | import org.springframework.context.annotation.Bean; 16 | import org.springframework.context.annotation.Configuration; 17 | import org.springframework.kafka.core.DefaultKafkaProducerFactory; 18 | import org.springframework.kafka.core.KafkaTemplate; 19 | import org.springframework.kafka.core.ProducerFactory; 20 | 21 | @Configuration 22 | public class CompositeTransactionConfiguration { 23 | 24 | @Value("${kafka.bootstrap-servers}") 25 | private String kafkaBootstrapServers; 26 | 27 | @Value("${kafka.consumer.enable.auto.commit}") 28 | private String kafkaEnableAutoCommit; 29 | 30 | @Value("${kafka.consumer.auto.commit.interval}") 31 | private String kafkaAutoCommitInterval; 32 | 33 | @Value("${kafka.consumer.session.timeout}") 34 | private String kafkaSessionTimeout; 35 | 36 | @Value("${kafka.consumer.auto.offset.reset}") 37 | private String kafkaAutoOffsetReset; 38 | 39 | @Bean 40 | public SpringContextProvider springContextProvider(){ 41 | return new SpringContextProvider(); 42 | } 43 | 44 | @Bean 45 | public CompositeTransactionManager compositeTransactionManager() { 46 | return new CompositeTransactionManagerKafkaImpl(); 47 | } 48 | 49 | @Bean 50 | public Map kafkaConsumerConfiguration() { 51 | Map propsMap = new HashMap<>(); 52 | // list of host:port pairs used for establishing the initial connections to the Kakfa cluster 53 | propsMap.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaBootstrapServers); 54 | propsMap.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, kafkaEnableAutoCommit); 55 | propsMap.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, kafkaAutoCommitInterval); 56 | propsMap.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, kafkaSessionTimeout); 57 | propsMap.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); 58 | propsMap.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); 59 | propsMap.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, kafkaAutoOffsetReset); 60 | return propsMap; 61 | } 62 | 63 | @Bean 64 | public Map kafkaProducerConfiguration() { 65 | Map props = new HashMap<>(); 66 | // list of host:port pairs used for establishing the initial connections to the Kakfa cluster 67 | props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaBootstrapServers); 68 | props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); 69 | props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); 70 | 71 | return props; 72 | } 73 | 74 | @Bean 75 | public ProducerFactory kafkaProducerFactory() { 76 | return new DefaultKafkaProducerFactory<>(kafkaProducerConfiguration()); 77 | } 78 | 79 | @Bean 80 | public KafkaTemplate kafkaTemplate() { 81 | return new KafkaTemplate(kafkaProducerFactory()); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /rahub-source-code-service/src/main/java/net/jotorren/microservices/content/configuration/H2DatabaseConfiguration.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.content.configuration; 2 | 3 | import org.h2.server.web.WebServlet; 4 | import org.springframework.boot.web.servlet.ServletRegistrationBean; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | public class H2DatabaseConfiguration { 10 | 11 | @Bean 12 | ServletRegistrationBean h2servletRegistration() { 13 | ServletRegistrationBean registrationBean = new ServletRegistrationBean(new WebServlet()); 14 | registrationBean.addUrlMappings("/h2/console/*"); 15 | return registrationBean; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /rahub-source-code-service/src/main/java/net/jotorren/microservices/content/configuration/WebServicesConfiguration.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.content.configuration; 2 | 3 | import java.util.logging.Level; 4 | import java.util.logging.Logger; 5 | 6 | import io.swagger.jaxrs.config.BeanConfig; 7 | import io.swagger.jaxrs.listing.ApiListingResource; 8 | import io.swagger.jaxrs.listing.SwaggerSerializers; 9 | 10 | import javax.annotation.PostConstruct; 11 | 12 | import net.jotorren.microservices.content.controller.ContentController; 13 | 14 | import org.glassfish.jersey.logging.LoggingFeature; 15 | import org.glassfish.jersey.server.ResourceConfig; 16 | import org.springframework.beans.factory.annotation.Value; 17 | import org.springframework.context.annotation.Configuration; 18 | 19 | @Configuration 20 | public class WebServicesConfiguration extends ResourceConfig { 21 | 22 | /** 23 | * maximum number of entity bytes to be logged (and buffered) - if the entity is larger, 24 | * logging filter will print (and buffer in memory) only the specified number of bytes 25 | * and print "...more..." string at the end. Negative values are interpreted as zero. 26 | */ 27 | @Value("${spring.jersey.log.entity.size:2048}") 28 | private int maxlog; 29 | 30 | @Value("${swagger.title}") 31 | private String title; 32 | 33 | @Value("${swagger.description}") 34 | private String description; 35 | 36 | @Value("${swagger.version}") 37 | private String version; 38 | 39 | @Value("${swagger.contact}") 40 | private String contact; 41 | 42 | @Value("${swagger.schemes}") 43 | private String schemes; 44 | 45 | @Value("${swagger.basePath}") 46 | private String basePath; 47 | 48 | @Value("${swagger.resourcePackage}") 49 | private String resourcePackage; 50 | 51 | @Value("${swagger.prettyPrint}") 52 | private boolean prettyPrint; 53 | 54 | @Value("${swagger.scan}") 55 | private boolean scan; 56 | 57 | private void configureSwagger() { 58 | // Available at localhost:port/swagger.json 59 | this.register(ApiListingResource.class); 60 | this.register(SwaggerSerializers.class); 61 | 62 | BeanConfig config = new BeanConfig(); 63 | // config.setConfigId(title); 64 | config.setTitle(title); 65 | config.setDescription(description); 66 | config.setVersion(version); 67 | config.setContact(contact); 68 | config.setSchemes(schemes.split(",")); 69 | config.setBasePath(basePath); 70 | config.setResourcePackage(resourcePackage); 71 | config.setPrettyPrint(prettyPrint); 72 | config.setScan(scan); 73 | } 74 | 75 | public WebServicesConfiguration() { 76 | // Register endpoints, providers, ... 77 | this.register(ContentController.class); 78 | this.register(new LoggingFeature( 79 | Logger.getLogger(LoggingFeature.DEFAULT_LOGGER_NAME), 80 | Level.SEVERE, 81 | LoggingFeature.Verbosity.PAYLOAD_ANY, 82 | maxlog)); 83 | } 84 | 85 | @PostConstruct 86 | public void init() { 87 | // Register components where DI is needed 88 | this.configureSwagger(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /rahub-source-code-service/src/main/java/net/jotorren/microservices/content/controller/ContentController.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.content.controller; 2 | 3 | import java.net.URI; 4 | 5 | import javax.ws.rs.GET; 6 | import javax.ws.rs.POST; 7 | import javax.ws.rs.Path; 8 | import javax.ws.rs.PathParam; 9 | import javax.ws.rs.Produces; 10 | import javax.ws.rs.WebApplicationException; 11 | import javax.ws.rs.core.Context; 12 | import javax.ws.rs.core.MediaType; 13 | import javax.ws.rs.core.Response; 14 | import javax.ws.rs.core.UriInfo; 15 | 16 | import org.slf4j.Logger; 17 | import org.slf4j.LoggerFactory; 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.web.bind.annotation.RestController; 20 | 21 | import io.swagger.annotations.Api; 22 | import io.swagger.annotations.ApiOperation; 23 | import io.swagger.annotations.ApiParam; 24 | import io.swagger.annotations.ApiResponse; 25 | import io.swagger.annotations.ApiResponses; 26 | import io.swagger.annotations.ResponseHeader; 27 | import net.jotorren.microservices.content.domain.SourceCodeItem; 28 | import net.jotorren.microservices.content.service.ContentService; 29 | import net.jotorren.microservices.rs.ExceptionRestHandler; 30 | import net.jotorren.microservices.tx.CompositeTransactionParticipantController; 31 | import net.jotorren.microservices.tx.CompositeTransactionParticipantService; 32 | 33 | @RestController 34 | @Path("/") 35 | @Api(value = "Content services") 36 | public class ContentController extends CompositeTransactionParticipantController { 37 | 38 | private static final Logger LOG = LoggerFactory.getLogger(ContentController.class); 39 | 40 | @Autowired 41 | private ContentService service; 42 | 43 | @Override 44 | public CompositeTransactionParticipantService getCompositeTransactionParticipantService() { 45 | return service; 46 | } 47 | 48 | @GET 49 | @Path("{id}") 50 | @Produces("application/json") 51 | @ApiOperation( 52 | code = 200, 53 | value = "Find a source code item by its Id", 54 | notes = "Queries data previously persisted in the database", 55 | response = SourceCodeItem.class, 56 | produces = "application/json" 57 | ) 58 | public SourceCodeItem get( 59 | @ApiParam(value = "Id of the item to retrieve", required = true) @PathParam("id") String id 60 | ) { 61 | LOG.info("Trying to get content item [{}] outside any transaction", id); 62 | 63 | return service.getContent(id); 64 | } 65 | 66 | @POST 67 | @ApiOperation( 68 | code = 201, 69 | value = "Save a new source code item in the database", 70 | notes = "The newly created resource can be referenced by the URI returned in the the Location header field", 71 | response = String.class, 72 | responseHeaders = { 73 | @ResponseHeader(name = "Location", description = "The URI of the saved item", response = String.class) 74 | } 75 | ) 76 | @ApiResponses(value = { 77 | @ApiResponse(code=500, message="Error saving the given item", response = String.class) 78 | }) 79 | public Response save(@Context UriInfo uriInfo, 80 | @ApiParam(value = "Data of the item", required = true) SourceCodeItem content 81 | ) { 82 | try{ 83 | LOG.info("Trying to save content outside any transaction"); 84 | 85 | String id = service.addNewContent(content); 86 | LOG.info("New content item id set to [{}]", id); 87 | 88 | URI location = uriInfo.getAbsolutePathBuilder().path("{id}") 89 | .resolveTemplate("id", id).build(); 90 | LOG.info("New content item uri [{}]", location); 91 | 92 | return Response.created(location).build(); 93 | } catch (Exception e){ 94 | Response response = Response.status(Response.Status.INTERNAL_SERVER_ERROR) 95 | .entity(new ExceptionRestHandler().toString(e)).type(MediaType.TEXT_PLAIN).build(); 96 | throw new WebApplicationException(response); 97 | } 98 | } 99 | 100 | // Composite Transaction methods 101 | 102 | @GET 103 | @Path("{txid}/{id}") 104 | @Produces("application/json") 105 | @ApiOperation( 106 | code = 200, 107 | value = "Find a source code item by its Id", 108 | notes = "Queries the transaction uncommitted data in addition to the one previously persisted in the database", 109 | response = SourceCodeItem.class, 110 | produces = "application/json" 111 | ) 112 | public SourceCodeItem getTxAware( 113 | @ApiParam(value = "Id of a composite transaction", required = true) @PathParam("txid") String txid, 114 | @ApiParam(value = "Id of the item to retrieve", required = true) @PathParam("id") String id 115 | ) { 116 | LOG.info("Trying to get content item [{}] inside transaction [{}]", id, txid); 117 | 118 | return service.getContent(txid, id); 119 | } 120 | 121 | @POST 122 | @Path("{txid}") 123 | @ApiOperation( 124 | code = 201, 125 | value = "Save a new source code item enlisting the operation in a composite transaction", 126 | notes = "No data will be persisted in the database until the transaction is explicitly committed. " 127 | + "The newly created resource can be referenced by the URI returned in the the Location header field", 128 | response = String.class, 129 | responseHeaders = { 130 | @ResponseHeader(name = "Location", description = "The URI of the saved item", response = String.class) 131 | } 132 | ) 133 | @ApiResponses(value = { 134 | @ApiResponse(code=500, message="Error saving the given item", response = String.class) 135 | }) 136 | public Response saveTxAware(@Context UriInfo uriInfo, 137 | @ApiParam(value = "Id of the composite transaction where the operation must be enlisted", required = true) @PathParam("txid") String txid, 138 | @ApiParam(value = "Data of the item", required = true) SourceCodeItem content 139 | ) { 140 | try{ 141 | LOG.info("Trying to save content inside transaction [{}]", txid); 142 | 143 | String id = service.addNewContent(txid, content); 144 | LOG.info("New content item id set to [{}]", id); 145 | 146 | URI location = uriInfo.getAbsolutePathBuilder().path("{id}") 147 | .resolveTemplate("id", id).build(); 148 | LOG.info("New content item uri [{}]", location); 149 | 150 | return Response.created(location).build(); 151 | } catch (Exception e){ 152 | Response response = Response.status(Response.Status.INTERNAL_SERVER_ERROR) 153 | .entity(new ExceptionRestHandler().toString(e)).type(MediaType.TEXT_PLAIN).build(); 154 | throw new WebApplicationException(response); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /rahub-source-code-service/src/main/java/net/jotorren/microservices/content/dao/ContentDao.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.content.dao; 2 | 3 | import net.jotorren.microservices.content.domain.SourceCodeItem; 4 | 5 | import org.springframework.data.repository.CrudRepository; 6 | 7 | public interface ContentDao extends CrudRepository { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /rahub-source-code-service/src/main/java/net/jotorren/microservices/content/dao/ContentTransactionAwareDao.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.content.dao; 2 | 3 | import net.jotorren.microservices.content.domain.SourceCodeItem; 4 | import net.jotorren.microservices.tx.CompositeTransactionParticipantDao; 5 | 6 | import org.springframework.context.annotation.Scope; 7 | import org.springframework.stereotype.Repository; 8 | 9 | @Repository 10 | @Scope("prototype") 11 | public class ContentTransactionAwareDao extends CompositeTransactionParticipantDao{ 12 | 13 | public SourceCodeItem findOne(String pk){ 14 | return getEntityManager().find(SourceCodeItem.class, pk); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /rahub-source-code-service/src/main/java/net/jotorren/microservices/content/domain/SourceCodeItem.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.content.domain; 2 | 3 | import java.io.Serializable; 4 | 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.Id; 8 | 9 | @Entity 10 | public class SourceCodeItem implements Serializable { 11 | private static final long serialVersionUID = 7803210035898675537L; 12 | 13 | @Id 14 | @Column(nullable=false) 15 | private String itemId; 16 | 17 | @Column(nullable=false) 18 | private String fileName; 19 | 20 | @Column(nullable=false) 21 | private String fileLocation; 22 | 23 | @Column 24 | private String fileContent; 25 | 26 | @Column 27 | private String fileDescription; 28 | 29 | @Column(nullable=false) 30 | private String fileOwner; 31 | 32 | public String getFileName() { 33 | return fileName; 34 | } 35 | 36 | public void setFileName(String fileName) { 37 | this.fileName = fileName; 38 | } 39 | 40 | public String getFileLocation() { 41 | return fileLocation; 42 | } 43 | 44 | public void setFileLocation(String fileLocation) { 45 | this.fileLocation = fileLocation; 46 | } 47 | 48 | public String getFileContent() { 49 | return fileContent; 50 | } 51 | 52 | public void setFileContent(String fileContent) { 53 | this.fileContent = fileContent; 54 | } 55 | 56 | public String getFileDescription() { 57 | return fileDescription; 58 | } 59 | 60 | public void setFileDescription(String fileDescription) { 61 | this.fileDescription = fileDescription; 62 | } 63 | 64 | public String getFileOwner() { 65 | return fileOwner; 66 | } 67 | 68 | public void setFileOwner(String fileOwner) { 69 | this.fileOwner = fileOwner; 70 | } 71 | 72 | public String getItemId() { 73 | return itemId; 74 | } 75 | 76 | public void setItemId(String itemId) { 77 | this.itemId = itemId; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /rahub-source-code-service/src/main/java/net/jotorren/microservices/content/domain/SourceCodeItemSerializer.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.content.domain; 2 | 3 | import java.io.IOException; 4 | 5 | import net.jotorren.microservices.tx.AbstractEntityCommandJsonSerializer; 6 | import net.jotorren.microservices.tx.EntityCommand; 7 | 8 | import org.springframework.core.serializer.support.SerializationFailedException; 9 | import org.springframework.stereotype.Component; 10 | 11 | import com.fasterxml.jackson.core.JsonParseException; 12 | import com.fasterxml.jackson.databind.JsonMappingException; 13 | import com.fasterxml.jackson.databind.JsonNode; 14 | 15 | @Component 16 | public class SourceCodeItemSerializer extends AbstractEntityCommandJsonSerializer{ 17 | 18 | @Override 19 | public EntityCommand readFromString(String chars) 20 | throws SerializationFailedException { 21 | try { 22 | EntityCommand command = new EntityCommand(); 23 | 24 | JsonNode node = getJacksonMapper().readValue(chars, JsonNode.class); 25 | command.setAction(EntityCommand.Action.valueOf(node.get("action").asText())); 26 | command.setTransactionId(node.get("transactionId").asText()); 27 | command.setTimestamp(node.get("timestamp").asLong()); 28 | command.setEntity(getJacksonMapper().readValue(node.get("entity").toString(), SourceCodeItem.class)); 29 | 30 | return command; 31 | } catch (JsonParseException e) { 32 | throw new SerializationFailedException(e.getMessage()); 33 | } catch (JsonMappingException e) { 34 | throw new SerializationFailedException(e.getMessage()); 35 | } catch (IOException e) { 36 | throw new SerializationFailedException(e.getMessage()); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /rahub-source-code-service/src/main/java/net/jotorren/microservices/content/service/ContentService.java: -------------------------------------------------------------------------------- 1 | package net.jotorren.microservices.content.service; 2 | 3 | import java.util.List; 4 | import java.util.UUID; 5 | 6 | import net.jotorren.microservices.content.dao.ContentDao; 7 | import net.jotorren.microservices.content.dao.ContentTransactionAwareDao; 8 | import net.jotorren.microservices.content.domain.SourceCodeItem; 9 | import net.jotorren.microservices.context.ThreadLocalContext; 10 | import net.jotorren.microservices.tx.CompositeTransactionParticipantDao; 11 | import net.jotorren.microservices.tx.CompositeTransactionParticipantService; 12 | import net.jotorren.microservices.tx.EntityCommand; 13 | 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.context.ApplicationContext; 18 | import org.springframework.stereotype.Service; 19 | 20 | @Service 21 | public class ContentService extends CompositeTransactionParticipantService { 22 | 23 | private static final Logger LOG = LoggerFactory.getLogger(ContentService.class); 24 | 25 | @Autowired 26 | private ApplicationContext context; 27 | 28 | @Autowired 29 | private ContentDao dao; 30 | 31 | public String addNewContent(SourceCodeItem content) { 32 | String uuid = UUID.randomUUID().toString(); 33 | content.setItemId(uuid); 34 | 35 | SourceCodeItem saved = dao.save(content); 36 | return saved.getItemId(); 37 | } 38 | 39 | public SourceCodeItem getContent(String pk) { 40 | return dao.findOne(pk); 41 | } 42 | 43 | // Composite Transaction methods 44 | 45 | @Override 46 | public CompositeTransactionParticipantDao getCompositeTransactionDao() { 47 | return context.getBean(ContentTransactionAwareDao.class); 48 | } 49 | 50 | public String addNewContent(String txId, SourceCodeItem content) { 51 | ThreadLocalContext.put(CURRENT_TRANSACTION_KEY, txId); 52 | 53 | String uuid = UUID.randomUUID().toString(); 54 | content.setItemId(uuid); 55 | 56 | LOG.info("Creating transaction [{}]", txId); 57 | SourceCodeItem saved = getCompositeTransactionDao().saveOrUpdate(content); 58 | 59 | return saved.getItemId(); 60 | } 61 | 62 | public SourceCodeItem getContent(String txId, String pk) { 63 | ThreadLocalContext.remove(CURRENT_TRANSACTION_KEY); 64 | 65 | LOG.warn("Looking for transaction [{}]", txId); 66 | List> transactionOperations = getCompositeTransactionManager().fetch(txId); 67 | if (null == transactionOperations){ 68 | LOG.error("Transaction [{}] does not exist", txId); 69 | return null; 70 | } 71 | ContentTransactionAwareDao unsynchronizedDao = (ContentTransactionAwareDao)getCompositeTransactionDao(); 72 | unsynchronizedDao.apply(transactionOperations); 73 | 74 | return unsynchronizedDao.findOne(pk); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /rahub-source-code-service/src/main/resources/META-INF/orm.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /rahub-source-code-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port = 8091 2 | 3 | spring.datasource.url = jdbc:h2:file:~/rahub-content-db 4 | spring.datasource.username = sa 5 | spring.datasource.password = 6 | spring.datasource.driverClassName = org.h2.Driver 7 | 8 | #spring.jpa.generate-ddl: false 9 | #spring.jpa.hibernate.ddl-auto: none 10 | spring.jpa.show-sql: true 11 | spring.jpa.properties.hibernate.format_sql=true 12 | spring.jersey.log.entity.size = 4096 13 | 14 | # Spring MVC dispatcher servlet path. Needs to be different than Jersey's to enable/disable Actuator endpoints access (/info, /health, ...) 15 | server.servlet-path: / 16 | # Jersey dispatcher servlet 17 | spring.jersey.application-path: /content 18 | 19 | # http://localhost:8091/content/swagger.json 20 | # http://localhost:8091/index.html?url=/content/swagger.json 21 | swagger.title = rahub-source-code-service 22 | swagger.description = Spring Boot raHub Source Code Service 23 | swagger.version = 1.0.0 24 | swagger.contact = jotorren 25 | #swagger.schemes = http,https 26 | swagger.schemes = http 27 | swagger.basePath = /content 28 | swagger.resourcePackage = net.jotorren.microservices.content.controller 29 | swagger.prettyPrint = true 30 | swagger.scan = true 31 | 32 | zookeeper.servers = localhost:2181 33 | zookeeper.session.timeout = 10000 34 | zookeeper.connection.timeout = 8000 35 | 36 | kafka.bootstrap-servers = localhost:9092 37 | kafka.consumer.enable.auto.commit = false 38 | kafka.consumer.auto.commit.interval = 1000 39 | kafka.consumer.session.timeout = 15000 40 | kafka.consumer.auto.offset.reset = earliest 41 | kafka.consumer.poll.timeout = 1000 -------------------------------------------------------------------------------- /rahub-source-code-service/src/main/resources/static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jotorren/microservices-transactions-tcc/8e35e840376084987577dc8e73daa28b999b40dc/rahub-source-code-service/src/main/resources/static/favicon-16x16.png -------------------------------------------------------------------------------- /rahub-source-code-service/src/main/resources/static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jotorren/microservices-transactions-tcc/8e35e840376084987577dc8e73daa28b999b40dc/rahub-source-code-service/src/main/resources/static/favicon-32x32.png -------------------------------------------------------------------------------- /rahub-source-code-service/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Swagger UI 7 | 8 | 9 | 10 | 11 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |
69 | 70 | 71 | 72 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /rahub-source-code-service/src/main/resources/static/oauth2-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 54 | -------------------------------------------------------------------------------- /rahub-source-code-service/src/main/resources/static/swagger-ui-bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"swagger-ui-bundle.js","sources":["webpack:///swagger-ui-bundle.js"],"mappings":"AAAA;AAu/FA;AA6+FA;;;;;;;;;;;;;;;;;;;;;;;;;;AAoTA;;;;;;AAoIA;AAi7FA;AAmtCA;AAi0IA;AA2pJA;AA+uFA;AA2rGA;AAgiFA;AA0rFA;AAk9CA;AA2hDA;AA4rCA;AAg6EA;;;;;AA4gCA;AA02JA;;;;;;;;;;;;;;AAuyEA;AA4mIA;AAquJA;AAwsHA;AA2mGA;AAiiEA;AAq4DA;AA+2DA;AAmfA;;;;;;AAmrFA;AA62FA;;;;;AAy3CA;AA2qFA;AAw2CA;AAqkCA;AA++CA;AAsiFA;AAk2FA;;;;;;;;;AAm/CA;AA2zIA;AAk4DA;AAolDA","sourceRoot":""} -------------------------------------------------------------------------------- /rahub-source-code-service/src/main/resources/static/swagger-ui-standalone-preset.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"swagger-ui-standalone-preset.js","sources":["webpack:///swagger-ui-standalone-preset.js"],"mappings":"AAAA;;;;;AA8QA;AAitGA;AA+vFA;;;;;;AA6eA;AAkvFA;AAu+CA;AAo+CA;AAgrCA;AAgyEA","sourceRoot":""} -------------------------------------------------------------------------------- /rahub-source-code-service/src/main/resources/static/swagger-ui.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8";.swagger-ui html{box-sizing:border-box}.swagger-ui *,.swagger-ui :after,.swagger-ui :before{box-sizing:inherit}.swagger-ui body{margin:0;background:#fafafa}.swagger-ui .wrapper{width:100%;max-width:1460px;margin:0 auto;padding:0 20px}.swagger-ui .opblock-tag-section{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.swagger-ui .opblock-tag{display:-webkit-box;display:-ms-flexbox;display:flex;padding:10px 20px 10px 10px;cursor:pointer;transition:all .2s;border-bottom:1px solid rgba(59,65,81,.3);-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .opblock-tag:hover{background:rgba(0,0,0,.02)}.swagger-ui .opblock-tag{font-size:24px;margin:0 0 5px;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .opblock-tag.no-desc span{-webkit-box-flex:1;-ms-flex:1;flex:1}.swagger-ui .opblock-tag svg{transition:all .4s}.swagger-ui .opblock-tag small{font-size:14px;font-weight:400;padding:0 10px;-webkit-box-flex:1;-ms-flex:1;flex:1;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .parаmeter__type{font-size:12px;padding:5px 0;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .view-line-link{position:relative;top:3px;width:20px;margin:0 5px;cursor:pointer;transition:all .5s}.swagger-ui .opblock{margin:0 0 15px;border:1px solid #000;border-radius:4px;box-shadow:0 0 3px rgba(0,0,0,.19)}.swagger-ui .opblock.is-open .opblock-summary{border-bottom:1px solid #000}.swagger-ui .opblock .opblock-section-header{padding:8px 20px;background:hsla(0,0%,100%,.8);box-shadow:0 1px 2px rgba(0,0,0,.1)}.swagger-ui .opblock .opblock-section-header,.swagger-ui .opblock .opblock-section-header label{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .opblock .opblock-section-header label{font-size:12px;font-weight:700;margin:0;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .opblock .opblock-section-header label span{padding:0 10px 0 0}.swagger-ui .opblock .opblock-section-header h4{font-size:14px;margin:0;-webkit-box-flex:1;-ms-flex:1;flex:1;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .opblock .opblock-summary-method{font-size:14px;font-weight:700;min-width:80px;padding:6px 15px;text-align:center;border-radius:3px;background:#000;text-shadow:0 1px 0 rgba(0,0,0,.1);font-family:Titillium Web,sans-serif;color:#fff}.swagger-ui .opblock .opblock-summary-path,.swagger-ui .opblock .opblock-summary-path__deprecated{font-size:16px;display:-webkit-box;display:-ms-flexbox;display:flex;padding:0 10px;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .opblock .opblock-summary-path .view-line-link,.swagger-ui .opblock .opblock-summary-path__deprecated .view-line-link{position:relative;top:2px;width:0;margin:0;cursor:pointer;transition:all .5s}.swagger-ui .opblock .opblock-summary-path:hover .view-line-link,.swagger-ui .opblock .opblock-summary-path__deprecated:hover .view-line-link{width:18px;margin:0 5px}.swagger-ui .opblock .opblock-summary-path__deprecated{text-decoration:line-through}.swagger-ui .opblock .opblock-summary-description{font-size:13px;-webkit-box-flex:1;-ms-flex:1;flex:1;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .opblock .opblock-summary{display:-webkit-box;display:-ms-flexbox;display:flex;padding:5px;cursor:pointer;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .opblock.opblock-post{border-color:#49cc90;background:rgba(73,204,144,.1)}.swagger-ui .opblock.opblock-post .opblock-summary-method{background:#49cc90}.swagger-ui .opblock.opblock-post .opblock-summary{border-color:#49cc90}.swagger-ui .opblock.opblock-put{border-color:#fca130;background:rgba(252,161,48,.1)}.swagger-ui .opblock.opblock-put .opblock-summary-method{background:#fca130}.swagger-ui .opblock.opblock-put .opblock-summary{border-color:#fca130}.swagger-ui .opblock.opblock-delete{border-color:#f93e3e;background:rgba(249,62,62,.1)}.swagger-ui .opblock.opblock-delete .opblock-summary-method{background:#f93e3e}.swagger-ui .opblock.opblock-delete .opblock-summary{border-color:#f93e3e}.swagger-ui .opblock.opblock-get{border-color:#61affe;background:rgba(97,175,254,.1)}.swagger-ui .opblock.opblock-get .opblock-summary-method{background:#61affe}.swagger-ui .opblock.opblock-get .opblock-summary{border-color:#61affe}.swagger-ui .opblock.opblock-patch{border-color:#50e3c2;background:rgba(80,227,194,.1)}.swagger-ui .opblock.opblock-patch .opblock-summary-method{background:#50e3c2}.swagger-ui .opblock.opblock-patch .opblock-summary{border-color:#50e3c2}.swagger-ui .opblock.opblock-head{border-color:#9012fe;background:rgba(144,18,254,.1)}.swagger-ui .opblock.opblock-head .opblock-summary-method{background:#9012fe}.swagger-ui .opblock.opblock-head .opblock-summary{border-color:#9012fe}.swagger-ui .opblock.opblock-options{border-color:#0d5aa7;background:rgba(13,90,167,.1)}.swagger-ui .opblock.opblock-options .opblock-summary-method{background:#0d5aa7}.swagger-ui .opblock.opblock-options .opblock-summary{border-color:#0d5aa7}.swagger-ui .opblock.opblock-deprecated{opacity:.6;border-color:#ebebeb;background:hsla(0,0%,92%,.1)}.swagger-ui .opblock.opblock-deprecated .opblock-summary-method{background:#ebebeb}.swagger-ui .opblock.opblock-deprecated .opblock-summary{border-color:#ebebeb}.swagger-ui .opblock .opblock-schemes{padding:8px 20px}.swagger-ui .opblock .opblock-schemes .schemes-title{padding:0 10px 0 0}.swagger-ui .tab{display:-webkit-box;display:-ms-flexbox;display:flex;margin:20px 0 10px;padding:0;list-style:none}.swagger-ui .tab li{font-size:12px;min-width:100px;min-width:90px;padding:0;cursor:pointer;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .tab li:first-of-type{position:relative;padding-left:0}.swagger-ui .tab li:first-of-type:after{position:absolute;top:0;right:6px;width:1px;height:100%;content:"";background:rgba(0,0,0,.2)}.swagger-ui .tab li.active{font-weight:700}.swagger-ui .opblock-description-wrapper,.swagger-ui .opblock-title_normal{padding:15px 20px}.swagger-ui .opblock-description-wrapper,.swagger-ui .opblock-description-wrapper h4,.swagger-ui .opblock-title_normal,.swagger-ui .opblock-title_normal h4{font-size:12px;margin:0 0 5px;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .opblock-description-wrapper p,.swagger-ui .opblock-title_normal p{font-size:14px;margin:0;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .execute-wrapper{padding:20px;text-align:right}.swagger-ui .execute-wrapper .btn{width:100%;padding:8px 40px}.swagger-ui .body-param-options{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.swagger-ui .body-param-options .body-param-edit{padding:10px 0}.swagger-ui .body-param-options label{padding:8px 0}.swagger-ui .body-param-options label select{margin:3px 0 0}.swagger-ui .responses-inner{padding:20px}.swagger-ui .responses-inner h4,.swagger-ui .responses-inner h5{font-size:12px;margin:10px 0 5px;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .response-col_status{font-size:14px;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .response-col_status .response-undocumented{font-size:11px;font-family:Source Code Pro,monospace;font-weight:600;color:#999}.swagger-ui .response-col_description__inner span{font-size:12px;font-style:italic;display:block;margin:10px 0;padding:10px;border-radius:4px;background:#41444e;font-family:Source Code Pro,monospace;font-weight:600;color:#fff}.swagger-ui .response-col_description__inner span p{margin:0}.swagger-ui .opblock-body pre{font-size:12px;margin:0;padding:10px;white-space:pre-wrap;border-radius:4px;background:#41444e;font-family:Source Code Pro,monospace;font-weight:600;color:#fff}.swagger-ui .opblock-body pre span{color:#fff!important}.swagger-ui .opblock-body pre .headerline{display:block}.swagger-ui .scheme-container{margin:0 0 20px;padding:30px 0;background:#fff;box-shadow:0 1px 2px 0 rgba(0,0,0,.15)}.swagger-ui .scheme-container .schemes{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .scheme-container .schemes>label{font-size:12px;font-weight:700;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:-20px 15px 0 0;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .scheme-container .schemes>label select{min-width:130px;text-transform:uppercase}.swagger-ui .loading-container{padding:40px 0 60px}.swagger-ui .loading-container .loading{position:relative}.swagger-ui .loading-container .loading:after{font-size:10px;font-weight:700;position:absolute;top:50%;left:50%;content:"loading";-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);text-transform:uppercase;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .loading-container .loading:before{position:absolute;top:50%;left:50%;display:block;width:60px;height:60px;margin:-30px;content:"";-webkit-animation:rotation 1s infinite linear,opacity .5s;animation:rotation 1s infinite linear,opacity .5s;opacity:1;border:2px solid rgba(85,85,85,.1);border-top-color:rgba(0,0,0,.6);border-radius:100%;-webkit-backface-visibility:hidden;backface-visibility:hidden}@-webkit-keyframes rotation{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes rotation{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@-webkit-keyframes blinker{50%{opacity:0}}@keyframes blinker{50%{opacity:0}}.swagger-ui .btn{font-size:14px;font-weight:700;padding:5px 23px;transition:all .3s;border:2px solid #888;border-radius:4px;background:transparent;box-shadow:0 1px 2px rgba(0,0,0,.1);font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .btn[disabled]{cursor:not-allowed;opacity:.3}.swagger-ui .btn:hover{box-shadow:0 0 5px rgba(0,0,0,.3)}.swagger-ui .btn.cancel{border-color:#ff6060;font-family:Titillium Web,sans-serif;color:#ff6060}.swagger-ui .btn.authorize{line-height:1;display:inline;color:#49cc90;border-color:#49cc90}.swagger-ui .btn.authorize span{float:left;padding:4px 20px 0 0}.swagger-ui .btn.authorize svg{fill:#49cc90}.swagger-ui .btn.execute{-webkit-animation:pulse 2s infinite;animation:pulse 2s infinite;color:#fff;border-color:#4990e2}@-webkit-keyframes pulse{0%{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,.8)}70%{box-shadow:0 0 0 5px rgba(73,144,226,0)}to{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,0)}}@keyframes pulse{0%{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,.8)}70%{box-shadow:0 0 0 5px rgba(73,144,226,0)}to{color:#fff;background:#4990e2;box-shadow:0 0 0 0 rgba(73,144,226,0)}}.swagger-ui .btn-group{display:-webkit-box;display:-ms-flexbox;display:flex;padding:30px}.swagger-ui .btn-group .btn{-webkit-box-flex:1;-ms-flex:1;flex:1}.swagger-ui .btn-group .btn:first-child{border-radius:4px 0 0 4px}.swagger-ui .btn-group .btn:last-child{border-radius:0 4px 4px 0}.swagger-ui .authorization__btn{padding:0 10px;border:none;background:none}.swagger-ui .authorization__btn.locked{opacity:1}.swagger-ui .authorization__btn.unlocked{opacity:.4}.swagger-ui .expand-methods,.swagger-ui .expand-operation{border:none;background:none}.swagger-ui .expand-methods svg,.swagger-ui .expand-operation svg{width:20px;height:20px}.swagger-ui .expand-methods{padding:0 10px}.swagger-ui .expand-methods:hover svg{fill:#444}.swagger-ui .expand-methods svg{transition:all .3s;fill:#777}.swagger-ui button{cursor:pointer;outline:none}.swagger-ui select{font-size:14px;font-weight:700;padding:5px 40px 5px 10px;border:2px solid #41444e;border-radius:4px;background:#f7f7f7 url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyMCAyMCI+ICAgIDxwYXRoIGQ9Ik0xMy40MTggNy44NTljLjI3MS0uMjY4LjcwOS0uMjY4Ljk3OCAwIC4yNy4yNjguMjcyLjcwMSAwIC45NjlsLTMuOTA4IDMuODNjLS4yNy4yNjgtLjcwNy4yNjgtLjk3OSAwbC0zLjkwOC0zLjgzYy0uMjctLjI2Ny0uMjctLjcwMSAwLS45NjkuMjcxLS4yNjguNzA5LS4yNjguOTc4IDBMMTAgMTFsMy40MTgtMy4xNDF6Ii8+PC9zdmc+) right 10px center no-repeat;background-size:20px;box-shadow:0 1px 2px 0 rgba(0,0,0,.25);font-family:Titillium Web,sans-serif;color:#3b4151;-webkit-appearance:none;-moz-appearance:none;appearance:none}.swagger-ui select[multiple]{margin:5px 0;padding:5px;background:#f7f7f7}.swagger-ui .opblock-body select{min-width:230px}.swagger-ui label{font-size:12px;font-weight:700;margin:0 0 5px;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui input[type=email],.swagger-ui input[type=password],.swagger-ui input[type=search],.swagger-ui input[type=text]{min-width:100px;margin:5px 0;padding:8px 10px;border:1px solid #d9d9d9;border-radius:4px;background:#fff}.swagger-ui input[type=email].invalid,.swagger-ui input[type=password].invalid,.swagger-ui input[type=search].invalid,.swagger-ui input[type=text].invalid{-webkit-animation:shake .4s 1;animation:shake .4s 1;border-color:#f93e3e;background:#feebeb}@-webkit-keyframes shake{10%,90%{-webkit-transform:translate3d(-1px,0,0);transform:translate3d(-1px,0,0)}20%,80%{-webkit-transform:translate3d(2px,0,0);transform:translate3d(2px,0,0)}30%,50%,70%{-webkit-transform:translate3d(-4px,0,0);transform:translate3d(-4px,0,0)}40%,60%{-webkit-transform:translate3d(4px,0,0);transform:translate3d(4px,0,0)}}@keyframes shake{10%,90%{-webkit-transform:translate3d(-1px,0,0);transform:translate3d(-1px,0,0)}20%,80%{-webkit-transform:translate3d(2px,0,0);transform:translate3d(2px,0,0)}30%,50%,70%{-webkit-transform:translate3d(-4px,0,0);transform:translate3d(-4px,0,0)}40%,60%{-webkit-transform:translate3d(4px,0,0);transform:translate3d(4px,0,0)}}.swagger-ui textarea{font-size:12px;width:100%;min-height:280px;padding:10px;border:none;border-radius:4px;outline:none;background:hsla(0,0%,100%,.8);font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui textarea:focus{border:2px solid #61affe}.swagger-ui textarea.curl{font-size:12px;min-height:100px;margin:0;padding:10px;resize:none;border-radius:4px;background:#41444e;font-family:Source Code Pro,monospace;font-weight:600;color:#fff}.swagger-ui .checkbox{padding:5px 0 10px;transition:opacity .5s;color:#333}.swagger-ui .checkbox label{display:-webkit-box;display:-ms-flexbox;display:flex}.swagger-ui .checkbox p{font-weight:400!important;font-style:italic;margin:0!important;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .checkbox input[type=checkbox]{display:none}.swagger-ui .checkbox input[type=checkbox]+label>.item{position:relative;top:3px;display:inline-block;width:16px;height:16px;margin:0 8px 0 0;padding:5px;cursor:pointer;border-radius:1px;background:#e8e8e8;box-shadow:0 0 0 2px #e8e8e8;-webkit-box-flex:0;-ms-flex:none;flex:none}.swagger-ui .checkbox input[type=checkbox]+label>.item:active{-webkit-transform:scale(.9);transform:scale(.9)}.swagger-ui .checkbox input[type=checkbox]:checked+label>.item{background:#e8e8e8 url("data:image/svg+xml;charset=utf-8,%3Csvg width='10' height='8' viewBox='3 7 10 8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%2341474E' fill-rule='evenodd' d='M6.333 15L3 11.667l1.333-1.334 2 2L11.667 7 13 8.333z'/%3E%3C/svg%3E") 50% no-repeat}.swagger-ui .dialog-ux{position:fixed;z-index:9999;top:0;right:0;bottom:0;left:0}.swagger-ui .dialog-ux .backdrop-ux{position:fixed;top:0;right:0;bottom:0;left:0;background:rgba(0,0,0,.8)}.swagger-ui .dialog-ux .modal-ux{position:absolute;z-index:9999;top:50%;left:50%;width:100%;min-width:300px;max-width:650px;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);border:1px solid #ebebeb;border-radius:4px;background:#fff;box-shadow:0 10px 30px 0 rgba(0,0,0,.2)}.swagger-ui .dialog-ux .modal-ux-content{overflow-y:auto;max-height:540px;padding:20px}.swagger-ui .dialog-ux .modal-ux-content p{font-size:12px;margin:0 0 5px;color:#41444e;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .dialog-ux .modal-ux-content h4{font-size:18px;font-weight:600;margin:15px 0 0;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .dialog-ux .modal-ux-header{display:-webkit-box;display:-ms-flexbox;display:flex;padding:12px 0;border-bottom:1px solid #ebebeb;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .dialog-ux .modal-ux-header .close-modal{padding:0 10px;border:none;background:none;-webkit-appearance:none;-moz-appearance:none;appearance:none}.swagger-ui .dialog-ux .modal-ux-header h3{font-size:20px;font-weight:600;margin:0;padding:0 20px;-webkit-box-flex:1;-ms-flex:1;flex:1;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .model{font-size:12px;font-weight:300;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .model-toggle{font-size:10px;position:relative;top:6px;display:inline-block;margin:auto .3em;cursor:pointer;transition:-webkit-transform .15s ease-in;transition:transform .15s ease-in;transition:transform .15s ease-in,-webkit-transform .15s ease-in;-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-transform-origin:50% 50%;transform-origin:50% 50%}.swagger-ui .model-toggle.collapsed{-webkit-transform:rotate(0deg);transform:rotate(0deg)}.swagger-ui .model-toggle:after{display:block;width:20px;height:20px;content:"";background:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z'/%3E%3C/svg%3E") 50% no-repeat;background-size:100%}.swagger-ui .model-jump-to-path{position:relative;cursor:pointer}.swagger-ui .model-jump-to-path .view-line-link{position:absolute;top:-.4em;cursor:pointer}.swagger-ui .model-title{position:relative}.swagger-ui .model-title:hover .model-hint{visibility:visible}.swagger-ui .model-hint{position:absolute;top:-1.8em;visibility:hidden;padding:.1em .5em;white-space:nowrap;color:#ebebeb;border-radius:4px;background:rgba(0,0,0,.7)}.swagger-ui section.models{margin:30px 0;border:1px solid rgba(59,65,81,.3);border-radius:4px}.swagger-ui section.models.is-open{padding:0 0 20px}.swagger-ui section.models.is-open h4{margin:0 0 5px;border-bottom:1px solid rgba(59,65,81,.3)}.swagger-ui section.models.is-open h4 svg{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.swagger-ui section.models h4{font-size:16px;display:-webkit-box;display:-ms-flexbox;display:flex;margin:0;padding:10px 20px 10px 10px;cursor:pointer;transition:all .2s;font-family:Titillium Web,sans-serif;color:#777;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui section.models h4 svg{transition:all .4s}.swagger-ui section.models h4 span{-webkit-box-flex:1;-ms-flex:1;flex:1}.swagger-ui section.models h4:hover{background:rgba(0,0,0,.02)}.swagger-ui section.models h5{font-size:16px;margin:0 0 10px;font-family:Titillium Web,sans-serif;color:#777}.swagger-ui section.models .model-jump-to-path{position:relative;top:5px}.swagger-ui section.models .model-container{margin:0 20px 15px;transition:all .5s;border-radius:4px;background:rgba(0,0,0,.05)}.swagger-ui section.models .model-container:hover{background:rgba(0,0,0,.07)}.swagger-ui section.models .model-container:first-of-type{margin:20px}.swagger-ui section.models .model-container:last-of-type{margin:0 20px}.swagger-ui section.models .model-box{background:none}.swagger-ui .model-box{padding:10px;border-radius:4px;background:rgba(0,0,0,.1)}.swagger-ui .model-box .model-jump-to-path{position:relative;top:4px}.swagger-ui .model-title{font-size:16px;font-family:Titillium Web,sans-serif;color:#555}.swagger-ui span>span.model,.swagger-ui span>span.model .brace-close{padding:0 0 0 10px}.swagger-ui .prop-type{color:#55a}.swagger-ui .prop-enum{display:block}.swagger-ui .prop-format{color:#999}.swagger-ui table{width:100%;padding:0 10px;border-collapse:collapse}.swagger-ui table.model tbody tr td{padding:0;vertical-align:top}.swagger-ui table.model tbody tr td:first-of-type{width:100px;padding:0}.swagger-ui table.headers td{font-size:12px;font-weight:300;vertical-align:middle;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui table tbody tr td{padding:10px 0 0;vertical-align:top}.swagger-ui table tbody tr td:first-of-type{width:20%;padding:10px 0}.swagger-ui table thead tr td,.swagger-ui table thead tr th{font-size:12px;font-weight:700;padding:12px 0;text-align:left;border-bottom:1px solid rgba(59,65,81,.2);font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .parameters-col_description p{font-size:14px;margin:0;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .parameters-col_description input[type=text]{width:100%;max-width:340px}.swagger-ui .parameter__name{font-size:16px;font-weight:400;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .parameter__name.required{font-weight:700}.swagger-ui .parameter__name.required:after{font-size:10px;position:relative;top:-6px;padding:5px;content:"required";color:rgba(255,0,0,.6)}.swagger-ui .parameter__in{font-size:12px;font-style:italic;font-family:Source Code Pro,monospace;font-weight:600;color:#888}.swagger-ui .table-container{padding:20px}.swagger-ui .topbar{padding:8px 30px;background-color:#89bf04}.swagger-ui .topbar .topbar-wrapper{-ms-flex-align:center}.swagger-ui .topbar .topbar-wrapper,.swagger-ui .topbar a{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;align-items:center}.swagger-ui .topbar a{font-size:1.5em;font-weight:700;max-width:300px;text-decoration:none;-webkit-box-flex:1;-ms-flex:1;flex:1;-ms-flex-align:center;font-family:Titillium Web,sans-serif;color:#fff}.swagger-ui .topbar a span{margin:0;padding:0 10px}.swagger-ui .topbar .download-url-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:3;-ms-flex:3;flex:3}.swagger-ui .topbar .download-url-wrapper input[type=text]{width:100%;min-width:350px;margin:0;border:2px solid #547f00;border-radius:4px 0 0 4px;outline:none}.swagger-ui .topbar .download-url-wrapper .download-url-button{font-size:16px;font-weight:700;padding:4px 40px;border:none;border-radius:0 4px 4px 0;background:#547f00;font-family:Titillium Web,sans-serif;color:#fff}.swagger-ui .info{margin:50px 0}.swagger-ui .info hgroup.main{margin:0 0 20px}.swagger-ui .info hgroup.main a{font-size:12px}.swagger-ui .info p{font-size:14px;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .info code{padding:3px 5px;border-radius:4px;background:rgba(0,0,0,.05);font-family:Source Code Pro,monospace;font-weight:600;color:#9012fe}.swagger-ui .info a{font-size:14px;transition:all .4s;font-family:Open Sans,sans-serif;color:#4990e2}.swagger-ui .info a:hover{color:#1f69c0}.swagger-ui .info>div{margin:0 0 5px}.swagger-ui .info .base-url{font-size:12px;font-weight:300!important;margin:0;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .info .title{font-size:36px;margin:0;font-family:Open Sans,sans-serif;color:#3b4151}.swagger-ui .info .title small{font-size:10px;position:relative;top:-5px;display:inline-block;margin:0 0 0 5px;padding:2px 4px;vertical-align:super;border-radius:57px;background:#7d8492}.swagger-ui .info .title small pre{margin:0;font-family:Titillium Web,sans-serif;color:#fff}.swagger-ui .auth-btn-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;padding:10px 0;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.swagger-ui .auth-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.swagger-ui .auth-wrapper .authorize{padding-right:20px}.swagger-ui .auth-container{margin:0 0 10px;padding:10px 20px;border-bottom:1px solid #ebebeb}.swagger-ui .auth-container:last-of-type{margin:0;padding:10px 20px;border:0}.swagger-ui .auth-container h4{margin:5px 0 15px!important}.swagger-ui .auth-container .wrapper{margin:0;padding:0}.swagger-ui .auth-container input[type=password],.swagger-ui .auth-container input[type=text]{min-width:230px}.swagger-ui .auth-container .errors{font-size:12px;padding:10px;border-radius:4px;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .scopes h2{font-size:14px;font-family:Titillium Web,sans-serif;color:#3b4151}.swagger-ui .scope-def{padding:0 0 20px}.swagger-ui .errors-wrapper{margin:20px;padding:10px 20px;-webkit-animation:scaleUp .5s;animation:scaleUp .5s;border:2px solid #f93e3e;border-radius:4px;background:rgba(249,62,62,.1)}.swagger-ui .errors-wrapper .error-wrapper{margin:0 0 10px}.swagger-ui .errors-wrapper .errors h4{font-size:14px;margin:0;font-family:Source Code Pro,monospace;font-weight:600;color:#3b4151}.swagger-ui .errors-wrapper .errors small{color:#666}.swagger-ui .errors-wrapper hgroup{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.swagger-ui .errors-wrapper hgroup h4{font-size:20px;margin:0;-webkit-box-flex:1;-ms-flex:1;flex:1;font-family:Titillium Web,sans-serif;color:#3b4151}@-webkit-keyframes scaleUp{0%{-webkit-transform:scale(.8);transform:scale(.8);opacity:0}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes scaleUp{0%{-webkit-transform:scale(.8);transform:scale(.8);opacity:0}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.swagger-ui .Resizer.vertical.disabled{display:none} 2 | /*# sourceMappingURL=swagger-ui.css.map*/ -------------------------------------------------------------------------------- /rahub-source-code-service/src/main/resources/static/swagger-ui.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"swagger-ui.css","sources":[],"mappings":"","sourceRoot":""} -------------------------------------------------------------------------------- /rahub-source-code-service/src/main/resources/static/swagger-ui.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"swagger-ui.js","sources":["webpack:///swagger-ui.js"],"mappings":"AAAA;;;;;;AAyxCA;AAoyHA;AAuxHA;AAy4FA;AA2sCA;AAmgCA;AA0iCA;AA64BA","sourceRoot":""} --------------------------------------------------------------------------------