├── .gitignore ├── LICENSE ├── README.md ├── spring-best-practice-hibernate-search-multi-tenancy ├── pom.xml └── src │ ├── main │ ├── java │ │ └── practice │ │ │ ├── Application.java │ │ │ ├── configure │ │ │ ├── AdditionalProperties.java │ │ │ ├── DataSourceConfiguration.java │ │ │ ├── DataSourceInitializer.java │ │ │ ├── MultiTenancyJpaConfiguration.java │ │ │ └── WebMvcConfiguration.java │ │ │ ├── entity │ │ │ └── Item.java │ │ │ ├── mvc │ │ │ ├── CurrentTenantIdentifierChangeInterceptor.java │ │ │ ├── IndexController.java │ │ │ └── ItemController.java │ │ │ ├── repository │ │ │ ├── ItemRepository.java │ │ │ ├── ItemRepositoryCustom.java │ │ │ └── ItemRepositoryImpl.java │ │ │ └── support │ │ │ ├── CurrentTenantIdentifierResolverImpl.java │ │ │ └── DataSourceBasedMultiTenantConnectionProviderImpl.java │ └── resources │ │ ├── application.properties │ │ ├── data1.sql │ │ ├── data2.sql │ │ ├── schema.sql │ │ └── templates │ │ ├── index.html │ │ └── items.html │ └── test │ └── java │ └── practice │ └── SpringBestPracticeHibernateSearchMultiTenancyApplicationTests.java ├── spring-best-practice-manual-flashmap ├── pom.xml └── src │ └── main │ ├── java │ └── practice │ │ ├── Application.java │ │ └── SampleController.java │ └── resources │ ├── application.properties │ └── templates │ ├── index.html │ └── result.html ├── spring-best-practice-post-redirect-get ├── pom.xml └── src │ └── main │ ├── java │ └── practice │ │ └── post_redirect_get │ │ ├── Application.java │ │ ├── BasicExampleController.java │ │ ├── BasicExampleForm.java │ │ ├── SessionExampleController.java │ │ └── SessionExampleForm.java │ └── resources │ └── templates │ ├── basic.html │ ├── session-step1.html │ └── session-step2.html ├── spring-best-practice-riot-ssr ├── README.md ├── pom.xml └── src │ ├── main │ ├── java │ │ └── practice │ │ │ ├── Application.java │ │ │ ├── IndexController.java │ │ │ ├── Todo.java │ │ │ ├── TodoRepository.java │ │ │ └── TodoRestController.java │ └── resources │ │ ├── application.properties │ │ ├── db │ │ └── migration │ │ │ └── V1_0_0__init.sql │ │ ├── static │ │ ├── css │ │ │ └── todo.css │ │ ├── js │ │ │ ├── jvm-npm.js │ │ │ ├── nashorn-riot.js │ │ │ ├── riot-render.js │ │ │ ├── riot.js │ │ │ ├── simple-dom.js │ │ │ ├── simple-html-tokenizer.js │ │ │ └── todo.js │ │ └── tag │ │ │ └── todo.tag │ │ └── templates │ │ └── index.html │ └── test │ └── java │ └── practice │ └── IndexControllerTest.java └── spring-best-practice-security-multi-tenancy ├── pom.xml └── src ├── main ├── java │ └── practice │ │ ├── Application.java │ │ ├── AuthorizedUserDetails.java │ │ ├── AuthorizedUserDetailsService.java │ │ ├── HelloController.java │ │ ├── LoginController.java │ │ ├── MultiTenantRememberMeServices.java │ │ ├── MultiTenantRememberMeToken.java │ │ ├── MultiTenantTokenRepository.java │ │ ├── SecurityConfig.java │ │ ├── User.java │ │ └── UserRepository.java └── resources │ ├── application.properties │ ├── db │ └── migration │ │ └── V0.0.1__init.sql │ └── templates │ ├── hello.html │ └── login.html └── test └── java └── practice └── SpringBestPracticeSecurityMultiTenancyApplicationTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | target/ 4 | db/ -------------------------------------------------------------------------------- /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 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # spring-best-practices 2 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | jp.co.tagbangers.spring-best-practices 7 | spring-best-practice-hibernate-search-multi-tenancy 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | spring-best-practice-hibernate-search-multi-tenancy 12 | Spring Best Practices Hibernate Search Multi Tenancy 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 1.2.5.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | 1.8 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-web 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-thymeleaf 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-data-jpa 39 | 40 | 41 | com.h2database 42 | h2 43 | 44 | 45 | 46 | org.springframework.boot 47 | spring-boot-configuration-processor 48 | true 49 | 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-starter-test 54 | test 55 | 56 | 57 | 58 | org.springframework 59 | springloaded 60 | 1.2.0.RELEASE 61 | 62 | 63 | 64 | org.hibernate 65 | hibernate-search-orm 66 | 5.3.0.Final 67 | 68 | 69 | 70 | 71 | 72 | 73 | org.springframework.boot 74 | spring-boot-maven-plugin 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/main/java/practice/Application.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 6 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 7 | import practice.configure.AdditionalProperties; 8 | 9 | @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) 10 | @EnableConfigurationProperties(AdditionalProperties.class) 11 | public class Application { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(Application.class, args); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/main/java/practice/configure/AdditionalProperties.java: -------------------------------------------------------------------------------- 1 | package practice.configure; 2 | 3 | import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.boot.context.properties.NestedConfigurationProperty; 6 | 7 | @ConfigurationProperties("spring.additional") 8 | public class AdditionalProperties { 9 | 10 | @NestedConfigurationProperty 11 | private DataSourceProperties datasource1; 12 | 13 | @NestedConfigurationProperty 14 | private DataSourceProperties datasource2; 15 | 16 | public DataSourceProperties getDatasource1() { 17 | return datasource1; 18 | } 19 | 20 | public void setDatasource1(DataSourceProperties datasource1) { 21 | this.datasource1 = datasource1; 22 | } 23 | 24 | public DataSourceProperties getDatasource2() { 25 | return datasource2; 26 | } 27 | 28 | public void setDatasource2(DataSourceProperties datasource2) { 29 | this.datasource2 = datasource2; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/main/java/practice/configure/DataSourceConfiguration.java: -------------------------------------------------------------------------------- 1 | package practice.configure; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder; 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | import javax.sql.DataSource; 10 | 11 | @Configuration 12 | public class DataSourceConfiguration { 13 | 14 | @Autowired 15 | private AdditionalProperties additionalProperties; 16 | 17 | @Bean(name = {"dataSource", "dataSource1"}) 18 | @ConfigurationProperties(prefix = "spring.additional.datasource1") 19 | public DataSource dataSource1() { 20 | DataSourceBuilder factory = DataSourceBuilder 21 | .create(this.additionalProperties.getDatasource1().getClassLoader()) 22 | .driverClassName(this.additionalProperties.getDatasource1().getDriverClassName()) 23 | .url(this.additionalProperties.getDatasource1().getUrl()); 24 | return factory.build(); 25 | } 26 | 27 | @Bean(name = "dataSource2") 28 | @ConfigurationProperties(prefix = "spring.additional.datasource2") 29 | public DataSource dataSource2() { 30 | DataSourceBuilder factory = DataSourceBuilder 31 | .create(this.additionalProperties.getDatasource2().getClassLoader()) 32 | .driverClassName(this.additionalProperties.getDatasource2().getDriverClassName()) 33 | .url(this.additionalProperties.getDatasource2().getUrl()); 34 | return factory.build(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/main/java/practice/configure/DataSourceInitializer.java: -------------------------------------------------------------------------------- 1 | package practice.configure; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.core.io.ClassPathResource; 5 | import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils; 6 | import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; 7 | import org.springframework.stereotype.Component; 8 | 9 | import javax.annotation.PostConstruct; 10 | import javax.sql.DataSource; 11 | 12 | @Component 13 | public class DataSourceInitializer { 14 | 15 | @Autowired 16 | private DataSource dataSource1; 17 | 18 | @Autowired 19 | private DataSource dataSource2; 20 | 21 | @PostConstruct 22 | public void init1() { 23 | ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); 24 | populator.addScript(new ClassPathResource("/schema.sql")); 25 | populator.addScript(new ClassPathResource("/data1.sql")); 26 | DatabasePopulatorUtils.execute(populator, this.dataSource1); 27 | } 28 | 29 | @PostConstruct 30 | public void init2() { 31 | ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); 32 | populator.addScript(new ClassPathResource("/schema.sql")); 33 | populator.addScript(new ClassPathResource("/data2.sql")); 34 | DatabasePopulatorUtils.execute(populator, this.dataSource2); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/main/java/practice/configure/MultiTenancyJpaConfiguration.java: -------------------------------------------------------------------------------- 1 | package practice.configure; 2 | 3 | import org.hibernate.MultiTenancyStrategy; 4 | import org.hibernate.cfg.Environment; 5 | import org.hibernate.context.spi.CurrentTenantIdentifierResolver; 6 | import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryBuilder; 9 | import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties; 10 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; 14 | import practice.entity.Item; 15 | 16 | import javax.sql.DataSource; 17 | import java.util.LinkedHashMap; 18 | import java.util.Map; 19 | 20 | @Configuration 21 | @EnableConfigurationProperties(JpaProperties.class) 22 | public class MultiTenancyJpaConfiguration { 23 | 24 | @Autowired 25 | private DataSource dataSource; 26 | 27 | @Autowired 28 | private JpaProperties jpaProperties; 29 | 30 | @Autowired 31 | private MultiTenantConnectionProvider multiTenantConnectionProvider; 32 | 33 | @Autowired 34 | private CurrentTenantIdentifierResolver currentTenantIdentifierResolver; 35 | 36 | @Bean 37 | public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder factoryBuilder) { 38 | Map vendorProperties = new LinkedHashMap<>(); 39 | vendorProperties.putAll(jpaProperties.getHibernateProperties(dataSource)); 40 | 41 | vendorProperties.put(Environment.MULTI_TENANT, MultiTenancyStrategy.DATABASE); 42 | vendorProperties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider); 43 | vendorProperties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolver); 44 | 45 | return factoryBuilder.dataSource(dataSource) 46 | .packages(Item.class.getPackage().getName()) 47 | .properties(vendorProperties) 48 | .jta(false) 49 | .build(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/main/java/practice/configure/WebMvcConfiguration.java: -------------------------------------------------------------------------------- 1 | package practice.configure; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 5 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 6 | import practice.mvc.CurrentTenantIdentifierChangeInterceptor; 7 | 8 | @Configuration 9 | public class WebMvcConfiguration extends WebMvcConfigurerAdapter { 10 | 11 | @Override 12 | public void addInterceptors(InterceptorRegistry registry) { 13 | registry.addInterceptor(new CurrentTenantIdentifierChangeInterceptor()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/main/java/practice/entity/Item.java: -------------------------------------------------------------------------------- 1 | package practice.entity; 2 | 3 | import org.hibernate.search.annotations.Indexed; 4 | 5 | import javax.persistence.Entity; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | import java.io.Serializable; 10 | 11 | @Entity 12 | @Indexed 13 | public class Item implements Serializable { 14 | 15 | @Id 16 | @GeneratedValue(strategy = GenerationType.AUTO) 17 | private long id; 18 | 19 | private String name; 20 | 21 | public long getId() { 22 | return id; 23 | } 24 | 25 | public void setId(long id) { 26 | this.id = id; 27 | } 28 | 29 | public String getName() { 30 | return name; 31 | } 32 | 33 | public void setName(String name) { 34 | this.name = name; 35 | } 36 | 37 | @Override 38 | public String toString() { 39 | return name; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/main/java/practice/mvc/CurrentTenantIdentifierChangeInterceptor.java: -------------------------------------------------------------------------------- 1 | package practice.mvc; 2 | 3 | import org.springframework.web.servlet.HandlerMapping; 4 | import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; 5 | import practice.support.CurrentTenantIdentifierResolverImpl; 6 | 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.servlet.http.HttpServletResponse; 9 | import java.util.Map; 10 | 11 | public class CurrentTenantIdentifierChangeInterceptor extends HandlerInterceptorAdapter { 12 | 13 | @Override 14 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 15 | Map pathVariables = (Map) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); 16 | if (pathVariables.containsKey("tenant")) { 17 | request.setAttribute(CurrentTenantIdentifierResolverImpl.IDENTIFIER_ATTRIBUTE, pathVariables.get("tenant")); 18 | } 19 | return true; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/main/java/practice/mvc/IndexController.java: -------------------------------------------------------------------------------- 1 | package practice.mvc; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | 6 | @Controller 7 | @RequestMapping("/") 8 | public class IndexController { 9 | 10 | @RequestMapping 11 | public String index() { 12 | return "index"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/main/java/practice/mvc/ItemController.java: -------------------------------------------------------------------------------- 1 | package practice.mvc; 2 | 3 | import org.hibernate.search.jpa.FullTextEntityManager; 4 | import org.hibernate.search.jpa.Search; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.ui.Model; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import practice.entity.Item; 11 | import practice.repository.ItemRepository; 12 | 13 | import javax.persistence.EntityManager; 14 | import javax.persistence.PersistenceContext; 15 | import javax.transaction.Transactional; 16 | import java.util.List; 17 | 18 | @Controller 19 | @RequestMapping("/{tenant}") 20 | public class ItemController { 21 | 22 | @Autowired 23 | private ItemRepository itemRepository; 24 | 25 | @PersistenceContext 26 | private EntityManager entityManager; 27 | 28 | @RequestMapping 29 | public String items(@PathVariable String tenant, Model model) { 30 | List items = itemRepository.search(); 31 | model.addAttribute("tenant", tenant); 32 | model.addAttribute("items", items); 33 | return "items"; 34 | } 35 | 36 | @RequestMapping("/re-index") 37 | @Transactional 38 | public String reIndex() throws InterruptedException { 39 | FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager); 40 | fullTextEntityManager.createIndexer().startAndWait(); 41 | return "redirect:/{tenant}"; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/main/java/practice/repository/ItemRepository.java: -------------------------------------------------------------------------------- 1 | package practice.repository; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import practice.entity.Item; 5 | 6 | public interface ItemRepository extends CrudRepository, ItemRepositoryCustom { 7 | } 8 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/main/java/practice/repository/ItemRepositoryCustom.java: -------------------------------------------------------------------------------- 1 | package practice.repository; 2 | 3 | import practice.entity.Item; 4 | 5 | import java.util.List; 6 | 7 | public interface ItemRepositoryCustom { 8 | 9 | List search(); 10 | } 11 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/main/java/practice/repository/ItemRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package practice.repository; 2 | 3 | import org.apache.lucene.search.Query; 4 | import org.hibernate.search.jpa.FullTextEntityManager; 5 | import org.hibernate.search.jpa.FullTextQuery; 6 | import org.hibernate.search.jpa.Search; 7 | import org.hibernate.search.query.dsl.BooleanJunction; 8 | import org.hibernate.search.query.dsl.QueryBuilder; 9 | import practice.entity.Item; 10 | 11 | import javax.persistence.EntityManager; 12 | import javax.persistence.PersistenceContext; 13 | import java.util.List; 14 | 15 | public class ItemRepositoryImpl implements ItemRepositoryCustom { 16 | 17 | @PersistenceContext 18 | private EntityManager entityManager; 19 | 20 | @Override 21 | public List search() { 22 | FullTextEntityManager fullTextEntityManager = Search.getFullTextEntityManager(entityManager); 23 | QueryBuilder qb = fullTextEntityManager.getSearchFactory() 24 | .buildQueryBuilder() 25 | .forEntity(Item.class) 26 | .get(); 27 | 28 | BooleanJunction junction = qb.bool(); 29 | junction.must(qb.all().createQuery()); 30 | 31 | Query searchQuery = junction.createQuery(); 32 | 33 | FullTextQuery persistenceQuery = fullTextEntityManager.createFullTextQuery(searchQuery, Item.class); 34 | return persistenceQuery.getResultList(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/main/java/practice/support/CurrentTenantIdentifierResolverImpl.java: -------------------------------------------------------------------------------- 1 | package practice.support; 2 | 3 | import org.hibernate.context.spi.CurrentTenantIdentifierResolver; 4 | import org.springframework.stereotype.Component; 5 | import org.springframework.web.context.request.RequestAttributes; 6 | import org.springframework.web.context.request.RequestContextHolder; 7 | 8 | @Component 9 | public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver { 10 | 11 | public static final String IDENTIFIER_ATTRIBUTE = CurrentTenantIdentifierResolverImpl.class.getName() + ".IDENTIFIER"; 12 | 13 | public static final String TENANT_1_IDENTIFIER = "tenant1"; 14 | public static final String TENANT_2_IDENTIFIER = "tenant2"; 15 | 16 | @Override 17 | public String resolveCurrentTenantIdentifier() { 18 | RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); 19 | if (requestAttributes != null) { 20 | String identifier = (String) requestAttributes.getAttribute(IDENTIFIER_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); 21 | if (identifier != null) { 22 | return identifier; 23 | } 24 | } 25 | return TENANT_1_IDENTIFIER; 26 | } 27 | 28 | @Override 29 | public boolean validateExistingCurrentSessions() { 30 | return true; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/main/java/practice/support/DataSourceBasedMultiTenantConnectionProviderImpl.java: -------------------------------------------------------------------------------- 1 | package practice.support; 2 | 3 | import org.hibernate.engine.jdbc.connections.spi.AbstractDataSourceBasedMultiTenantConnectionProviderImpl; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Component; 6 | 7 | import javax.annotation.PostConstruct; 8 | import javax.sql.DataSource; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | @Component 13 | public class DataSourceBasedMultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl { 14 | 15 | @Autowired 16 | private DataSource dataSource1; 17 | 18 | @Autowired 19 | private DataSource dataSource2; 20 | 21 | private Map dataSourceMap = new HashMap<>(); 22 | 23 | @PostConstruct 24 | public void init() { 25 | dataSourceMap.put(CurrentTenantIdentifierResolverImpl.TENANT_1_IDENTIFIER, dataSource1); 26 | dataSourceMap.put(CurrentTenantIdentifierResolverImpl.TENANT_2_IDENTIFIER, dataSource2); 27 | } 28 | 29 | @Override 30 | protected DataSource selectAnyDataSource() { 31 | return dataSource1; 32 | } 33 | 34 | @Override 35 | protected DataSource selectDataSource(String tenantIdentifier) { 36 | return dataSourceMap.get(tenantIdentifier); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.jpa.hibernate.ddl-auto=none 2 | 3 | spring.jpa.properties.hibernate.search.default.directory_provider=ram 4 | 5 | spring.thymeleaf.cache=false 6 | 7 | spring.additional.datasource1.url=jdbc:h2:mem:test1 8 | spring.additional.datasource2.url=jdbc:h2:mem:test2 9 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/main/resources/data1.sql: -------------------------------------------------------------------------------- 1 | insert into Item (name) values ('Cheerleader'); 2 | insert into Item (name) values ('Can''t Feel My Face'); 3 | insert into Item (name) values ('See You Again'); 4 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/main/resources/data2.sql: -------------------------------------------------------------------------------- 1 | insert into Item (name) values ('Bad Blood'); 2 | insert into Item (name) values ('Watch Me'); 3 | insert into Item (name) values ('Trap Queen'); 4 | insert into Item (name) values ('Shut Up And Dance'); 5 | insert into Item (name) values ('Fight Song'); 6 | insert into Item (name) values ('Lean On'); 7 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/main/resources/schema.sql: -------------------------------------------------------------------------------- 1 | create table Item ( 2 | id bigint generated by default as identity, 3 | name varchar(255), 4 | primary key (id) 5 | ); -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Spring Best Practices Hibernate Search Multi Tenancy 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Hibernate Search Multi Tenancy

13 | 17 |
18 | 19 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/main/resources/templates/items.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Spring Best Practices Hibernate Search Multi Tenancy 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 | 15 |
16 |

Tenant

17 |
18 |
    19 |
  • 20 |
21 |
22 | 23 | -------------------------------------------------------------------------------- /spring-best-practice-hibernate-search-multi-tenancy/src/test/java/practice/SpringBestPracticeHibernateSearchMultiTenancyApplicationTests.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.SpringApplicationConfiguration; 6 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 7 | 8 | @RunWith(SpringJUnit4ClassRunner.class) 9 | @SpringApplicationConfiguration(classes = Application.class) 10 | public class SpringBestPracticeHibernateSearchMultiTenancyApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /spring-best-practice-manual-flashmap/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | sample 7 | spring-best-practice-manual-flashmap 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | Spring Best Practices Manual FlashMap 12 | Spring Best Practices Manual FlashMap 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 1.3.5.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | 1.8 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-devtools 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-thymeleaf 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-web 38 | 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-test 43 | test 44 | 45 | 46 | 47 | 48 | 49 | 50 | org.springframework.boot 51 | spring-boot-maven-plugin 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /spring-best-practice-manual-flashmap/src/main/java/practice/Application.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /spring-best-practice-manual-flashmap/src/main/java/practice/SampleController.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.servlet.FlashMap; 6 | import org.springframework.web.servlet.mvc.support.RedirectAttributes; 7 | import org.springframework.web.servlet.support.RequestContextUtils; 8 | 9 | import javax.servlet.http.HttpServletRequest; 10 | import javax.servlet.http.HttpServletResponse; 11 | 12 | @Controller 13 | public class SampleController { 14 | 15 | @RequestMapping("/") 16 | public String index( 17 | RedirectAttributes redirectAttributes, 18 | HttpServletRequest request, 19 | HttpServletResponse response) { 20 | // redirectAttributes.addFlashAttribute("message", "Hello!"); 21 | // return "redirect:/result"; 22 | 23 | FlashMap flashMap = RequestContextUtils.getOutputFlashMap(request); 24 | flashMap.put("message", "Hello!"); 25 | RequestContextUtils.getFlashMapManager(request).saveOutputFlashMap(flashMap, request, response); 26 | return "index"; 27 | } 28 | 29 | @RequestMapping("/result") 30 | public String result() { 31 | return "result"; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /spring-best-practice-manual-flashmap/src/main/resources/application.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tagbangers/spring-best-practices/1b5b9d2c4e0997d527f492187267b9199e858b7d/spring-best-practice-manual-flashmap/src/main/resources/application.properties -------------------------------------------------------------------------------- /spring-best-practice-manual-flashmap/src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Manual FlashMap 5 | 6 | 7 |

Manual FlashMap

8 | Go result 9 | 10 | -------------------------------------------------------------------------------- /spring-best-practice-manual-flashmap/src/main/resources/templates/result.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Manual FlashMap 5 | 6 | 7 |

Manual FlashMap

8 |

Message is: Message

9 | 10 | -------------------------------------------------------------------------------- /spring-best-practice-post-redirect-get/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.springframework.boot 8 | spring-boot-starter-parent 9 | 1.3.0.BUILD-SNAPSHOT 10 | 11 | jp.co.tagbangers.spring-best-practices 12 | spring-best-practice-post-redirect-get 13 | pom 14 | 15 | Spring Best Practices Post-Redirect-Get 16 | Spring Best Practices Post-Redirect-Get 17 | 18 | 19 | Tagbangers, Inc. 20 | https://www.tagbangers.co.jp/ 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-thymeleaf 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-test 31 | test 32 | 33 | 34 | 35 | 36 | 37 | spring-snapshots 38 | Spring Snapshots 39 | http://repo.spring.io/snapshot 40 | 41 | true 42 | 43 | 44 | 45 | spring-milestones 46 | Spring Milestones 47 | http://repo.spring.io/milestone 48 | 49 | false 50 | 51 | 52 | 53 | 54 | 55 | 56 | spring-snapshots 57 | Spring Snapshots 58 | http://repo.spring.io/snapshot 59 | 60 | true 61 | 62 | 63 | 64 | spring-milestones 65 | Spring Milestones 66 | http://repo.spring.io/milestone 67 | 68 | false 69 | 70 | 71 | 72 | spring-releases 73 | Spring Releases 74 | http://repo.spring.io/release 75 | 76 | false 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /spring-best-practice-post-redirect-get/src/main/java/practice/post_redirect_get/Application.java: -------------------------------------------------------------------------------- 1 | package practice.post_redirect_get; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) throws Exception { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | } -------------------------------------------------------------------------------- /spring-best-practice-post-redirect-get/src/main/java/practice/post_redirect_get/BasicExampleController.java: -------------------------------------------------------------------------------- 1 | package practice.post_redirect_get; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.ui.Model; 5 | import org.springframework.validation.BindingResult; 6 | import org.springframework.validation.annotation.Validated; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestMethod; 9 | import org.springframework.web.servlet.mvc.support.RedirectAttributes; 10 | 11 | @Controller 12 | @RequestMapping("/basic") 13 | public class BasicExampleController { 14 | 15 | public static final String FORM_MODEL_KEY = "form"; 16 | public static final String ERRORS_MODEL_KEY = BindingResult.MODEL_KEY_PREFIX + FORM_MODEL_KEY; 17 | 18 | @RequestMapping 19 | public String get(Model model) { 20 | BasicExampleForm form = (BasicExampleForm) model.asMap().get(FORM_MODEL_KEY); 21 | if (form == null) { 22 | form = new BasicExampleForm(); 23 | } 24 | model.addAttribute(FORM_MODEL_KEY, form); 25 | return "basic"; 26 | } 27 | 28 | @RequestMapping(method = RequestMethod.POST) 29 | public String post( 30 | @Validated BasicExampleForm form, 31 | BindingResult result, 32 | RedirectAttributes redirectAttributes) { 33 | redirectAttributes.addFlashAttribute(FORM_MODEL_KEY, form); 34 | redirectAttributes.addFlashAttribute(ERRORS_MODEL_KEY, result); 35 | 36 | if (result.hasErrors()) { 37 | return "redirect:/basic"; 38 | } 39 | 40 | // Do something. For example, call a method of a service class. 41 | 42 | redirectAttributes.getFlashAttributes().clear(); 43 | redirectAttributes.addFlashAttribute("message", "Success!"); 44 | return "redirect:/basic"; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /spring-best-practice-post-redirect-get/src/main/java/practice/post_redirect_get/BasicExampleForm.java: -------------------------------------------------------------------------------- 1 | package practice.post_redirect_get; 2 | 3 | import org.hibernate.validator.constraints.Email; 4 | import org.hibernate.validator.constraints.NotEmpty; 5 | 6 | import java.io.Serializable; 7 | 8 | public class BasicExampleForm implements Serializable { 9 | 10 | @NotEmpty 11 | @Email 12 | private String email; 13 | 14 | public String getEmail() { 15 | return email; 16 | } 17 | 18 | public void setEmail(String email) { 19 | this.email = email; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /spring-best-practice-post-redirect-get/src/main/java/practice/post_redirect_get/SessionExampleController.java: -------------------------------------------------------------------------------- 1 | package practice.post_redirect_get; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.ui.Model; 5 | import org.springframework.validation.BindingResult; 6 | import org.springframework.validation.annotation.Validated; 7 | import org.springframework.web.HttpSessionRequiredException; 8 | import org.springframework.web.bind.annotation.ExceptionHandler; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | import org.springframework.web.bind.annotation.RequestMethod; 11 | import org.springframework.web.bind.annotation.SessionAttributes; 12 | import org.springframework.web.servlet.mvc.support.RedirectAttributes; 13 | 14 | @Controller 15 | @RequestMapping("/session") 16 | @SessionAttributes(types = SessionExampleForm.class) 17 | public class SessionExampleController { 18 | 19 | public static final String FORM_MODEL_KEY = "form"; 20 | public static final String ERRORS_MODEL_KEY = BindingResult.MODEL_KEY_PREFIX + FORM_MODEL_KEY; 21 | 22 | @ExceptionHandler(HttpSessionRequiredException.class) 23 | public String handleHttpSessionRequiredException(HttpSessionRequiredException exception) { 24 | return "redirect:/session?expired"; 25 | } 26 | 27 | @RequestMapping 28 | public String init(Model model) { 29 | SessionExampleForm form = new SessionExampleForm(); 30 | model.addAttribute(FORM_MODEL_KEY, form); 31 | return "session-step1"; 32 | } 33 | 34 | @RequestMapping(params = "step1") 35 | public String step1(Model model) { 36 | SessionExampleForm form = (SessionExampleForm) model.asMap().get(FORM_MODEL_KEY); 37 | if (form == null) { 38 | form = new SessionExampleForm(); 39 | } 40 | model.addAttribute(FORM_MODEL_KEY, form); 41 | return "session-step1"; 42 | } 43 | 44 | @RequestMapping(params = "step1", method = RequestMethod.POST) 45 | public String step1( 46 | @Validated SessionExampleForm form, 47 | BindingResult result, 48 | RedirectAttributes redirectAttributes) { 49 | redirectAttributes.addFlashAttribute(FORM_MODEL_KEY, form); 50 | redirectAttributes.addFlashAttribute(ERRORS_MODEL_KEY, result); 51 | 52 | if (result.hasFieldErrors("email")) { 53 | return "redirect:/session?step1"; 54 | } 55 | 56 | redirectAttributes.getFlashAttributes().clear(); 57 | return "redirect:/session?step2"; 58 | } 59 | 60 | @RequestMapping(params = "step2") 61 | public String step2(Model model) throws HttpSessionRequiredException { 62 | SessionExampleForm form = (SessionExampleForm) model.asMap().get(FORM_MODEL_KEY); 63 | if (form == null) { 64 | throw new HttpSessionRequiredException("Expected session attribute '" + FORM_MODEL_KEY + "'"); 65 | } 66 | model.addAttribute(FORM_MODEL_KEY, form); 67 | return "session-step2"; 68 | } 69 | 70 | @RequestMapping(params = "step2", method = RequestMethod.POST) 71 | public String step2( 72 | @Validated SessionExampleForm form, 73 | BindingResult result, 74 | RedirectAttributes redirectAttributes) { 75 | redirectAttributes.addFlashAttribute(FORM_MODEL_KEY, form); 76 | redirectAttributes.addFlashAttribute(ERRORS_MODEL_KEY, result); 77 | 78 | if (result.hasErrors()) { 79 | return "redirect:/session?step2"; 80 | } 81 | 82 | // Do something. For example, call a method of a service class. 83 | 84 | redirectAttributes.getFlashAttributes().clear(); 85 | redirectAttributes.addFlashAttribute("message", "Success!"); 86 | return "redirect:/session"; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /spring-best-practice-post-redirect-get/src/main/java/practice/post_redirect_get/SessionExampleForm.java: -------------------------------------------------------------------------------- 1 | package practice.post_redirect_get; 2 | 3 | import org.hibernate.validator.constraints.Email; 4 | import org.hibernate.validator.constraints.Length; 5 | import org.hibernate.validator.constraints.NotEmpty; 6 | 7 | import java.io.Serializable; 8 | 9 | public class SessionExampleForm implements Serializable { 10 | 11 | @NotEmpty 12 | @Email 13 | private String email; 14 | 15 | @NotEmpty 16 | @Length(min = 8) 17 | private String password; 18 | 19 | public String getEmail() { 20 | return email; 21 | } 22 | 23 | public void setEmail(String email) { 24 | this.email = email; 25 | } 26 | 27 | public String getPassword() { 28 | return password; 29 | } 30 | 31 | public void setPassword(String password) { 32 | this.password = password; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /spring-best-practice-post-redirect-get/src/main/resources/templates/basic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Post-Redirect-Get 5 | 6 | 7 |

Post-Redirect-Get

8 |

Message

9 |
10 |
11 |

Validation error

12 |
13 | 14 | 15 | 16 |
17 | 18 | -------------------------------------------------------------------------------- /spring-best-practice-post-redirect-get/src/main/resources/templates/session-step1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Post-Redirect-Get 5 | 6 | 7 |

Post-Redirect-Get

8 |

Step1

9 |

Session Expired

10 |

Message

11 |
12 |
13 |

Validation error

14 |
15 | 16 | 17 | 18 |
19 | 20 | -------------------------------------------------------------------------------- /spring-best-practice-post-redirect-get/src/main/resources/templates/session-step2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Post-Redirect-Get 5 | 6 | 7 |

Post-Redirect-Get

8 |

Step2

9 |

Message

10 |
11 |
12 |

Validation error

13 |
14 | 15 | 16 | 17 |
18 | 19 | -------------------------------------------------------------------------------- /spring-best-practice-riot-ssr/README.md: -------------------------------------------------------------------------------- 1 | # Server-side rendering by Spring Boot and Riot.js 2 | 3 | ## How to Run 4 | 5 | ``` 6 | cd spring-best-practice-riot-ssr 7 | mvn spring-boot:run 8 | ``` 9 | 10 | ## Compile Riot Tag 11 | 12 | ``` 13 | cd spring-best-practice-riot-ssr/src/main/resources/static 14 | riot tag/todo.tag js/ 15 | ``` 16 | -------------------------------------------------------------------------------- /spring-best-practice-riot-ssr/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.example 7 | spring-best-practice-riot-ssr 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | spring-best-practice-riot-ssr 12 | Riot.js Server-side rendering 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 1.4.0.M3 18 | 19 | 20 | 21 | 22 | UTF-8 23 | 1.8 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-web 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-data-jpa 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-thymeleaf 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-devtools 42 | 43 | 44 | 45 | org.flywaydb 46 | flyway-core 47 | 48 | 49 | 50 | org.projectlombok 51 | lombok 52 | 53 | 54 | 55 | com.h2database 56 | h2 57 | runtime 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-starter-test 62 | test 63 | 64 | 65 | 66 | 67 | 68 | 69 | org.springframework.boot 70 | spring-boot-maven-plugin 71 | 72 | 73 | 74 | 75 | 76 | 77 | spring-snapshots 78 | Spring Snapshots 79 | https://repo.spring.io/snapshot 80 | 81 | true 82 | 83 | 84 | 85 | spring-milestones 86 | Spring Milestones 87 | https://repo.spring.io/milestone 88 | 89 | false 90 | 91 | 92 | 93 | 94 | 95 | spring-snapshots 96 | Spring Snapshots 97 | https://repo.spring.io/snapshot 98 | 99 | true 100 | 101 | 102 | 103 | spring-milestones 104 | Spring Milestones 105 | https://repo.spring.io/milestone 106 | 107 | false 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /spring-best-practice-riot-ssr/src/main/java/practice/Application.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * @author Ogawa, Takeshi 8 | */ 9 | @SpringBootApplication 10 | public class Application { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(Application.class, args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /spring-best-practice-riot-ssr/src/main/java/practice/IndexController.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import jdk.nashorn.api.scripting.NashornScriptEngine; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.core.io.ResourceLoader; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.ui.Model; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | 11 | import javax.script.ScriptEngineManager; 12 | import javax.script.ScriptException; 13 | import java.io.IOException; 14 | import java.io.InputStreamReader; 15 | import java.util.List; 16 | 17 | /** 18 | * @author Ogawa, Takeshi 19 | */ 20 | @Controller 21 | public class IndexController { 22 | 23 | @Autowired 24 | private ResourceLoader resourceLoader; 25 | 26 | @Autowired 27 | private TodoRepository todoRepository; 28 | 29 | @Autowired 30 | private ObjectMapper objectMapper; 31 | 32 | @RequestMapping("/") 33 | public String index(Model model) throws ScriptException, IOException { 34 | NashornScriptEngine nashorn = (NashornScriptEngine) new ScriptEngineManager().getEngineByName("nashorn"); 35 | nashorn.eval(new InputStreamReader(resourceLoader.getResource("classpath:static/js/jvm-npm.js").getInputStream(), "UTF-8")); 36 | nashorn.eval(new InputStreamReader(resourceLoader.getResource("classpath:static/js/nashorn-riot.js").getInputStream(), "UTF-8")); 37 | nashorn.eval(new InputStreamReader(resourceLoader.getResource("classpath:static/js/riot.js").getInputStream(), "UTF-8")); 38 | nashorn.eval(new InputStreamReader(resourceLoader.getResource("classpath:static/js/riot-render.js").getInputStream(), "UTF-8")); 39 | nashorn.eval(new InputStreamReader(resourceLoader.getResource("classpath:static/js/todo.js").getInputStream(), "UTF-8")); 40 | 41 | String title = "I want to behave!"; 42 | List todos = todoRepository.findAll(); 43 | String todoTag = (String) nashorn.eval(String.format("riot.render('todo', {title: '%s', items: %s})", title, objectMapper.writeValueAsString(todos))); 44 | 45 | model.addAttribute("title", title); 46 | model.addAttribute("todos", todos); 47 | model.addAttribute("todoTag", todoTag); 48 | return "index"; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /spring-best-practice-riot-ssr/src/main/java/practice/Todo.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import lombok.*; 4 | 5 | import javax.persistence.*; 6 | import java.io.Serializable; 7 | 8 | /** 9 | * @author Ogawa, Takeshi 10 | */ 11 | @Entity 12 | @Table(name = "todo") 13 | @Data 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class Todo implements Serializable{ 17 | 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.IDENTITY) 20 | private long id; 21 | 22 | @Lob 23 | private String title; 24 | 25 | private boolean done; 26 | } 27 | -------------------------------------------------------------------------------- /spring-best-practice-riot-ssr/src/main/java/practice/TodoRepository.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.stereotype.Repository; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * @author Ogawa, Takeshi 10 | */ 11 | @Repository 12 | public interface TodoRepository extends JpaRepository { 13 | 14 | List findAllByDoneIsTrue(); 15 | } 16 | -------------------------------------------------------------------------------- /spring-best-practice-riot-ssr/src/main/java/practice/TodoRestController.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.web.bind.annotation.*; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * @author Ogawa, Takeshi 10 | */ 11 | @RestController 12 | @RequestMapping("/todo") 13 | public class TodoRestController { 14 | 15 | @Autowired 16 | private TodoRepository todoRepository; 17 | 18 | @PostMapping 19 | public Todo add(@RequestParam String title) { 20 | Todo todo = new Todo(); 21 | todo.setTitle(title); 22 | return todoRepository.save(todo); 23 | } 24 | 25 | @PutMapping("/{id}") 26 | public Todo toggle(@PathVariable long id) { 27 | Todo todo = todoRepository.findOne(id); 28 | todo.setDone(!todo.isDone()); 29 | todo = todoRepository.save(todo); 30 | return todo; 31 | } 32 | 33 | @DeleteMapping 34 | public void removeAllDone() { 35 | List todos = todoRepository.findAllByDoneIsTrue(); 36 | todoRepository.deleteInBatch(todos); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /spring-best-practice-riot-ssr/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:h2:file:./db/todo 2 | 3 | spring.jpa.database=H2 4 | spring.jpa.hibernate.ddl-auto=validate 5 | spring.jpa.properties.hibernate.show_sql=true 6 | -------------------------------------------------------------------------------- /spring-best-practice-riot-ssr/src/main/resources/db/migration/V1_0_0__init.sql: -------------------------------------------------------------------------------- 1 | // Create tables 2 | CREATE TABLE todo ( 3 | id BIGINT generated BY DEFAULT AS IDENTITY, 4 | done BOOLEAN NOT NULL, 5 | title CLOB, 6 | PRIMARY KEY (id) 7 | ); 8 | 9 | // Insert initial data 10 | INSERT INTO todo (title, done) VALUES ('Avoid excessive caffeine', TRUE); 11 | INSERT INTO todo (title, done) VALUES ('Be less provocative', FALSE); 12 | INSERT INTO todo (title, done) VALUES ('Be nice to people', FALSE); -------------------------------------------------------------------------------- /spring-best-practice-riot-ssr/src/main/resources/static/css/todo.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | font-family: 'myriad pro', sans-serif; 4 | font-size: 20px; 5 | border: 0; 6 | } 7 | 8 | todo { 9 | display: block; 10 | max-width: 400px; 11 | margin: 5% auto; 12 | } 13 | 14 | form input { 15 | font-size: 85%; 16 | padding: .4em; 17 | border: 1px solid #ccc; 18 | border-radius: 2px; 19 | } 20 | 21 | button { 22 | background-color: #1FADC5; 23 | border: 1px solid rgba(0,0,0,.2); 24 | font-size: 75%; 25 | color: #fff; 26 | padding: .4em 1.2em; 27 | border-radius: 2em; 28 | cursor: pointer; 29 | margin: 0 .23em; 30 | outline: none; 31 | } 32 | 33 | button[disabled] { 34 | background-color: #ddd; 35 | color: #aaa; 36 | } 37 | 38 | ul { 39 | padding: 0; 40 | } 41 | 42 | li { 43 | list-style-type: none; 44 | padding: .2em 0; 45 | } 46 | 47 | .completed { 48 | text-decoration: line-through; 49 | color: #ccc; 50 | } 51 | 52 | label { 53 | cursor: pointer; 54 | } 55 | -------------------------------------------------------------------------------- /spring-best-practice-riot-ssr/src/main/resources/static/js/jvm-npm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 Lance Ball 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | // Since we intend to use the Function constructor. 17 | /* jshint evil: true */ 18 | 19 | module = (typeof module == 'undefined') ? {} : module; 20 | 21 | (function() { 22 | var System = java.lang.System, 23 | Scanner = java.util.Scanner, 24 | File = java.io.File; 25 | 26 | NativeRequire = (typeof NativeRequire === 'undefined') ? {} : NativeRequire; 27 | if (typeof require === 'function' && !NativeRequire.require) { 28 | NativeRequire.require = require; 29 | } 30 | 31 | function Module(id, parent, core) { 32 | this.id = id; 33 | this.core = core; 34 | this.parent = parent; 35 | this.children = []; 36 | this.filename = id; 37 | this.loaded = false; 38 | 39 | Object.defineProperty( this, 'exports', { 40 | get: function() { 41 | return this._exports; 42 | }.bind(this), 43 | set: function(val) { 44 | Require.cache[this.filename] = val; 45 | this._exports = val; 46 | }.bind(this), 47 | } ); 48 | this.exports = {}; 49 | 50 | if (parent && parent.children) parent.children.push(this); 51 | 52 | this.require = function(id) { 53 | return Require(id, this); 54 | }.bind(this); 55 | } 56 | 57 | Module._load = function _load(file, parent, core, main) { 58 | var module = new Module(file, parent, core); 59 | var __FILENAME__ = module.filename; 60 | var body = readFile(module.filename, module.core), 61 | dir = new File(module.filename).getParent(), 62 | args = ['exports', 'module', 'require', '__filename', '__dirname'], 63 | func = new Function(args, body); 64 | func.apply(module, 65 | [module.exports, module, module.require, module.filename, dir]); 66 | module.loaded = true; 67 | module.main = main; 68 | return module.exports; 69 | }; 70 | 71 | Module.runMain = function runMain(main) { 72 | var file = Require.resolve(main); 73 | Module._load(file, undefined, false, true); 74 | }; 75 | 76 | function Require(id, parent) { 77 | var core, native, file = Require.resolve(id, parent); 78 | 79 | if (!file) { 80 | if (typeof NativeRequire.require === 'function') { 81 | if (Require.debug) { 82 | System.out.println(['Cannot resolve', id, 'defaulting to native'].join(' ')); 83 | } 84 | native = NativeRequire.require(id); 85 | if (native) return native; 86 | } 87 | System.err.println("Cannot find module " + id); 88 | throw new ModuleError("Cannot find module " + id, "MODULE_NOT_FOUND"); 89 | } 90 | 91 | if (file.core) { 92 | file = file.path; 93 | core = true; 94 | } 95 | try { 96 | if (Require.cache[file]) { 97 | return Require.cache[file]; 98 | } else if (file.endsWith('.js')) { 99 | return Module._load(file, parent, core); 100 | } else if (file.endsWith('.json')) { 101 | return loadJSON(file); 102 | } 103 | } catch(ex) { 104 | if (ex instanceof java.lang.Exception) { 105 | throw new ModuleError("Cannot load module " + id, "LOAD_ERROR", ex); 106 | } else { 107 | System.out.println("Cannot load module " + id + " LOAD_ERROR"); 108 | throw ex; 109 | } 110 | } 111 | } 112 | 113 | Require.resolve = function(id, parent) { 114 | var roots = findRoots(parent); 115 | for ( var i = 0 ; i < roots.length ; ++i ) { 116 | var root = roots[i]; 117 | var result = resolveCoreModule(id, root) || 118 | resolveAsFile(id, root, '.js') || 119 | resolveAsFile(id, root, '.json') || 120 | resolveAsDirectory(id, root) || 121 | resolveAsNodeModule(id, root); 122 | if ( result ) { 123 | return result; 124 | } 125 | } 126 | return false; 127 | }; 128 | 129 | Require.root = System.getProperty('user.dir'); 130 | Require.NODE_PATH = undefined; 131 | 132 | function findRoots(parent) { 133 | var r = []; 134 | r.push( findRoot( parent ) ); 135 | return r.concat( Require.paths() ); 136 | } 137 | 138 | function parsePaths(paths) { 139 | if ( ! paths ) { 140 | return []; 141 | } 142 | if ( paths === '' ) { 143 | return []; 144 | } 145 | var osName = java.lang.System.getProperty("os.name").toLowerCase(); 146 | var separator; 147 | 148 | if ( osName.indexOf( 'win' ) >= 0 ) { 149 | separator = ';'; 150 | } else { 151 | separator = ':'; 152 | } 153 | 154 | return paths.split( separator ); 155 | } 156 | 157 | Require.paths = function() { 158 | var r = []; 159 | r.push( java.lang.System.getProperty( "user.home" ) + "/.node_modules" ); 160 | r.push( java.lang.System.getProperty( "user.home" ) + "/.node_libraries" ); 161 | 162 | if ( Require.NODE_PATH ) { 163 | r = r.concat( parsePaths( Require.NODE_PATH ) ); 164 | } else { 165 | var NODE_PATH = java.lang.System.getenv.NODE_PATH; 166 | if ( NODE_PATH ) { 167 | r = r.concat( parsePaths( NODE_PATH ) ); 168 | } 169 | } 170 | // r.push( $PREFIX + "/node/library" ); 171 | return r; 172 | }; 173 | 174 | function findRoot(parent) { 175 | if (!parent || !parent.id) { return Require.root; } 176 | var pathParts = parent.id.split(/[\/|\\,]+/g); 177 | pathParts.pop(); 178 | return pathParts.join('/'); 179 | } 180 | 181 | Require.debug = true; 182 | Require.cache = {}; 183 | Require.extensions = {}; 184 | require = Require; 185 | 186 | module.exports = Module; 187 | 188 | 189 | function loadJSON(file) { 190 | var json = JSON.parse(readFile(file)); 191 | Require.cache[file] = json; 192 | return json; 193 | } 194 | 195 | function resolveAsNodeModule(id, root) { 196 | var base = [root, 'node_modules'].join('/'); 197 | return resolveAsFile(id, base) || 198 | resolveAsDirectory(id, base) || 199 | (root ? resolveAsNodeModule(id, new File(root).getParent()) : false); 200 | } 201 | 202 | function resolveAsDirectory(id, root) { 203 | var base = [root, id].join('/'), 204 | file = new File([base, 'package.json'].join('/')); 205 | if (file.exists()) { 206 | try { 207 | var body = readFile(file.getCanonicalPath()), 208 | package = JSON.parse(body); 209 | if (package.main) { 210 | return (resolveAsFile(package.main, base) || 211 | resolveAsDirectory(package.main, base)); 212 | } 213 | // if no package.main exists, look for index.js 214 | return resolveAsFile('index.js', base); 215 | } catch(ex) { 216 | throw new ModuleError("Cannot load JSON file", "PARSE_ERROR", ex); 217 | } 218 | } 219 | return resolveAsFile('index.js', base); 220 | } 221 | 222 | function resolveAsFile(id, root, ext) { 223 | var file; 224 | if ( id.length > 0 && id[0] === '/' ) { 225 | file = new File(normalizeName(id, ext || '.js')); 226 | if (!file.exists()) { 227 | return resolveAsDirectory(id); 228 | } 229 | } else { 230 | file = new File([root, normalizeName(id, ext || '.js')].join('/')); 231 | } 232 | if (file.exists()) { 233 | return file.getCanonicalPath(); 234 | } 235 | } 236 | 237 | function resolveCoreModule(id, root) { 238 | var name = normalizeName(id); 239 | var classloader = java.lang.Thread.currentThread().getContextClassLoader(); 240 | if (classloader.getResource(name)) 241 | return { path: name, core: true }; 242 | } 243 | 244 | function normalizeName(fileName, ext) { 245 | var extension = ext || '.js'; 246 | if (fileName.endsWith(extension)) { 247 | return fileName; 248 | } 249 | return fileName + extension; 250 | } 251 | 252 | function readFile(filename, core) { 253 | var input; 254 | try { 255 | if (core) { 256 | var classloader = java.lang.Thread.currentThread().getContextClassLoader(); 257 | input = classloader.getResourceAsStream(filename); 258 | } else { 259 | input = new File(filename); 260 | } 261 | // TODO: I think this is not very efficient 262 | return new Scanner(input).useDelimiter("\\A").next(); 263 | } catch(e) { 264 | throw new ModuleError("Cannot read file ["+input+"]: ", "IO_ERROR", e); 265 | } 266 | } 267 | 268 | function ModuleError(message, code, cause) { 269 | this.code = code || "UNDEFINED"; 270 | this.message = message || "Error loading module"; 271 | this.cause = cause; 272 | } 273 | 274 | // Helper function until ECMAScript 6 is complete 275 | if (typeof String.prototype.endsWith !== 'function') { 276 | String.prototype.endsWith = function(suffix) { 277 | if (!suffix) return false; 278 | return this.indexOf(suffix, this.length - suffix.length) !== -1; 279 | }; 280 | } 281 | 282 | ModuleError.prototype = new Error(); 283 | ModuleError.prototype.constructor = ModuleError; 284 | 285 | }()); 286 | -------------------------------------------------------------------------------- /spring-best-practice-riot-ssr/src/main/resources/static/js/nashorn-riot.js: -------------------------------------------------------------------------------- 1 | // simple-dom helper 2 | 3 | var simpleDom = require('static/js/simple-dom') 4 | var simpleTokenizer = require('static/js/simple-html-tokenizer') 5 | 6 | // create `document` to make riot work 7 | if (typeof window == 'undefined') { 8 | /*eslint-disable*/ 9 | document = new simpleDom.Document() 10 | /*eslint-enable*/ 11 | } 12 | 13 | // easy like a pie! closes #1780 14 | document.createElementNS = document.createElement 15 | 16 | document.querySelector = function () { return false } 17 | document.getElementsByTagName = function(tagName) { return [new simpleDom.Element(tagName)] } 18 | 19 | // add `innerHTML` property to simple-dom element 20 | Object.defineProperty(simpleDom.Element.prototype, 'innerHTML', { 21 | set: function(html) { 22 | var frag = sdom.parse(html) 23 | while (this.firstChild) this.removeChild(this.firstChild) 24 | this.appendChild(frag) 25 | }, 26 | get: function() { 27 | var html = '', 28 | next = this.firstChild 29 | 30 | while (next) { 31 | html += sdom.serialize(next) 32 | next = next.nextSibling 33 | } 34 | 35 | return html 36 | } 37 | }) 38 | 39 | // set the value attribute correctly on the input tags 40 | Object.defineProperty(simpleDom.Element.prototype, 'value', { 41 | set: function(val) { 42 | // is an input tag 43 | if (~['input', 'option', 'textarea'].indexOf(this.tagName.toLowerCase())) 44 | this.setAttribute('value', val) 45 | else 46 | this.value = val 47 | } 48 | }) 49 | 50 | // add `outerHTML` property to simple-dom element 51 | Object.defineProperty(simpleDom.Element.prototype, 'outerHTML', { 52 | get: function() { 53 | var html = sdom.serialize(this) 54 | var rxstr = '^(<' + this.tagName + '>.*?)' 55 | var match = html.match(new RegExp(rxstr, 'i')) 56 | return match ? match[0] : html 57 | } 58 | }) 59 | 60 | // add `style` property to simple-dom element 61 | Object.defineProperty(simpleDom.Element.prototype, 'style', { 62 | get: function() { 63 | var el = this 64 | return Object.defineProperty({}, 'display', { 65 | set: function(value) { 66 | el.setAttribute('style', 'display: ' + value + ';') 67 | } 68 | }) 69 | } 70 | }) 71 | 72 | var sdom = module.exports = { 73 | parse: function(html) { 74 | // parse html string to simple-dom document 75 | var blank = new simpleDom.Document() 76 | var parser = new simpleDom.HTMLParser(simpleTokenizer.tokenize, blank, simpleDom.voidMap) 77 | return parser.parse(html) 78 | }, 79 | serialize: function(doc) { 80 | // serialize simple-dom document to html string 81 | var serializer = new simpleDom.HTMLSerializer(simpleDom.voidMap) 82 | return serializer.serialize(doc) 83 | } 84 | } 85 | 86 | var window = this; 87 | 88 | var history = {}; 89 | history.location = {} 90 | history.location.href = null; 91 | 92 | var console = {}; 93 | console.debug = print; 94 | console.warn = print; 95 | console.log = print; 96 | 97 | var setTimeout = function () {} -------------------------------------------------------------------------------- /spring-best-practice-riot-ssr/src/main/resources/static/js/riot-render.js: -------------------------------------------------------------------------------- 1 | riot.render = function(tagName, opts) { 2 | var tag = riot.render.tag(tagName, opts), 3 | html = sdom.serialize(tag.root) 4 | // unmount the tag avoiding memory leaks 5 | tag.unmount() 6 | return html 7 | } 8 | 9 | riot.render.dom = function(tagName, opts) { 10 | return riot.render.tag(tagName, opts).root 11 | } 12 | 13 | riot.render.tag = function(tagName, opts) { 14 | var root = document.createElement(tagName), 15 | tag = riot.mount(root, opts)[0] 16 | return tag 17 | } 18 | -------------------------------------------------------------------------------- /spring-best-practice-riot-ssr/src/main/resources/static/js/simple-dom.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 3 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 4 | factory((global.SimpleDOMTests = {})); 5 | }(this, function (exports) { 'use strict'; 6 | 7 | function Node(nodeType, nodeName, nodeValue) { 8 | this.nodeType = nodeType; 9 | this.nodeName = nodeName; 10 | this.nodeValue = nodeValue; 11 | 12 | this.childNodes = new ChildNodes(this); 13 | 14 | this.parentNode = null; 15 | this.previousSibling = null; 16 | this.nextSibling = null; 17 | this.firstChild = null; 18 | this.lastChild = null; 19 | } 20 | 21 | Node.prototype._cloneNode = function() { 22 | return new Node(this.nodeType, this.nodeName, this.nodeValue); 23 | }; 24 | 25 | Node.prototype.cloneNode = function(deep) { 26 | var node = this._cloneNode(); 27 | 28 | if (deep) { 29 | var child = this.firstChild, nextChild = child; 30 | 31 | while (nextChild) { 32 | nextChild = child.nextSibling; 33 | node.appendChild(child.cloneNode(true)); 34 | child = nextChild; 35 | } 36 | } 37 | 38 | return node; 39 | }; 40 | 41 | Node.prototype.appendChild = function(node) { 42 | if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { 43 | insertFragment(node, this, this.lastChild, null); 44 | return; 45 | } 46 | 47 | if (node.parentNode) { node.parentNode.removeChild(node); } 48 | 49 | node.parentNode = this; 50 | var refNode = this.lastChild; 51 | if (refNode === null) { 52 | this.firstChild = node; 53 | this.lastChild = node; 54 | } else { 55 | node.previousSibling = refNode; 56 | refNode.nextSibling = node; 57 | this.lastChild = node; 58 | } 59 | }; 60 | 61 | function insertFragment(fragment, newParent, before, after) { 62 | if (!fragment.firstChild) { return; } 63 | 64 | var firstChild = fragment.firstChild; 65 | var lastChild = firstChild; 66 | var node = firstChild; 67 | 68 | firstChild.previousSibling = before; 69 | if (before) { 70 | before.nextSibling = firstChild; 71 | } else { 72 | newParent.firstChild = firstChild; 73 | } 74 | 75 | while (node) { 76 | node.parentNode = newParent; 77 | lastChild = node; 78 | node = node.nextSibling; 79 | } 80 | 81 | lastChild.nextSibling = after; 82 | if (after) { 83 | after.previousSibling = lastChild; 84 | } else { 85 | newParent.lastChild = lastChild; 86 | } 87 | } 88 | 89 | Node.prototype.insertBefore = function(node, refNode) { 90 | if (refNode == null) { 91 | return this.appendChild(node); 92 | } 93 | 94 | if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE) { 95 | insertFragment(node, this, refNode ? refNode.previousSibling : null, refNode); 96 | return; 97 | } 98 | 99 | node.parentNode = this; 100 | 101 | var previousSibling = refNode.previousSibling; 102 | if (previousSibling) { 103 | previousSibling.nextSibling = node; 104 | node.previousSibling = previousSibling; 105 | }else{ 106 | node.previousSibling = null 107 | } 108 | 109 | refNode.previousSibling = node; 110 | node.nextSibling = refNode; 111 | 112 | if (this.firstChild === refNode) { 113 | this.firstChild = node; 114 | } 115 | }; 116 | 117 | Node.prototype.removeChild = function(refNode) { 118 | if (this.firstChild === refNode) { 119 | this.firstChild = refNode.nextSibling; 120 | } 121 | if (this.lastChild === refNode) { 122 | this.lastChild = refNode.previousSibling; 123 | } 124 | if (refNode.previousSibling) { 125 | refNode.previousSibling.nextSibling = refNode.nextSibling; 126 | } 127 | if (refNode.nextSibling) { 128 | refNode.nextSibling.previousSibling = refNode.previousSibling; 129 | } 130 | refNode.parentNode = null; 131 | refNode.nextSibling = null; 132 | refNode.previousSibling = null; 133 | }; 134 | 135 | Node.ELEMENT_NODE = 1; 136 | Node.ATTRIBUTE_NODE = 2; 137 | Node.TEXT_NODE = 3; 138 | Node.CDATA_SECTION_NODE = 4; 139 | Node.ENTITY_REFERENCE_NODE = 5; 140 | Node.ENTITY_NODE = 6; 141 | Node.PROCESSING_INSTRUCTION_NODE = 7; 142 | Node.COMMENT_NODE = 8; 143 | Node.DOCUMENT_NODE = 9; 144 | Node.DOCUMENT_TYPE_NODE = 10; 145 | Node.DOCUMENT_FRAGMENT_NODE = 11; 146 | Node.NOTATION_NODE = 12; 147 | 148 | function ChildNodes(node) { 149 | this.node = node; 150 | } 151 | 152 | ChildNodes.prototype.item = function(index) { 153 | var child = this.node.firstChild; 154 | 155 | for (var i = 0; child && index !== i; i++) { 156 | child = child.nextSibling; 157 | } 158 | 159 | return child; 160 | }; 161 | 162 | function Element(tagName) { 163 | tagName = tagName.toUpperCase(); 164 | 165 | this.nodeConstructor(1, tagName, null); 166 | this.attributes = []; 167 | this.tagName = tagName; 168 | } 169 | 170 | Element.prototype = Object.create(Node.prototype); 171 | Element.prototype.constructor = Element; 172 | Element.prototype.nodeConstructor = Node; 173 | 174 | Element.prototype._cloneNode = function() { 175 | var node = new Element(this.tagName); 176 | 177 | node.attributes = this.attributes.map(function(attr) { 178 | return { name: attr.name, value: attr.value, specified: attr.specified }; 179 | }); 180 | 181 | return node; 182 | }; 183 | 184 | Element.prototype.getAttribute = function(_name) { 185 | var attributes = this.attributes; 186 | var name = _name.toLowerCase(); 187 | var attr; 188 | for (var i=0, l=attributes.length; i'; 394 | }; 395 | 396 | HTMLSerializer.prototype.closeTag = function(element) { 397 | return ''; 398 | }; 399 | 400 | HTMLSerializer.prototype.isVoid = function(element) { 401 | return this.voidMap[element.nodeName] === true; 402 | }; 403 | 404 | HTMLSerializer.prototype.attributes = function(namedNodeMap) { 405 | var buffer = ''; 406 | for (var i=0, l=namedNodeMap.length; i]/g, function(match) { 435 | switch(match) { 436 | case '&': 437 | return '&'; 438 | case '<': 439 | return '<'; 440 | case '>': 441 | return '>'; 442 | } 443 | }); 444 | }; 445 | 446 | HTMLSerializer.prototype.text = function(text) { 447 | return this.escapeText(text.nodeValue); 448 | }; 449 | 450 | HTMLSerializer.prototype.rawHTMLSection = function(text) { 451 | return text.nodeValue; 452 | }; 453 | 454 | HTMLSerializer.prototype.comment = function(comment) { 455 | return ''; 456 | }; 457 | 458 | HTMLSerializer.prototype.serializeChildren = function(node) { 459 | var buffer = ''; 460 | var next = node.firstChild; 461 | if (next) { 462 | buffer += this.serialize(next); 463 | 464 | while(next = next.nextSibling) { 465 | buffer += this.serialize(next); 466 | } 467 | } 468 | return buffer; 469 | }; 470 | 471 | HTMLSerializer.prototype.serialize = function(node) { 472 | var buffer = ''; 473 | 474 | // open 475 | switch (node.nodeType) { 476 | case 1: 477 | buffer += this.openTag(node); 478 | break; 479 | case 3: 480 | buffer += this.text(node); 481 | break; 482 | case -1: 483 | buffer += this.rawHTMLSection(node); 484 | break; 485 | case 8: 486 | buffer += this.comment(node); 487 | break; 488 | default: 489 | break; 490 | } 491 | 492 | buffer += this.serializeChildren(node); 493 | 494 | if (node.nodeType === 1 && !this.isVoid(node)) { 495 | buffer += this.closeTag(node); 496 | } 497 | 498 | return buffer; 499 | }; 500 | 501 | var _voidMap = { 502 | AREA: true, 503 | BASE: true, 504 | BR: true, 505 | COL: true, 506 | COMMAND: true, 507 | EMBED: true, 508 | HR: true, 509 | IMG: true, 510 | INPUT: true, 511 | KEYGEN: true, 512 | LINK: true, 513 | META: true, 514 | PARAM: true, 515 | SOURCE: true, 516 | TRACK: true, 517 | WBR: true 518 | }; 519 | 520 | exports.Node = Node; 521 | exports.Element = Element; 522 | exports.DocumentFragment = DocumentFragment; 523 | exports.Document = Document; 524 | exports.HTMLParser = HTMLParser; 525 | exports.HTMLSerializer = HTMLSerializer; 526 | exports.voidMap = _voidMap; 527 | 528 | })); 529 | //# sourceMappingURL=simple-dom.js.map -------------------------------------------------------------------------------- /spring-best-practice-riot-ssr/src/main/resources/static/js/simple-html-tokenizer.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 3 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 4 | factory((global.HTML5Tokenizer = {})); 5 | }(this, function (exports) { 'use strict'; 6 | 7 | var HTML5NamedCharRefs = { 8 | Aacute:"Á",aacute:"á",Abreve:"Ă",abreve:"ă",ac:"∾",acd:"∿",acE:"∾̳",Acirc:"Â",acirc:"â",acute:"´",Acy:"А",acy:"а",AElig:"Æ",aelig:"æ",af:"\u2061",Afr:"𝔄",afr:"𝔞",Agrave:"À",agrave:"à",alefsym:"ℵ",aleph:"ℵ",Alpha:"Α",alpha:"α",Amacr:"Ā",amacr:"ā",amalg:"⨿",AMP:"&",amp:"&",And:"⩓",and:"∧",andand:"⩕",andd:"⩜",andslope:"⩘",andv:"⩚",ang:"∠",ange:"⦤",angle:"∠",angmsd:"∡",angmsdaa:"⦨",angmsdab:"⦩",angmsdac:"⦪",angmsdad:"⦫",angmsdae:"⦬",angmsdaf:"⦭",angmsdag:"⦮",angmsdah:"⦯",angrt:"∟",angrtvb:"⊾",angrtvbd:"⦝",angsph:"∢",angst:"Å",angzarr:"⍼",Aogon:"Ą",aogon:"ą",Aopf:"𝔸",aopf:"𝕒",ap:"≈",apacir:"⩯",apE:"⩰",ape:"≊",apid:"≋",apos:"'",ApplyFunction:"\u2061",approx:"≈",approxeq:"≊",Aring:"Å",aring:"å",Ascr:"𝒜",ascr:"𝒶",Assign:"≔",ast:"*",asymp:"≈",asympeq:"≍",Atilde:"Ã",atilde:"ã",Auml:"Ä",auml:"ä",awconint:"∳",awint:"⨑",backcong:"≌",backepsilon:"϶",backprime:"‵",backsim:"∽",backsimeq:"⋍",Backslash:"∖",Barv:"⫧",barvee:"⊽",Barwed:"⌆",barwed:"⌅",barwedge:"⌅",bbrk:"⎵",bbrktbrk:"⎶",bcong:"≌",Bcy:"Б",bcy:"б",bdquo:"„",becaus:"∵",Because:"∵",because:"∵",bemptyv:"⦰",bepsi:"϶",bernou:"ℬ",Bernoullis:"ℬ",Beta:"Β",beta:"β",beth:"ℶ",between:"≬",Bfr:"𝔅",bfr:"𝔟",bigcap:"⋂",bigcirc:"◯",bigcup:"⋃",bigodot:"⨀",bigoplus:"⨁",bigotimes:"⨂",bigsqcup:"⨆",bigstar:"★",bigtriangledown:"▽",bigtriangleup:"△",biguplus:"⨄",bigvee:"⋁",bigwedge:"⋀",bkarow:"⤍",blacklozenge:"⧫",blacksquare:"▪",blacktriangle:"▴",blacktriangledown:"▾",blacktriangleleft:"◂",blacktriangleright:"▸",blank:"␣",blk12:"▒",blk14:"░",blk34:"▓",block:"█",bne:"=⃥",bnequiv:"≡⃥",bNot:"⫭",bnot:"⌐",Bopf:"𝔹",bopf:"𝕓",bot:"⊥",bottom:"⊥",bowtie:"⋈",boxbox:"⧉",boxDL:"╗",boxDl:"╖",boxdL:"╕",boxdl:"┐",boxDR:"╔",boxDr:"╓",boxdR:"╒",boxdr:"┌",boxH:"═",boxh:"─",boxHD:"╦",boxHd:"╤",boxhD:"╥",boxhd:"┬",boxHU:"╩",boxHu:"╧",boxhU:"╨",boxhu:"┴",boxminus:"⊟",boxplus:"⊞",boxtimes:"⊠",boxUL:"╝",boxUl:"╜",boxuL:"╛",boxul:"┘",boxUR:"╚",boxUr:"╙",boxuR:"╘",boxur:"└",boxV:"║",boxv:"│",boxVH:"╬",boxVh:"╫",boxvH:"╪",boxvh:"┼",boxVL:"╣",boxVl:"╢",boxvL:"╡",boxvl:"┤",boxVR:"╠",boxVr:"╟",boxvR:"╞",boxvr:"├",bprime:"‵",Breve:"˘",breve:"˘",brvbar:"¦",Bscr:"ℬ",bscr:"𝒷",bsemi:"⁏",bsim:"∽",bsime:"⋍",bsol:"\\",bsolb:"⧅",bsolhsub:"⟈",bull:"•",bullet:"•",bump:"≎",bumpE:"⪮",bumpe:"≏",Bumpeq:"≎",bumpeq:"≏",Cacute:"Ć",cacute:"ć",Cap:"⋒",cap:"∩",capand:"⩄",capbrcup:"⩉",capcap:"⩋",capcup:"⩇",capdot:"⩀",CapitalDifferentialD:"ⅅ",caps:"∩︀",caret:"⁁",caron:"ˇ",Cayleys:"ℭ",ccaps:"⩍",Ccaron:"Č",ccaron:"č",Ccedil:"Ç",ccedil:"ç",Ccirc:"Ĉ",ccirc:"ĉ",Cconint:"∰",ccups:"⩌",ccupssm:"⩐",Cdot:"Ċ",cdot:"ċ",cedil:"¸",Cedilla:"¸",cemptyv:"⦲",cent:"¢",CenterDot:"·",centerdot:"·",Cfr:"ℭ",cfr:"𝔠",CHcy:"Ч",chcy:"ч",check:"✓",checkmark:"✓",Chi:"Χ",chi:"χ",cir:"○",circ:"ˆ",circeq:"≗",circlearrowleft:"↺",circlearrowright:"↻",circledast:"⊛",circledcirc:"⊚",circleddash:"⊝",CircleDot:"⊙",circledR:"®",circledS:"Ⓢ",CircleMinus:"⊖",CirclePlus:"⊕",CircleTimes:"⊗",cirE:"⧃",cire:"≗",cirfnint:"⨐",cirmid:"⫯",cirscir:"⧂",ClockwiseContourIntegral:"∲",CloseCurlyDoubleQuote:"”",CloseCurlyQuote:"’",clubs:"♣",clubsuit:"♣",Colon:"∷",colon:":",Colone:"⩴",colone:"≔",coloneq:"≔",comma:",",commat:"@",comp:"∁",compfn:"∘",complement:"∁",complexes:"ℂ",cong:"≅",congdot:"⩭",Congruent:"≡",Conint:"∯",conint:"∮",ContourIntegral:"∮",Copf:"ℂ",copf:"𝕔",coprod:"∐",Coproduct:"∐",COPY:"©",copy:"©",copysr:"℗",CounterClockwiseContourIntegral:"∳",crarr:"↵",Cross:"⨯",cross:"✗",Cscr:"𝒞",cscr:"𝒸",csub:"⫏",csube:"⫑",csup:"⫐",csupe:"⫒",ctdot:"⋯",cudarrl:"⤸",cudarrr:"⤵",cuepr:"⋞",cuesc:"⋟",cularr:"↶",cularrp:"⤽",Cup:"⋓",cup:"∪",cupbrcap:"⩈",CupCap:"≍",cupcap:"⩆",cupcup:"⩊",cupdot:"⊍",cupor:"⩅",cups:"∪︀",curarr:"↷",curarrm:"⤼",curlyeqprec:"⋞",curlyeqsucc:"⋟",curlyvee:"⋎",curlywedge:"⋏",curren:"¤",curvearrowleft:"↶",curvearrowright:"↷",cuvee:"⋎",cuwed:"⋏",cwconint:"∲",cwint:"∱",cylcty:"⌭",Dagger:"‡",dagger:"†",daleth:"ℸ",Darr:"↡",dArr:"⇓",darr:"↓",dash:"‐",Dashv:"⫤",dashv:"⊣",dbkarow:"⤏",dblac:"˝",Dcaron:"Ď",dcaron:"ď",Dcy:"Д",dcy:"д",DD:"ⅅ",dd:"ⅆ",ddagger:"‡",ddarr:"⇊",DDotrahd:"⤑",ddotseq:"⩷",deg:"°",Del:"∇",Delta:"Δ",delta:"δ",demptyv:"⦱",dfisht:"⥿",Dfr:"𝔇",dfr:"𝔡",dHar:"⥥",dharl:"⇃",dharr:"⇂",DiacriticalAcute:"´",DiacriticalDot:"˙",DiacriticalDoubleAcute:"˝",DiacriticalGrave:"`",DiacriticalTilde:"˜",diam:"⋄",Diamond:"⋄",diamond:"⋄",diamondsuit:"♦",diams:"♦",die:"¨",DifferentialD:"ⅆ",digamma:"ϝ",disin:"⋲",div:"÷",divide:"÷",divideontimes:"⋇",divonx:"⋇",DJcy:"Ђ",djcy:"ђ",dlcorn:"⌞",dlcrop:"⌍",dollar:"$",Dopf:"𝔻",dopf:"𝕕",Dot:"¨",dot:"˙",DotDot:"⃜",doteq:"≐",doteqdot:"≑",DotEqual:"≐",dotminus:"∸",dotplus:"∔",dotsquare:"⊡",doublebarwedge:"⌆",DoubleContourIntegral:"∯",DoubleDot:"¨",DoubleDownArrow:"⇓",DoubleLeftArrow:"⇐",DoubleLeftRightArrow:"⇔",DoubleLeftTee:"⫤",DoubleLongLeftArrow:"⟸",DoubleLongLeftRightArrow:"⟺",DoubleLongRightArrow:"⟹",DoubleRightArrow:"⇒",DoubleRightTee:"⊨",DoubleUpArrow:"⇑",DoubleUpDownArrow:"⇕",DoubleVerticalBar:"∥",DownArrow:"↓",Downarrow:"⇓",downarrow:"↓",DownArrowBar:"⤓",DownArrowUpArrow:"⇵",DownBreve:"̑",downdownarrows:"⇊",downharpoonleft:"⇃",downharpoonright:"⇂",DownLeftRightVector:"⥐",DownLeftTeeVector:"⥞",DownLeftVector:"↽",DownLeftVectorBar:"⥖",DownRightTeeVector:"⥟",DownRightVector:"⇁",DownRightVectorBar:"⥗",DownTee:"⊤",DownTeeArrow:"↧",drbkarow:"⤐",drcorn:"⌟",drcrop:"⌌",Dscr:"𝒟",dscr:"𝒹",DScy:"Ѕ",dscy:"ѕ",dsol:"⧶",Dstrok:"Đ",dstrok:"đ",dtdot:"⋱",dtri:"▿",dtrif:"▾",duarr:"⇵",duhar:"⥯",dwangle:"⦦",DZcy:"Џ",dzcy:"џ",dzigrarr:"⟿",Eacute:"É",eacute:"é",easter:"⩮",Ecaron:"Ě",ecaron:"ě",ecir:"≖",Ecirc:"Ê",ecirc:"ê",ecolon:"≕",Ecy:"Э",ecy:"э",eDDot:"⩷",Edot:"Ė",eDot:"≑",edot:"ė",ee:"ⅇ",efDot:"≒",Efr:"𝔈",efr:"𝔢",eg:"⪚",Egrave:"È",egrave:"è",egs:"⪖",egsdot:"⪘",el:"⪙",Element:"∈",elinters:"⏧",ell:"ℓ",els:"⪕",elsdot:"⪗",Emacr:"Ē",emacr:"ē",empty:"∅",emptyset:"∅",EmptySmallSquare:"◻",emptyv:"∅",EmptyVerySmallSquare:"▫",emsp:" ",emsp13:" ",emsp14:" ",ENG:"Ŋ",eng:"ŋ",ensp:" ",Eogon:"Ę",eogon:"ę",Eopf:"𝔼",eopf:"𝕖",epar:"⋕",eparsl:"⧣",eplus:"⩱",epsi:"ε",Epsilon:"Ε",epsilon:"ε",epsiv:"ϵ",eqcirc:"≖",eqcolon:"≕",eqsim:"≂",eqslantgtr:"⪖",eqslantless:"⪕",Equal:"⩵",equals:"=",EqualTilde:"≂",equest:"≟",Equilibrium:"⇌",equiv:"≡",equivDD:"⩸",eqvparsl:"⧥",erarr:"⥱",erDot:"≓",Escr:"ℰ",escr:"ℯ",esdot:"≐",Esim:"⩳",esim:"≂",Eta:"Η",eta:"η",ETH:"Ð",eth:"ð",Euml:"Ë",euml:"ë",euro:"€",excl:"!",exist:"∃",Exists:"∃",expectation:"ℰ",ExponentialE:"ⅇ",exponentiale:"ⅇ",fallingdotseq:"≒",Fcy:"Ф",fcy:"ф",female:"♀",ffilig:"ffi",fflig:"ff",ffllig:"ffl",Ffr:"𝔉",ffr:"𝔣",filig:"fi",FilledSmallSquare:"◼",FilledVerySmallSquare:"▪",fjlig:"fj",flat:"♭",fllig:"fl",fltns:"▱",fnof:"ƒ",Fopf:"𝔽",fopf:"𝕗",ForAll:"∀",forall:"∀",fork:"⋔",forkv:"⫙",Fouriertrf:"ℱ",fpartint:"⨍",frac12:"½",frac13:"⅓",frac14:"¼",frac15:"⅕",frac16:"⅙",frac18:"⅛",frac23:"⅔",frac25:"⅖",frac34:"¾",frac35:"⅗",frac38:"⅜",frac45:"⅘",frac56:"⅚",frac58:"⅝",frac78:"⅞",frasl:"⁄",frown:"⌢",Fscr:"ℱ",fscr:"𝒻",gacute:"ǵ",Gamma:"Γ",gamma:"γ",Gammad:"Ϝ",gammad:"ϝ",gap:"⪆",Gbreve:"Ğ",gbreve:"ğ",Gcedil:"Ģ",Gcirc:"Ĝ",gcirc:"ĝ",Gcy:"Г",gcy:"г",Gdot:"Ġ",gdot:"ġ",gE:"≧",ge:"≥",gEl:"⪌",gel:"⋛",geq:"≥",geqq:"≧",geqslant:"⩾",ges:"⩾",gescc:"⪩",gesdot:"⪀",gesdoto:"⪂",gesdotol:"⪄",gesl:"⋛︀",gesles:"⪔",Gfr:"𝔊",gfr:"𝔤",Gg:"⋙",gg:"≫",ggg:"⋙",gimel:"ℷ",GJcy:"Ѓ",gjcy:"ѓ",gl:"≷",gla:"⪥",glE:"⪒",glj:"⪤",gnap:"⪊",gnapprox:"⪊",gnE:"≩",gne:"⪈",gneq:"⪈",gneqq:"≩",gnsim:"⋧",Gopf:"𝔾",gopf:"𝕘",grave:"`",GreaterEqual:"≥",GreaterEqualLess:"⋛",GreaterFullEqual:"≧",GreaterGreater:"⪢",GreaterLess:"≷",GreaterSlantEqual:"⩾",GreaterTilde:"≳",Gscr:"𝒢",gscr:"ℊ",gsim:"≳",gsime:"⪎",gsiml:"⪐",GT:">",Gt:"≫",gt:">",gtcc:"⪧",gtcir:"⩺",gtdot:"⋗",gtlPar:"⦕",gtquest:"⩼",gtrapprox:"⪆",gtrarr:"⥸",gtrdot:"⋗",gtreqless:"⋛",gtreqqless:"⪌",gtrless:"≷",gtrsim:"≳",gvertneqq:"≩︀",gvnE:"≩︀",Hacek:"ˇ",hairsp:" ",half:"½",hamilt:"ℋ",HARDcy:"Ъ",hardcy:"ъ",hArr:"⇔",harr:"↔",harrcir:"⥈",harrw:"↭",Hat:"^",hbar:"ℏ",Hcirc:"Ĥ",hcirc:"ĥ",hearts:"♥",heartsuit:"♥",hellip:"…",hercon:"⊹",Hfr:"ℌ",hfr:"𝔥",HilbertSpace:"ℋ",hksearow:"⤥",hkswarow:"⤦",hoarr:"⇿",homtht:"∻",hookleftarrow:"↩",hookrightarrow:"↪",Hopf:"ℍ",hopf:"𝕙",horbar:"―",HorizontalLine:"─",Hscr:"ℋ",hscr:"𝒽",hslash:"ℏ",Hstrok:"Ħ",hstrok:"ħ",HumpDownHump:"≎",HumpEqual:"≏",hybull:"⁃",hyphen:"‐",Iacute:"Í",iacute:"í",ic:"\u2063",Icirc:"Î",icirc:"î",Icy:"И",icy:"и",Idot:"İ",IEcy:"Е",iecy:"е",iexcl:"¡",iff:"⇔",Ifr:"ℑ",ifr:"𝔦",Igrave:"Ì",igrave:"ì",ii:"ⅈ",iiiint:"⨌",iiint:"∭",iinfin:"⧜",iiota:"℩",IJlig:"IJ",ijlig:"ij",Im:"ℑ",Imacr:"Ī",imacr:"ī",image:"ℑ",ImaginaryI:"ⅈ",imagline:"ℐ",imagpart:"ℑ",imath:"ı",imof:"⊷",imped:"Ƶ",Implies:"⇒",in:"∈",incare:"℅",infin:"∞",infintie:"⧝",inodot:"ı",Int:"∬",int:"∫",intcal:"⊺",integers:"ℤ",Integral:"∫",intercal:"⊺",Intersection:"⋂",intlarhk:"⨗",intprod:"⨼",InvisibleComma:"\u2063",InvisibleTimes:"\u2062",IOcy:"Ё",iocy:"ё",Iogon:"Į",iogon:"į",Iopf:"𝕀",iopf:"𝕚",Iota:"Ι",iota:"ι",iprod:"⨼",iquest:"¿",Iscr:"ℐ",iscr:"𝒾",isin:"∈",isindot:"⋵",isinE:"⋹",isins:"⋴",isinsv:"⋳",isinv:"∈",it:"\u2062",Itilde:"Ĩ",itilde:"ĩ",Iukcy:"І",iukcy:"і",Iuml:"Ï",iuml:"ï",Jcirc:"Ĵ",jcirc:"ĵ",Jcy:"Й",jcy:"й",Jfr:"𝔍",jfr:"𝔧",jmath:"ȷ",Jopf:"𝕁",jopf:"𝕛",Jscr:"𝒥",jscr:"𝒿",Jsercy:"Ј",jsercy:"ј",Jukcy:"Є",jukcy:"є",Kappa:"Κ",kappa:"κ",kappav:"ϰ",Kcedil:"Ķ",kcedil:"ķ",Kcy:"К",kcy:"к",Kfr:"𝔎",kfr:"𝔨",kgreen:"ĸ",KHcy:"Х",khcy:"х",KJcy:"Ќ",kjcy:"ќ",Kopf:"𝕂",kopf:"𝕜",Kscr:"𝒦",kscr:"𝓀",lAarr:"⇚",Lacute:"Ĺ",lacute:"ĺ",laemptyv:"⦴",lagran:"ℒ",Lambda:"Λ",lambda:"λ",Lang:"⟪",lang:"⟨",langd:"⦑",langle:"⟨",lap:"⪅",Laplacetrf:"ℒ",laquo:"«",Larr:"↞",lArr:"⇐",larr:"←",larrb:"⇤",larrbfs:"⤟",larrfs:"⤝",larrhk:"↩",larrlp:"↫",larrpl:"⤹",larrsim:"⥳",larrtl:"↢",lat:"⪫",lAtail:"⤛",latail:"⤙",late:"⪭",lates:"⪭︀",lBarr:"⤎",lbarr:"⤌",lbbrk:"❲",lbrace:"{",lbrack:"[",lbrke:"⦋",lbrksld:"⦏",lbrkslu:"⦍",Lcaron:"Ľ",lcaron:"ľ",Lcedil:"Ļ",lcedil:"ļ",lceil:"⌈",lcub:"{",Lcy:"Л",lcy:"л",ldca:"⤶",ldquo:"“",ldquor:"„",ldrdhar:"⥧",ldrushar:"⥋",ldsh:"↲",lE:"≦",le:"≤",LeftAngleBracket:"⟨",LeftArrow:"←",Leftarrow:"⇐",leftarrow:"←",LeftArrowBar:"⇤",LeftArrowRightArrow:"⇆",leftarrowtail:"↢",LeftCeiling:"⌈",LeftDoubleBracket:"⟦",LeftDownTeeVector:"⥡",LeftDownVector:"⇃",LeftDownVectorBar:"⥙",LeftFloor:"⌊",leftharpoondown:"↽",leftharpoonup:"↼",leftleftarrows:"⇇",LeftRightArrow:"↔",Leftrightarrow:"⇔",leftrightarrow:"↔",leftrightarrows:"⇆",leftrightharpoons:"⇋",leftrightsquigarrow:"↭",LeftRightVector:"⥎",LeftTee:"⊣",LeftTeeArrow:"↤",LeftTeeVector:"⥚",leftthreetimes:"⋋",LeftTriangle:"⊲",LeftTriangleBar:"⧏",LeftTriangleEqual:"⊴",LeftUpDownVector:"⥑",LeftUpTeeVector:"⥠",LeftUpVector:"↿",LeftUpVectorBar:"⥘",LeftVector:"↼",LeftVectorBar:"⥒",lEg:"⪋",leg:"⋚",leq:"≤",leqq:"≦",leqslant:"⩽",les:"⩽",lescc:"⪨",lesdot:"⩿",lesdoto:"⪁",lesdotor:"⪃",lesg:"⋚︀",lesges:"⪓",lessapprox:"⪅",lessdot:"⋖",lesseqgtr:"⋚",lesseqqgtr:"⪋",LessEqualGreater:"⋚",LessFullEqual:"≦",LessGreater:"≶",lessgtr:"≶",LessLess:"⪡",lesssim:"≲",LessSlantEqual:"⩽",LessTilde:"≲",lfisht:"⥼",lfloor:"⌊",Lfr:"𝔏",lfr:"𝔩",lg:"≶",lgE:"⪑",lHar:"⥢",lhard:"↽",lharu:"↼",lharul:"⥪",lhblk:"▄",LJcy:"Љ",ljcy:"љ",Ll:"⋘",ll:"≪",llarr:"⇇",llcorner:"⌞",Lleftarrow:"⇚",llhard:"⥫",lltri:"◺",Lmidot:"Ŀ",lmidot:"ŀ",lmoust:"⎰",lmoustache:"⎰",lnap:"⪉",lnapprox:"⪉",lnE:"≨",lne:"⪇",lneq:"⪇",lneqq:"≨",lnsim:"⋦",loang:"⟬",loarr:"⇽",lobrk:"⟦",LongLeftArrow:"⟵",Longleftarrow:"⟸",longleftarrow:"⟵",LongLeftRightArrow:"⟷",Longleftrightarrow:"⟺",longleftrightarrow:"⟷",longmapsto:"⟼",LongRightArrow:"⟶",Longrightarrow:"⟹",longrightarrow:"⟶",looparrowleft:"↫",looparrowright:"↬",lopar:"⦅",Lopf:"𝕃",lopf:"𝕝",loplus:"⨭",lotimes:"⨴",lowast:"∗",lowbar:"_",LowerLeftArrow:"↙",LowerRightArrow:"↘",loz:"◊",lozenge:"◊",lozf:"⧫",lpar:"(",lparlt:"⦓",lrarr:"⇆",lrcorner:"⌟",lrhar:"⇋",lrhard:"⥭",lrm:"\u200e",lrtri:"⊿",lsaquo:"‹",Lscr:"ℒ",lscr:"𝓁",Lsh:"↰",lsh:"↰",lsim:"≲",lsime:"⪍",lsimg:"⪏",lsqb:"[",lsquo:"‘",lsquor:"‚",Lstrok:"Ł",lstrok:"ł",LT:"<",Lt:"≪",lt:"<",ltcc:"⪦",ltcir:"⩹",ltdot:"⋖",lthree:"⋋",ltimes:"⋉",ltlarr:"⥶",ltquest:"⩻",ltri:"◃",ltrie:"⊴",ltrif:"◂",ltrPar:"⦖",lurdshar:"⥊",luruhar:"⥦",lvertneqq:"≨︀",lvnE:"≨︀",macr:"¯",male:"♂",malt:"✠",maltese:"✠",Map:"⤅",map:"↦",mapsto:"↦",mapstodown:"↧",mapstoleft:"↤",mapstoup:"↥",marker:"▮",mcomma:"⨩",Mcy:"М",mcy:"м",mdash:"—",mDDot:"∺",measuredangle:"∡",MediumSpace:" ",Mellintrf:"ℳ",Mfr:"𝔐",mfr:"𝔪",mho:"℧",micro:"µ",mid:"∣",midast:"*",midcir:"⫰",middot:"·",minus:"−",minusb:"⊟",minusd:"∸",minusdu:"⨪",MinusPlus:"∓",mlcp:"⫛",mldr:"…",mnplus:"∓",models:"⊧",Mopf:"𝕄",mopf:"𝕞",mp:"∓",Mscr:"ℳ",mscr:"𝓂",mstpos:"∾",Mu:"Μ",mu:"μ",multimap:"⊸",mumap:"⊸",nabla:"∇",Nacute:"Ń",nacute:"ń",nang:"∠⃒",nap:"≉",napE:"⩰̸",napid:"≋̸",napos:"ʼn",napprox:"≉",natur:"♮",natural:"♮",naturals:"ℕ",nbsp:" ",nbump:"≎̸",nbumpe:"≏̸",ncap:"⩃",Ncaron:"Ň",ncaron:"ň",Ncedil:"Ņ",ncedil:"ņ",ncong:"≇",ncongdot:"⩭̸",ncup:"⩂",Ncy:"Н",ncy:"н",ndash:"–",ne:"≠",nearhk:"⤤",neArr:"⇗",nearr:"↗",nearrow:"↗",nedot:"≐̸",NegativeMediumSpace:"​",NegativeThickSpace:"​",NegativeThinSpace:"​",NegativeVeryThinSpace:"​",nequiv:"≢",nesear:"⤨",nesim:"≂̸",NestedGreaterGreater:"≫",NestedLessLess:"≪",NewLine:"\u000a",nexist:"∄",nexists:"∄",Nfr:"𝔑",nfr:"𝔫",ngE:"≧̸",nge:"≱",ngeq:"≱",ngeqq:"≧̸",ngeqslant:"⩾̸",nges:"⩾̸",nGg:"⋙̸",ngsim:"≵",nGt:"≫⃒",ngt:"≯",ngtr:"≯",nGtv:"≫̸",nhArr:"⇎",nharr:"↮",nhpar:"⫲",ni:"∋",nis:"⋼",nisd:"⋺",niv:"∋",NJcy:"Њ",njcy:"њ",nlArr:"⇍",nlarr:"↚",nldr:"‥",nlE:"≦̸",nle:"≰",nLeftarrow:"⇍",nleftarrow:"↚",nLeftrightarrow:"⇎",nleftrightarrow:"↮",nleq:"≰",nleqq:"≦̸",nleqslant:"⩽̸",nles:"⩽̸",nless:"≮",nLl:"⋘̸",nlsim:"≴",nLt:"≪⃒",nlt:"≮",nltri:"⋪",nltrie:"⋬",nLtv:"≪̸",nmid:"∤",NoBreak:"\u2060",NonBreakingSpace:" ",Nopf:"ℕ",nopf:"𝕟",Not:"⫬",not:"¬",NotCongruent:"≢",NotCupCap:"≭",NotDoubleVerticalBar:"∦",NotElement:"∉",NotEqual:"≠",NotEqualTilde:"≂̸",NotExists:"∄",NotGreater:"≯",NotGreaterEqual:"≱",NotGreaterFullEqual:"≧̸",NotGreaterGreater:"≫̸",NotGreaterLess:"≹",NotGreaterSlantEqual:"⩾̸",NotGreaterTilde:"≵",NotHumpDownHump:"≎̸",NotHumpEqual:"≏̸",notin:"∉",notindot:"⋵̸",notinE:"⋹̸",notinva:"∉",notinvb:"⋷",notinvc:"⋶",NotLeftTriangle:"⋪",NotLeftTriangleBar:"⧏̸",NotLeftTriangleEqual:"⋬",NotLess:"≮",NotLessEqual:"≰",NotLessGreater:"≸",NotLessLess:"≪̸",NotLessSlantEqual:"⩽̸",NotLessTilde:"≴",NotNestedGreaterGreater:"⪢̸",NotNestedLessLess:"⪡̸",notni:"∌",notniva:"∌",notnivb:"⋾",notnivc:"⋽",NotPrecedes:"⊀",NotPrecedesEqual:"⪯̸",NotPrecedesSlantEqual:"⋠",NotReverseElement:"∌",NotRightTriangle:"⋫",NotRightTriangleBar:"⧐̸",NotRightTriangleEqual:"⋭",NotSquareSubset:"⊏̸",NotSquareSubsetEqual:"⋢",NotSquareSuperset:"⊐̸",NotSquareSupersetEqual:"⋣",NotSubset:"⊂⃒",NotSubsetEqual:"⊈",NotSucceeds:"⊁",NotSucceedsEqual:"⪰̸",NotSucceedsSlantEqual:"⋡",NotSucceedsTilde:"≿̸",NotSuperset:"⊃⃒",NotSupersetEqual:"⊉",NotTilde:"≁",NotTildeEqual:"≄",NotTildeFullEqual:"≇",NotTildeTilde:"≉",NotVerticalBar:"∤",npar:"∦",nparallel:"∦",nparsl:"⫽⃥",npart:"∂̸",npolint:"⨔",npr:"⊀",nprcue:"⋠",npre:"⪯̸",nprec:"⊀",npreceq:"⪯̸",nrArr:"⇏",nrarr:"↛",nrarrc:"⤳̸",nrarrw:"↝̸",nRightarrow:"⇏",nrightarrow:"↛",nrtri:"⋫",nrtrie:"⋭",nsc:"⊁",nsccue:"⋡",nsce:"⪰̸",Nscr:"𝒩",nscr:"𝓃",nshortmid:"∤",nshortparallel:"∦",nsim:"≁",nsime:"≄",nsimeq:"≄",nsmid:"∤",nspar:"∦",nsqsube:"⋢",nsqsupe:"⋣",nsub:"⊄",nsubE:"⫅̸",nsube:"⊈",nsubset:"⊂⃒",nsubseteq:"⊈",nsubseteqq:"⫅̸",nsucc:"⊁",nsucceq:"⪰̸",nsup:"⊅",nsupE:"⫆̸",nsupe:"⊉",nsupset:"⊃⃒",nsupseteq:"⊉",nsupseteqq:"⫆̸",ntgl:"≹",Ntilde:"Ñ",ntilde:"ñ",ntlg:"≸",ntriangleleft:"⋪",ntrianglelefteq:"⋬",ntriangleright:"⋫",ntrianglerighteq:"⋭",Nu:"Ν",nu:"ν",num:"#",numero:"№",numsp:" ",nvap:"≍⃒",nVDash:"⊯",nVdash:"⊮",nvDash:"⊭",nvdash:"⊬",nvge:"≥⃒",nvgt:">⃒",nvHarr:"⤄",nvinfin:"⧞",nvlArr:"⤂",nvle:"≤⃒",nvlt:"<⃒",nvltrie:"⊴⃒",nvrArr:"⤃",nvrtrie:"⊵⃒",nvsim:"∼⃒",nwarhk:"⤣",nwArr:"⇖",nwarr:"↖",nwarrow:"↖",nwnear:"⤧",Oacute:"Ó",oacute:"ó",oast:"⊛",ocir:"⊚",Ocirc:"Ô",ocirc:"ô",Ocy:"О",ocy:"о",odash:"⊝",Odblac:"Ő",odblac:"ő",odiv:"⨸",odot:"⊙",odsold:"⦼",OElig:"Œ",oelig:"œ",ofcir:"⦿",Ofr:"𝔒",ofr:"𝔬",ogon:"˛",Ograve:"Ò",ograve:"ò",ogt:"⧁",ohbar:"⦵",ohm:"Ω",oint:"∮",olarr:"↺",olcir:"⦾",olcross:"⦻",oline:"‾",olt:"⧀",Omacr:"Ō",omacr:"ō",Omega:"Ω",omega:"ω",Omicron:"Ο",omicron:"ο",omid:"⦶",ominus:"⊖",Oopf:"𝕆",oopf:"𝕠",opar:"⦷",OpenCurlyDoubleQuote:"“",OpenCurlyQuote:"‘",operp:"⦹",oplus:"⊕",Or:"⩔",or:"∨",orarr:"↻",ord:"⩝",order:"ℴ",orderof:"ℴ",ordf:"ª",ordm:"º",origof:"⊶",oror:"⩖",orslope:"⩗",orv:"⩛",oS:"Ⓢ",Oscr:"𝒪",oscr:"ℴ",Oslash:"Ø",oslash:"ø",osol:"⊘",Otilde:"Õ",otilde:"õ",Otimes:"⨷",otimes:"⊗",otimesas:"⨶",Ouml:"Ö",ouml:"ö",ovbar:"⌽",OverBar:"‾",OverBrace:"⏞",OverBracket:"⎴",OverParenthesis:"⏜",par:"∥",para:"¶",parallel:"∥",parsim:"⫳",parsl:"⫽",part:"∂",PartialD:"∂",Pcy:"П",pcy:"п",percnt:"%",period:".",permil:"‰",perp:"⊥",pertenk:"‱",Pfr:"𝔓",pfr:"𝔭",Phi:"Φ",phi:"φ",phiv:"ϕ",phmmat:"ℳ",phone:"☎",Pi:"Π",pi:"π",pitchfork:"⋔",piv:"ϖ",planck:"ℏ",planckh:"ℎ",plankv:"ℏ",plus:"+",plusacir:"⨣",plusb:"⊞",pluscir:"⨢",plusdo:"∔",plusdu:"⨥",pluse:"⩲",PlusMinus:"±",plusmn:"±",plussim:"⨦",plustwo:"⨧",pm:"±",Poincareplane:"ℌ",pointint:"⨕",Popf:"ℙ",popf:"𝕡",pound:"£",Pr:"⪻",pr:"≺",prap:"⪷",prcue:"≼",prE:"⪳",pre:"⪯",prec:"≺",precapprox:"⪷",preccurlyeq:"≼",Precedes:"≺",PrecedesEqual:"⪯",PrecedesSlantEqual:"≼",PrecedesTilde:"≾",preceq:"⪯",precnapprox:"⪹",precneqq:"⪵",precnsim:"⋨",precsim:"≾",Prime:"″",prime:"′",primes:"ℙ",prnap:"⪹",prnE:"⪵",prnsim:"⋨",prod:"∏",Product:"∏",profalar:"⌮",profline:"⌒",profsurf:"⌓",prop:"∝",Proportion:"∷",Proportional:"∝",propto:"∝",prsim:"≾",prurel:"⊰",Pscr:"𝒫",pscr:"𝓅",Psi:"Ψ",psi:"ψ",puncsp:" ",Qfr:"𝔔",qfr:"𝔮",qint:"⨌",Qopf:"ℚ",qopf:"𝕢",qprime:"⁗",Qscr:"𝒬",qscr:"𝓆",quaternions:"ℍ",quatint:"⨖",quest:"?",questeq:"≟",QUOT:"\"",quot:"\"",rAarr:"⇛",race:"∽̱",Racute:"Ŕ",racute:"ŕ",radic:"√",raemptyv:"⦳",Rang:"⟫",rang:"⟩",rangd:"⦒",range:"⦥",rangle:"⟩",raquo:"»",Rarr:"↠",rArr:"⇒",rarr:"→",rarrap:"⥵",rarrb:"⇥",rarrbfs:"⤠",rarrc:"⤳",rarrfs:"⤞",rarrhk:"↪",rarrlp:"↬",rarrpl:"⥅",rarrsim:"⥴",Rarrtl:"⤖",rarrtl:"↣",rarrw:"↝",rAtail:"⤜",ratail:"⤚",ratio:"∶",rationals:"ℚ",RBarr:"⤐",rBarr:"⤏",rbarr:"⤍",rbbrk:"❳",rbrace:"}",rbrack:"]",rbrke:"⦌",rbrksld:"⦎",rbrkslu:"⦐",Rcaron:"Ř",rcaron:"ř",Rcedil:"Ŗ",rcedil:"ŗ",rceil:"⌉",rcub:"}",Rcy:"Р",rcy:"р",rdca:"⤷",rdldhar:"⥩",rdquo:"”",rdquor:"”",rdsh:"↳",Re:"ℜ",real:"ℜ",realine:"ℛ",realpart:"ℜ",reals:"ℝ",rect:"▭",REG:"®",reg:"®",ReverseElement:"∋",ReverseEquilibrium:"⇋",ReverseUpEquilibrium:"⥯",rfisht:"⥽",rfloor:"⌋",Rfr:"ℜ",rfr:"𝔯",rHar:"⥤",rhard:"⇁",rharu:"⇀",rharul:"⥬",Rho:"Ρ",rho:"ρ",rhov:"ϱ",RightAngleBracket:"⟩",RightArrow:"→",Rightarrow:"⇒",rightarrow:"→",RightArrowBar:"⇥",RightArrowLeftArrow:"⇄",rightarrowtail:"↣",RightCeiling:"⌉",RightDoubleBracket:"⟧",RightDownTeeVector:"⥝",RightDownVector:"⇂",RightDownVectorBar:"⥕",RightFloor:"⌋",rightharpoondown:"⇁",rightharpoonup:"⇀",rightleftarrows:"⇄",rightleftharpoons:"⇌",rightrightarrows:"⇉",rightsquigarrow:"↝",RightTee:"⊢",RightTeeArrow:"↦",RightTeeVector:"⥛",rightthreetimes:"⋌",RightTriangle:"⊳",RightTriangleBar:"⧐",RightTriangleEqual:"⊵",RightUpDownVector:"⥏",RightUpTeeVector:"⥜",RightUpVector:"↾",RightUpVectorBar:"⥔",RightVector:"⇀",RightVectorBar:"⥓",ring:"˚",risingdotseq:"≓",rlarr:"⇄",rlhar:"⇌",rlm:"\u200f",rmoust:"⎱",rmoustache:"⎱",rnmid:"⫮",roang:"⟭",roarr:"⇾",robrk:"⟧",ropar:"⦆",Ropf:"ℝ",ropf:"𝕣",roplus:"⨮",rotimes:"⨵",RoundImplies:"⥰",rpar:")",rpargt:"⦔",rppolint:"⨒",rrarr:"⇉",Rrightarrow:"⇛",rsaquo:"›",Rscr:"ℛ",rscr:"𝓇",Rsh:"↱",rsh:"↱",rsqb:"]",rsquo:"’",rsquor:"’",rthree:"⋌",rtimes:"⋊",rtri:"▹",rtrie:"⊵",rtrif:"▸",rtriltri:"⧎",RuleDelayed:"⧴",ruluhar:"⥨",rx:"℞",Sacute:"Ś",sacute:"ś",sbquo:"‚",Sc:"⪼",sc:"≻",scap:"⪸",Scaron:"Š",scaron:"š",sccue:"≽",scE:"⪴",sce:"⪰",Scedil:"Ş",scedil:"ş",Scirc:"Ŝ",scirc:"ŝ",scnap:"⪺",scnE:"⪶",scnsim:"⋩",scpolint:"⨓",scsim:"≿",Scy:"С",scy:"с",sdot:"⋅",sdotb:"⊡",sdote:"⩦",searhk:"⤥",seArr:"⇘",searr:"↘",searrow:"↘",sect:"§",semi:";",seswar:"⤩",setminus:"∖",setmn:"∖",sext:"✶",Sfr:"𝔖",sfr:"𝔰",sfrown:"⌢",sharp:"♯",SHCHcy:"Щ",shchcy:"щ",SHcy:"Ш",shcy:"ш",ShortDownArrow:"↓",ShortLeftArrow:"←",shortmid:"∣",shortparallel:"∥",ShortRightArrow:"→",ShortUpArrow:"↑",shy:"\u00ad",Sigma:"Σ",sigma:"σ",sigmaf:"ς",sigmav:"ς",sim:"∼",simdot:"⩪",sime:"≃",simeq:"≃",simg:"⪞",simgE:"⪠",siml:"⪝",simlE:"⪟",simne:"≆",simplus:"⨤",simrarr:"⥲",slarr:"←",SmallCircle:"∘",smallsetminus:"∖",smashp:"⨳",smeparsl:"⧤",smid:"∣",smile:"⌣",smt:"⪪",smte:"⪬",smtes:"⪬︀",SOFTcy:"Ь",softcy:"ь",sol:"/",solb:"⧄",solbar:"⌿",Sopf:"𝕊",sopf:"𝕤",spades:"♠",spadesuit:"♠",spar:"∥",sqcap:"⊓",sqcaps:"⊓︀",sqcup:"⊔",sqcups:"⊔︀",Sqrt:"√",sqsub:"⊏",sqsube:"⊑",sqsubset:"⊏",sqsubseteq:"⊑",sqsup:"⊐",sqsupe:"⊒",sqsupset:"⊐",sqsupseteq:"⊒",squ:"□",Square:"□",square:"□",SquareIntersection:"⊓",SquareSubset:"⊏",SquareSubsetEqual:"⊑",SquareSuperset:"⊐",SquareSupersetEqual:"⊒",SquareUnion:"⊔",squarf:"▪",squf:"▪",srarr:"→",Sscr:"𝒮",sscr:"𝓈",ssetmn:"∖",ssmile:"⌣",sstarf:"⋆",Star:"⋆",star:"☆",starf:"★",straightepsilon:"ϵ",straightphi:"ϕ",strns:"¯",Sub:"⋐",sub:"⊂",subdot:"⪽",subE:"⫅",sube:"⊆",subedot:"⫃",submult:"⫁",subnE:"⫋",subne:"⊊",subplus:"⪿",subrarr:"⥹",Subset:"⋐",subset:"⊂",subseteq:"⊆",subseteqq:"⫅",SubsetEqual:"⊆",subsetneq:"⊊",subsetneqq:"⫋",subsim:"⫇",subsub:"⫕",subsup:"⫓",succ:"≻",succapprox:"⪸",succcurlyeq:"≽",Succeeds:"≻",SucceedsEqual:"⪰",SucceedsSlantEqual:"≽",SucceedsTilde:"≿",succeq:"⪰",succnapprox:"⪺",succneqq:"⪶",succnsim:"⋩",succsim:"≿",SuchThat:"∋",Sum:"∑",sum:"∑",sung:"♪",Sup:"⋑",sup:"⊃",sup1:"¹",sup2:"²",sup3:"³",supdot:"⪾",supdsub:"⫘",supE:"⫆",supe:"⊇",supedot:"⫄",Superset:"⊃",SupersetEqual:"⊇",suphsol:"⟉",suphsub:"⫗",suplarr:"⥻",supmult:"⫂",supnE:"⫌",supne:"⊋",supplus:"⫀",Supset:"⋑",supset:"⊃",supseteq:"⊇",supseteqq:"⫆",supsetneq:"⊋",supsetneqq:"⫌",supsim:"⫈",supsub:"⫔",supsup:"⫖",swarhk:"⤦",swArr:"⇙",swarr:"↙",swarrow:"↙",swnwar:"⤪",szlig:"ß",Tab:"\u0009",target:"⌖",Tau:"Τ",tau:"τ",tbrk:"⎴",Tcaron:"Ť",tcaron:"ť",Tcedil:"Ţ",tcedil:"ţ",Tcy:"Т",tcy:"т",tdot:"⃛",telrec:"⌕",Tfr:"𝔗",tfr:"𝔱",there4:"∴",Therefore:"∴",therefore:"∴",Theta:"Θ",theta:"θ",thetasym:"ϑ",thetav:"ϑ",thickapprox:"≈",thicksim:"∼",ThickSpace:"  ",thinsp:" ",ThinSpace:" ",thkap:"≈",thksim:"∼",THORN:"Þ",thorn:"þ",Tilde:"∼",tilde:"˜",TildeEqual:"≃",TildeFullEqual:"≅",TildeTilde:"≈",times:"×",timesb:"⊠",timesbar:"⨱",timesd:"⨰",tint:"∭",toea:"⤨",top:"⊤",topbot:"⌶",topcir:"⫱",Topf:"𝕋",topf:"𝕥",topfork:"⫚",tosa:"⤩",tprime:"‴",TRADE:"™",trade:"™",triangle:"▵",triangledown:"▿",triangleleft:"◃",trianglelefteq:"⊴",triangleq:"≜",triangleright:"▹",trianglerighteq:"⊵",tridot:"◬",trie:"≜",triminus:"⨺",TripleDot:"⃛",triplus:"⨹",trisb:"⧍",tritime:"⨻",trpezium:"⏢",Tscr:"𝒯",tscr:"𝓉",TScy:"Ц",tscy:"ц",TSHcy:"Ћ",tshcy:"ћ",Tstrok:"Ŧ",tstrok:"ŧ",twixt:"≬",twoheadleftarrow:"↞",twoheadrightarrow:"↠",Uacute:"Ú",uacute:"ú",Uarr:"↟",uArr:"⇑",uarr:"↑",Uarrocir:"⥉",Ubrcy:"Ў",ubrcy:"ў",Ubreve:"Ŭ",ubreve:"ŭ",Ucirc:"Û",ucirc:"û",Ucy:"У",ucy:"у",udarr:"⇅",Udblac:"Ű",udblac:"ű",udhar:"⥮",ufisht:"⥾",Ufr:"𝔘",ufr:"𝔲",Ugrave:"Ù",ugrave:"ù",uHar:"⥣",uharl:"↿",uharr:"↾",uhblk:"▀",ulcorn:"⌜",ulcorner:"⌜",ulcrop:"⌏",ultri:"◸",Umacr:"Ū",umacr:"ū",uml:"¨",UnderBar:"_",UnderBrace:"⏟",UnderBracket:"⎵",UnderParenthesis:"⏝",Union:"⋃",UnionPlus:"⊎",Uogon:"Ų",uogon:"ų",Uopf:"𝕌",uopf:"𝕦",UpArrow:"↑",Uparrow:"⇑",uparrow:"↑",UpArrowBar:"⤒",UpArrowDownArrow:"⇅",UpDownArrow:"↕",Updownarrow:"⇕",updownarrow:"↕",UpEquilibrium:"⥮",upharpoonleft:"↿",upharpoonright:"↾",uplus:"⊎",UpperLeftArrow:"↖",UpperRightArrow:"↗",Upsi:"ϒ",upsi:"υ",upsih:"ϒ",Upsilon:"Υ",upsilon:"υ",UpTee:"⊥",UpTeeArrow:"↥",upuparrows:"⇈",urcorn:"⌝",urcorner:"⌝",urcrop:"⌎",Uring:"Ů",uring:"ů",urtri:"◹",Uscr:"𝒰",uscr:"𝓊",utdot:"⋰",Utilde:"Ũ",utilde:"ũ",utri:"▵",utrif:"▴",uuarr:"⇈",Uuml:"Ü",uuml:"ü",uwangle:"⦧",vangrt:"⦜",varepsilon:"ϵ",varkappa:"ϰ",varnothing:"∅",varphi:"ϕ",varpi:"ϖ",varpropto:"∝",vArr:"⇕",varr:"↕",varrho:"ϱ",varsigma:"ς",varsubsetneq:"⊊︀",varsubsetneqq:"⫋︀",varsupsetneq:"⊋︀",varsupsetneqq:"⫌︀",vartheta:"ϑ",vartriangleleft:"⊲",vartriangleright:"⊳",Vbar:"⫫",vBar:"⫨",vBarv:"⫩",Vcy:"В",vcy:"в",VDash:"⊫",Vdash:"⊩",vDash:"⊨",vdash:"⊢",Vdashl:"⫦",Vee:"⋁",vee:"∨",veebar:"⊻",veeeq:"≚",vellip:"⋮",Verbar:"‖",verbar:"|",Vert:"‖",vert:"|",VerticalBar:"∣",VerticalLine:"|",VerticalSeparator:"❘",VerticalTilde:"≀",VeryThinSpace:" ",Vfr:"𝔙",vfr:"𝔳",vltri:"⊲",vnsub:"⊂⃒",vnsup:"⊃⃒",Vopf:"𝕍",vopf:"𝕧",vprop:"∝",vrtri:"⊳",Vscr:"𝒱",vscr:"𝓋",vsubnE:"⫋︀",vsubne:"⊊︀",vsupnE:"⫌︀",vsupne:"⊋︀",Vvdash:"⊪",vzigzag:"⦚",Wcirc:"Ŵ",wcirc:"ŵ",wedbar:"⩟",Wedge:"⋀",wedge:"∧",wedgeq:"≙",weierp:"℘",Wfr:"𝔚",wfr:"𝔴",Wopf:"𝕎",wopf:"𝕨",wp:"℘",wr:"≀",wreath:"≀",Wscr:"𝒲",wscr:"𝓌",xcap:"⋂",xcirc:"◯",xcup:"⋃",xdtri:"▽",Xfr:"𝔛",xfr:"𝔵",xhArr:"⟺",xharr:"⟷",Xi:"Ξ",xi:"ξ",xlArr:"⟸",xlarr:"⟵",xmap:"⟼",xnis:"⋻",xodot:"⨀",Xopf:"𝕏",xopf:"𝕩",xoplus:"⨁",xotime:"⨂",xrArr:"⟹",xrarr:"⟶",Xscr:"𝒳",xscr:"𝓍",xsqcup:"⨆",xuplus:"⨄",xutri:"△",xvee:"⋁",xwedge:"⋀",Yacute:"Ý",yacute:"ý",YAcy:"Я",yacy:"я",Ycirc:"Ŷ",ycirc:"ŷ",Ycy:"Ы",ycy:"ы",yen:"¥",Yfr:"𝔜",yfr:"𝔶",YIcy:"Ї",yicy:"ї",Yopf:"𝕐",yopf:"𝕪",Yscr:"𝒴",yscr:"𝓎",YUcy:"Ю",yucy:"ю",Yuml:"Ÿ",yuml:"ÿ",Zacute:"Ź",zacute:"ź",Zcaron:"Ž",zcaron:"ž",Zcy:"З",zcy:"з",Zdot:"Ż",zdot:"ż",zeetrf:"ℨ",ZeroWidthSpace:"​",Zeta:"Ζ",zeta:"ζ",Zfr:"ℨ",zfr:"𝔷",ZHcy:"Ж",zhcy:"ж",zigrarr:"⇝",Zopf:"ℤ",zopf:"𝕫",Zscr:"𝒵",zscr:"𝓏",zwj:"\u200d",zwnj:"\u200c" 9 | }; 10 | 11 | function EntityParser(named) { 12 | this.named = named; 13 | } 14 | 15 | var HEXCHARCODE = /^#[xX]([A-Fa-f0-9]+)$/; 16 | var CHARCODE = /^#([0-9]+)$/; 17 | var NAMED = /^([A-Za-z0-9]+)$/; 18 | 19 | EntityParser.prototype.parse = function (entity) { 20 | if (!entity) { 21 | return; 22 | } 23 | var matches = entity.match(HEXCHARCODE); 24 | if (matches) { 25 | return String.fromCharCode(parseInt(matches[1], 16)); 26 | } 27 | matches = entity.match(CHARCODE); 28 | if (matches) { 29 | return String.fromCharCode(parseInt(matches[1], 10)); 30 | } 31 | matches = entity.match(NAMED); 32 | if (matches) { 33 | return this.named[matches[1]]; 34 | } 35 | }; 36 | 37 | var WSP = /[\t\n\f ]/; 38 | var ALPHA = /[A-Za-z]/; 39 | var CRLF = /\r\n?/g; 40 | 41 | function isSpace(char) { 42 | return WSP.test(char); 43 | } 44 | 45 | function isAlpha(char) { 46 | return ALPHA.test(char); 47 | } 48 | 49 | function preprocessInput(input) { 50 | return input.replace(CRLF, "\n"); 51 | } 52 | 53 | function EventedTokenizer(delegate, entityParser) { 54 | this.delegate = delegate; 55 | this.entityParser = entityParser; 56 | 57 | this.state = null; 58 | this.input = null; 59 | 60 | this.index = -1; 61 | this.line = -1; 62 | this.column = -1; 63 | this.tagLine = -1; 64 | this.tagColumn = -1; 65 | 66 | this.reset(); 67 | } 68 | 69 | EventedTokenizer.prototype = { 70 | reset: function() { 71 | this.state = 'beforeData'; 72 | this.input = ''; 73 | 74 | this.index = 0; 75 | this.line = 1; 76 | this.column = 0; 77 | 78 | this.tagLine = -1; 79 | this.tagColumn = -1; 80 | 81 | this.delegate.reset(); 82 | }, 83 | 84 | tokenize: function(input) { 85 | this.reset(); 86 | this.tokenizePart(input); 87 | this.tokenizeEOF(); 88 | }, 89 | 90 | tokenizePart: function(input) { 91 | this.input += preprocessInput(input); 92 | 93 | while (this.index < this.input.length) { 94 | this.states[this.state].call(this); 95 | } 96 | }, 97 | 98 | tokenizeEOF: function() { 99 | this.flushData(); 100 | }, 101 | 102 | flushData: function() { 103 | if (this.state === 'data') { 104 | this.delegate.finishData(); 105 | this.state = 'beforeData'; 106 | } 107 | }, 108 | 109 | peek: function() { 110 | return this.input.charAt(this.index); 111 | }, 112 | 113 | consume: function() { 114 | var char = this.peek(); 115 | 116 | this.index++; 117 | 118 | if (char === "\n") { 119 | this.line++; 120 | this.column = 0; 121 | } else { 122 | this.column++; 123 | } 124 | 125 | return char; 126 | }, 127 | 128 | consumeCharRef: function() { 129 | var endIndex = this.input.indexOf(';', this.index); 130 | if (endIndex === -1) { 131 | return; 132 | } 133 | var entity = this.input.slice(this.index, endIndex); 134 | var chars = this.entityParser.parse(entity); 135 | if (chars) { 136 | var count = entity.length; 137 | // consume the entity chars 138 | while (count) { 139 | this.consume(); 140 | count--; 141 | } 142 | // consume the `;` 143 | this.consume(); 144 | 145 | return chars; 146 | } 147 | }, 148 | 149 | markTagStart: function() { 150 | // these properties to be removed in next major bump 151 | this.tagLine = this.line; 152 | this.tagColumn = this.column; 153 | 154 | if (this.delegate.tagOpen) { 155 | this.delegate.tagOpen(); 156 | } 157 | }, 158 | 159 | states: { 160 | beforeData: function() { 161 | var char = this.peek(); 162 | 163 | if (char === "<") { 164 | this.state = 'tagOpen'; 165 | this.markTagStart(); 166 | this.consume(); 167 | } else { 168 | this.state = 'data'; 169 | this.delegate.beginData(); 170 | } 171 | }, 172 | 173 | data: function() { 174 | var char = this.peek(); 175 | 176 | if (char === "<") { 177 | this.delegate.finishData(); 178 | this.state = 'tagOpen'; 179 | this.markTagStart(); 180 | this.consume();  181 | } else if (char === "&") { 182 | this.consume(); 183 | this.delegate.appendToData(this.consumeCharRef() || "&"); 184 | } else { 185 | this.consume(); 186 | this.delegate.appendToData(char); 187 | } 188 | }, 189 | 190 | tagOpen: function() { 191 | var char = this.consume(); 192 | 193 | if (char === "!") { 194 | this.state = 'markupDeclaration'; 195 | } else if (char === "/") { 196 | this.state = 'endTagOpen'; 197 | } else if (isAlpha(char)) { 198 | this.state = 'tagName'; 199 | this.delegate.beginStartTag(); 200 | this.delegate.appendToTagName(char.toLowerCase()); 201 | } 202 | }, 203 | 204 | markupDeclaration: function() { 205 | var char = this.consume(); 206 | 207 | if (char === "-" && this.input.charAt(this.index) === "-") { 208 | this.consume(); 209 | this.state = 'commentStart'; 210 | this.delegate.beginComment(); 211 | } 212 | }, 213 | 214 | commentStart: function() { 215 | var char = this.consume(); 216 | 217 | if (char === "-") { 218 | this.state = 'commentStartDash'; 219 | } else if (char === ">") { 220 | this.delegate.finishComment(); 221 | this.state = 'beforeData'; 222 | } else { 223 | this.delegate.appendToCommentData(char); 224 | this.state = 'comment'; 225 | } 226 | }, 227 | 228 | commentStartDash: function() { 229 | var char = this.consume(); 230 | 231 | if (char === "-") { 232 | this.state = 'commentEnd'; 233 | } else if (char === ">") { 234 | this.delegate.finishComment(); 235 | this.state = 'beforeData'; 236 | } else { 237 | this.delegate.appendToCommentData("-"); 238 | this.state = 'comment'; 239 | } 240 | }, 241 | 242 | comment: function() { 243 | var char = this.consume(); 244 | 245 | if (char === "-") { 246 | this.state = 'commentEndDash'; 247 | } else { 248 | this.delegate.appendToCommentData(char); 249 | } 250 | }, 251 | 252 | commentEndDash: function() { 253 | var char = this.consume(); 254 | 255 | if (char === "-") { 256 | this.state = 'commentEnd'; 257 | } else { 258 | this.delegate.appendToCommentData("-" + char); 259 | this.state = 'comment'; 260 | } 261 | }, 262 | 263 | commentEnd: function() { 264 | var char = this.consume(); 265 | 266 | if (char === ">") { 267 | this.delegate.finishComment(); 268 | this.state = 'beforeData'; 269 | } else { 270 | this.delegate.appendToCommentData("--" + char); 271 | this.state = 'comment'; 272 | } 273 | }, 274 | 275 | tagName: function() { 276 | var char = this.consume(); 277 | 278 | if (isSpace(char)) { 279 | this.state = 'beforeAttributeName'; 280 | } else if (char === "/") { 281 | this.state = 'selfClosingStartTag'; 282 | } else if (char === ">") { 283 | this.delegate.finishTag(); 284 | this.state = 'beforeData'; 285 | } else { 286 | this.delegate.appendToTagName(char); 287 | } 288 | }, 289 | 290 | beforeAttributeName: function() { 291 | var char = this.peek(); 292 | 293 | if (isSpace(char)) { 294 | this.consume(); 295 | return; 296 | } else if (char === "/") { 297 | this.state = 'selfClosingStartTag'; 298 | this.consume(); 299 | } else if (char === ">") { 300 | this.consume(); 301 | this.delegate.finishTag(); 302 | this.state = 'beforeData'; 303 | } else { 304 | this.state = 'attributeName'; 305 | this.delegate.beginAttribute(); 306 | this.consume(); 307 | this.delegate.appendToAttributeName(char); 308 | } 309 | }, 310 | 311 | attributeName: function() { 312 | var char = this.peek(); 313 | 314 | if (isSpace(char)) { 315 | this.state = 'afterAttributeName'; 316 | this.consume(); 317 | } else if (char === "/") { 318 | this.delegate.beginAttributeValue(false); 319 | this.delegate.finishAttributeValue(); 320 | this.consume(); 321 | this.state = 'selfClosingStartTag'; 322 | } else if (char === "=") { 323 | this.state = 'beforeAttributeValue'; 324 | this.consume(); 325 | } else if (char === ">") { 326 | this.delegate.beginAttributeValue(false); 327 | this.delegate.finishAttributeValue(); 328 | this.consume(); 329 | this.delegate.finishTag(); 330 | this.state = 'beforeData'; 331 | } else { 332 | this.consume(); 333 | this.delegate.appendToAttributeName(char); 334 | } 335 | }, 336 | 337 | afterAttributeName: function() { 338 | var char = this.peek(); 339 | 340 | if (isSpace(char)) { 341 | this.consume(); 342 | return; 343 | } else if (char === "/") { 344 | this.delegate.beginAttributeValue(false); 345 | this.delegate.finishAttributeValue(); 346 | this.consume(); 347 | this.state = 'selfClosingStartTag'; 348 | } else if (char === "=") { 349 | this.consume(); 350 | this.state = 'beforeAttributeValue'; 351 | } else if (char === ">") { 352 | this.delegate.beginAttributeValue(false); 353 | this.delegate.finishAttributeValue(); 354 | this.consume(); 355 | this.delegate.finishTag(); 356 | this.state = 'beforeData'; 357 | } else { 358 | this.delegate.beginAttributeValue(false); 359 | this.delegate.finishAttributeValue(); 360 | this.consume(); 361 | this.state = 'attributeName'; 362 | this.delegate.beginAttribute(); 363 | this.delegate.appendToAttributeName(char); 364 | } 365 | }, 366 | 367 | beforeAttributeValue: function() { 368 | var char = this.peek(); 369 | 370 | if (isSpace(char)) { 371 | this.consume(); 372 | } else if (char === '"') { 373 | this.state = 'attributeValueDoubleQuoted'; 374 | this.delegate.beginAttributeValue(true); 375 | this.consume(); 376 | } else if (char === "'") { 377 | this.state = 'attributeValueSingleQuoted'; 378 | this.delegate.beginAttributeValue(true); 379 | this.consume(); 380 | } else if (char === ">") { 381 | this.delegate.beginAttributeValue(false); 382 | this.delegate.finishAttributeValue(); 383 | this.consume(); 384 | this.delegate.finishTag(); 385 | this.state = 'beforeData'; 386 | } else { 387 | this.state = 'attributeValueUnquoted'; 388 | this.delegate.beginAttributeValue(false); 389 | this.consume(); 390 | this.delegate.appendToAttributeValue(char); 391 | } 392 | }, 393 | 394 | attributeValueDoubleQuoted: function() { 395 | var char = this.consume(); 396 | 397 | if (char === '"') { 398 | this.delegate.finishAttributeValue(); 399 | this.state = 'afterAttributeValueQuoted'; 400 | } else if (char === "&") { 401 | this.delegate.appendToAttributeValue(this.consumeCharRef('"') || "&"); 402 | } else { 403 | this.delegate.appendToAttributeValue(char); 404 | } 405 | }, 406 | 407 | attributeValueSingleQuoted: function() { 408 | var char = this.consume(); 409 | 410 | if (char === "'") { 411 | this.delegate.finishAttributeValue(); 412 | this.state = 'afterAttributeValueQuoted'; 413 | } else if (char === "&") { 414 | this.delegate.appendToAttributeValue(this.consumeCharRef("'") || "&"); 415 | } else { 416 | this.delegate.appendToAttributeValue(char); 417 | } 418 | }, 419 | 420 | attributeValueUnquoted: function() { 421 | var char = this.peek(); 422 | 423 | if (isSpace(char)) { 424 | this.delegate.finishAttributeValue(); 425 | this.consume(); 426 | this.state = 'beforeAttributeName'; 427 | } else if (char === "&") { 428 | this.consume(); 429 | this.delegate.appendToAttributeValue(this.consumeCharRef(">") || "&"); 430 | } else if (char === ">") { 431 | this.delegate.finishAttributeValue(); 432 | this.consume(); 433 | this.delegate.finishTag(); 434 | this.state = 'beforeData'; 435 | } else { 436 | this.consume(); 437 | this.delegate.appendToAttributeValue(char); 438 | } 439 | }, 440 | 441 | afterAttributeValueQuoted: function() { 442 | var char = this.peek(); 443 | 444 | if (isSpace(char)) { 445 | this.consume(); 446 | this.state = 'beforeAttributeName'; 447 | } else if (char === "/") { 448 | this.consume(); 449 | this.state = 'selfClosingStartTag'; 450 | } else if (char === ">") { 451 | this.consume(); 452 | this.delegate.finishTag(); 453 | this.state = 'beforeData'; 454 | } else { 455 | this.state = 'beforeAttributeName'; 456 | } 457 | }, 458 | 459 | selfClosingStartTag: function() { 460 | var char = this.peek(); 461 | 462 | if (char === ">") { 463 | this.consume(); 464 | this.delegate.markTagAsSelfClosing(); 465 | this.delegate.finishTag(); 466 | this.state = 'beforeData'; 467 | } else { 468 | this.state = 'beforeAttributeName'; 469 | } 470 | }, 471 | 472 | endTagOpen: function() { 473 | var char = this.consume(); 474 | 475 | if (isAlpha(char)) { 476 | this.state = 'tagName'; 477 | this.delegate.beginEndTag(); 478 | this.delegate.appendToTagName(char.toLowerCase()); 479 | } 480 | } 481 | } 482 | }; 483 | 484 | function Tokenizer(entityParser, options) { 485 | this.token = null; 486 | this.startLine = 1; 487 | this.startColumn = 0; 488 | this.options = options || {}; 489 | this.tokenizer = new EventedTokenizer(this, entityParser); 490 | } 491 | 492 | Tokenizer.prototype = { 493 | tokenize: function(input) { 494 | this.tokens = []; 495 | this.tokenizer.tokenize(input); 496 | return this.tokens; 497 | }, 498 | 499 | tokenizePart: function(input) { 500 | this.tokens = []; 501 | this.tokenizer.tokenizePart(input); 502 | return this.tokens; 503 | }, 504 | 505 | tokenizeEOF: function() { 506 | this.tokens = []; 507 | this.tokenizer.tokenizeEOF(); 508 | return this.tokens[0]; 509 | }, 510 | 511 | reset: function() { 512 | this.token = null; 513 | this.startLine = 1; 514 | this.startColumn = 0; 515 | }, 516 | 517 | addLocInfo: function() { 518 | if (this.options.loc) { 519 | this.token.loc = { 520 | start: { 521 | line: this.startLine, 522 | column: this.startColumn 523 | }, 524 | end: { 525 | line: this.tokenizer.line, 526 | column: this.tokenizer.column 527 | } 528 | }; 529 | } 530 | this.startLine = this.tokenizer.line; 531 | this.startColumn = this.tokenizer.column; 532 | }, 533 | 534 | // Data 535 | 536 | beginData: function() { 537 | this.token = { 538 | type: 'Chars', 539 | chars: '' 540 | }; 541 | this.tokens.push(this.token); 542 | }, 543 | 544 | appendToData: function(char) { 545 | this.token.chars += char; 546 | }, 547 | 548 | finishData: function() { 549 | this.addLocInfo(); 550 | }, 551 | 552 | // Comment 553 | 554 | beginComment: function() { 555 | this.token = { 556 | type: 'Comment', 557 | chars: '' 558 | }; 559 | this.tokens.push(this.token); 560 | }, 561 | 562 | appendToCommentData: function(char) { 563 | this.token.chars += char; 564 | }, 565 | 566 | finishComment: function() { 567 | this.addLocInfo(); 568 | }, 569 | 570 | // Tags - basic 571 | 572 | beginStartTag: function() { 573 | this.token = { 574 | type: 'StartTag', 575 | tagName: '', 576 | attributes: [], 577 | selfClosing: false 578 | }; 579 | this.tokens.push(this.token); 580 | }, 581 | 582 | beginEndTag: function() { 583 | this.token = { 584 | type: 'EndTag', 585 | tagName: '' 586 | }; 587 | this.tokens.push(this.token); 588 | }, 589 | 590 | finishTag: function() { 591 | this.addLocInfo(); 592 | }, 593 | 594 | markTagAsSelfClosing: function() { 595 | this.token.selfClosing = true; 596 | }, 597 | 598 | // Tags - name 599 | 600 | appendToTagName: function(char) { 601 | this.token.tagName += char; 602 | }, 603 | 604 | // Tags - attributes 605 | 606 | beginAttribute: function() { 607 | this._currentAttribute = ["", "", null]; 608 | this.token.attributes.push(this._currentAttribute); 609 | }, 610 | 611 | appendToAttributeName: function(char) { 612 | this._currentAttribute[0] += char; 613 | }, 614 | 615 | beginAttributeValue: function(isQuoted) { 616 | this._currentAttribute[2] = isQuoted; 617 | }, 618 | 619 | appendToAttributeValue: function(char) { 620 | this._currentAttribute[1] = this._currentAttribute[1] || ""; 621 | this._currentAttribute[1] += char; 622 | }, 623 | 624 | finishAttributeValue: function() { 625 | } 626 | }; 627 | 628 | function tokenize(input, options) { 629 | var tokenizer = new Tokenizer(new EntityParser(HTML5NamedCharRefs), options); 630 | return tokenizer.tokenize(input); 631 | } 632 | 633 | exports.HTML5NamedCharRefs = HTML5NamedCharRefs; 634 | exports.EntityParser = EntityParser; 635 | exports.EventedTokenizer = EventedTokenizer; 636 | exports.Tokenizer = Tokenizer; 637 | exports.tokenize = tokenize; 638 | 639 | })); 640 | //# sourceMappingURL=simple-html-tokenizer.js.map -------------------------------------------------------------------------------- /spring-best-practice-riot-ssr/src/main/resources/static/js/todo.js: -------------------------------------------------------------------------------- 1 | riot.tag2('todo', '

{opts.title}

', '', '', function(opts) { 2 | var todo = this; 3 | todo.items = opts.items 4 | 5 | this.edit = function(e) { 6 | todo.text = e.target.value 7 | }.bind(this) 8 | 9 | this.add = function(e) { 10 | if (todo.text) { 11 | var data = new FormData() 12 | data.append('title', todo.text) 13 | 14 | fetch('/todo.json', {method: 'post', body: data}).then(function(response) { 15 | return response.json(); 16 | }).then(function(json) { 17 | todo.items.push({ id: json.id, title: json.title }) 18 | todo.text = todo.input.value = '' 19 | todo.update(); 20 | }) 21 | } 22 | }.bind(this) 23 | 24 | this.removeAllDone = function(e) { 25 | fetch('/todo.json', {method: 'delete'}).then(function(response) { 26 | todo.items = todo.items.filter(function(item) { 27 | return !item.done 28 | }) 29 | todo.update(); 30 | }) 31 | }.bind(this) 32 | 33 | this.whatShow = function(item) { 34 | return !item.hidden 35 | }.bind(this) 36 | 37 | this.onlyDone = function(item) { 38 | return item.done 39 | }.bind(this) 40 | 41 | this.toggle = function(e) { 42 | fetch('/todo/' + e.item.id + '.json', {method: 'put'}).then(function(response) { 43 | return response.json(); 44 | }).then(function(json) { 45 | var item = e.item 46 | item.done = json.done 47 | todo.update(); 48 | return true 49 | }) 50 | }.bind(this) 51 | }); 52 | -------------------------------------------------------------------------------- /spring-best-practice-riot-ssr/src/main/resources/static/tag/todo.tag: -------------------------------------------------------------------------------- 1 | 2 |

{ opts.title }

3 |
    4 |
  • 5 | 8 |
  • 9 |
10 |
11 | 12 | 13 | 14 |
15 | 67 |
68 | -------------------------------------------------------------------------------- /spring-best-practice-riot-ssr/src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Riot todo 5 | 6 | 7 | 8 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /spring-best-practice-riot-ssr/src/test/java/practice/IndexControllerTest.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import org.mockito.InjectMocks; 7 | import org.mockito.Mock; 8 | import org.mockito.MockitoAnnotations; 9 | import org.mockito.stubbing.Answer; 10 | import org.springframework.core.io.DefaultResourceLoader; 11 | import org.springframework.core.io.Resource; 12 | import org.springframework.core.io.ResourceLoader; 13 | import org.springframework.ui.ExtendedModelMap; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | import static org.mockito.Matchers.anyObject; 20 | import static org.mockito.Matchers.anyString; 21 | import static org.mockito.Mockito.when; 22 | 23 | /** 24 | * @author Ogawa, Takeshi 25 | */ 26 | public class IndexControllerTest { 27 | 28 | @Mock 29 | private ResourceLoader resourceLoader; 30 | 31 | @Mock 32 | private TodoRepository todoRepository; 33 | 34 | @Mock 35 | private ObjectMapper objectMapper = new ObjectMapper(); 36 | 37 | @InjectMocks 38 | private IndexController indexController = new IndexController(); 39 | 40 | @Before 41 | public void setup() { 42 | MockitoAnnotations.initMocks(this); 43 | } 44 | 45 | @Test 46 | public void index() throws Exception { 47 | ResourceLoader defaultResourceLoader = new DefaultResourceLoader(); 48 | when(resourceLoader.getResource(anyString())).thenAnswer((Answer) invocationOnMock -> defaultResourceLoader.getResource((String) invocationOnMock.getArguments()[0])); 49 | 50 | List todos = new ArrayList<>(); 51 | todos.add(new Todo(1, "title1", true)); 52 | todos.add(new Todo(2, "title2", false)); 53 | todos.add(new Todo(3, "title3", false)); 54 | when(todoRepository.findAll()).thenReturn(todos); 55 | 56 | when(objectMapper.writeValueAsString(anyObject())).thenReturn(new ObjectMapper().writeValueAsString(todos)); 57 | 58 | String viewName = indexController.index(new ExtendedModelMap()); 59 | assertEquals("index", viewName); 60 | } 61 | } -------------------------------------------------------------------------------- /spring-best-practice-security-multi-tenancy/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | jp.co.tagbangers.spring-best-practices 7 | spring-best-practice-security-multi-tenancy 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | spring-best-practice-security-multi-tenancy 12 | Spring Best Practices Security Multi Tenancy 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 1.3.2.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | 1.8 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-devtools 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-security 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-thymeleaf 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-data-jpa 42 | 43 | 44 | 45 | org.flywaydb 46 | flyway-core 47 | 48 | 49 | 50 | org.webjars 51 | jquery 52 | 2.2.0 53 | 54 | 55 | org.webjars 56 | bootstrap 57 | 3.3.6 58 | 59 | 60 | 61 | com.h2database 62 | h2 63 | runtime 64 | 65 | 66 | org.springframework.boot 67 | spring-boot-starter-test 68 | test 69 | 70 | 71 | 72 | 73 | 74 | 75 | org.springframework.boot 76 | spring-boot-maven-plugin 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /spring-best-practice-security-multi-tenancy/src/main/java/practice/Application.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /spring-best-practice-security-multi-tenancy/src/main/java/practice/AuthorizedUserDetails.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import org.springframework.beans.BeanUtils; 4 | import org.springframework.security.core.GrantedAuthority; 5 | import org.springframework.security.core.userdetails.UserDetails; 6 | 7 | import java.util.Collection; 8 | 9 | public class AuthorizedUserDetails extends User implements UserDetails { 10 | 11 | public AuthorizedUserDetails(User user) { 12 | BeanUtils.copyProperties(user, this); 13 | } 14 | 15 | @Override 16 | public Collection getAuthorities() { 17 | return null; 18 | } 19 | 20 | @Override 21 | public boolean isAccountNonExpired() { 22 | return true; 23 | } 24 | 25 | @Override 26 | public boolean isAccountNonLocked() { 27 | return true; 28 | } 29 | 30 | @Override 31 | public boolean isCredentialsNonExpired() { 32 | return true; 33 | } 34 | 35 | @Override 36 | public boolean isEnabled() { 37 | return true; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /spring-best-practice-security-multi-tenancy/src/main/java/practice/AuthorizedUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.security.core.userdetails.UserDetails; 5 | import org.springframework.security.core.userdetails.UserDetailsService; 6 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.web.bind.ServletRequestUtils; 9 | import org.springframework.web.context.request.RequestContextHolder; 10 | import org.springframework.web.context.request.ServletRequestAttributes; 11 | 12 | import javax.servlet.http.HttpServletRequest; 13 | 14 | @Component 15 | public class AuthorizedUserDetailsService implements UserDetailsService { 16 | 17 | public static final String TENANT_ID_ATTRIBUTE = AuthorizedUserDetailsService.class.getCanonicalName() + ".tenantId"; 18 | public static final String TENANT_ID_PARAM_NAME = "tenantId"; 19 | 20 | @Autowired 21 | private UserRepository userRepository; 22 | 23 | @Override 24 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 25 | HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); 26 | String tenantId = ServletRequestUtils.getStringParameter(request, TENANT_ID_PARAM_NAME, (String) request.getAttribute(TENANT_ID_ATTRIBUTE)); 27 | if (tenantId == null) { 28 | throw new UsernameNotFoundException("tenantId is Null"); 29 | } 30 | 31 | User user = userRepository.findOneByTenantIdAndUsername(tenantId, username); 32 | if (user != null) { 33 | return new AuthorizedUserDetails(user); 34 | } 35 | throw new UsernameNotFoundException(username); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /spring-best-practice-security-multi-tenancy/src/main/java/practice/HelloController.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 4 | import org.springframework.stereotype.Controller; 5 | import org.springframework.ui.Model; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | 8 | @Controller 9 | @RequestMapping("/") 10 | public class HelloController { 11 | 12 | @RequestMapping 13 | public String hello( 14 | @AuthenticationPrincipal AuthorizedUserDetails user, 15 | Model model) { 16 | model.addAttribute("user", user); 17 | return "hello"; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /spring-best-practice-security-multi-tenancy/src/main/java/practice/LoginController.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | 6 | @Controller 7 | @RequestMapping("/login") 8 | public class LoginController { 9 | 10 | @RequestMapping 11 | public String login() { 12 | return "login"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /spring-best-practice-security-multi-tenancy/src/main/java/practice/MultiTenantRememberMeServices.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.security.core.userdetails.UserDetails; 5 | import org.springframework.security.core.userdetails.UserDetailsService; 6 | import org.springframework.security.crypto.codec.Base64; 7 | import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices; 8 | import org.springframework.security.web.authentication.rememberme.CookieTheftException; 9 | import org.springframework.security.web.authentication.rememberme.InvalidCookieException; 10 | import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException; 11 | import org.springframework.util.Assert; 12 | 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | import java.security.SecureRandom; 16 | import java.util.Arrays; 17 | import java.util.Date; 18 | 19 | public class MultiTenantRememberMeServices extends AbstractRememberMeServices { 20 | 21 | private MultiTenantTokenRepository tokenRepository = new MultiTenantTokenRepository(); 22 | private SecureRandom random; 23 | 24 | public static final int DEFAULT_SERIES_LENGTH = 16; 25 | public static final int DEFAULT_TOKEN_LENGTH = 16; 26 | 27 | private int seriesLength = DEFAULT_SERIES_LENGTH; 28 | private int tokenLength = DEFAULT_TOKEN_LENGTH; 29 | 30 | public MultiTenantRememberMeServices( 31 | String key, 32 | UserDetailsService userDetailsService, 33 | MultiTenantTokenRepository tokenRepository) { 34 | super(key, userDetailsService); 35 | random = new SecureRandom(); 36 | this.tokenRepository = tokenRepository; 37 | } 38 | 39 | @Override 40 | protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) { 41 | if (cookieTokens.length != 2) { 42 | throw new InvalidCookieException("Cookie token did not contain " + 2 43 | + " tokens, but contained '" + Arrays.asList(cookieTokens) + "'"); 44 | } 45 | 46 | final String presentedSeries = cookieTokens[0]; 47 | final String presentedToken = cookieTokens[1]; 48 | 49 | MultiTenantRememberMeToken token = tokenRepository.getTokenForSeries(presentedSeries); 50 | 51 | if (token == null) { 52 | // No series match, so we can't authenticate using this cookie 53 | throw new CookieTheftException( 54 | "No persistent token found for series id: " + presentedSeries); 55 | } 56 | 57 | // We have a match for this user/series combination 58 | if (!presentedToken.equals(token.getTokenValue())) { 59 | // Token doesn't match series value. Delete all logins for this user and throw 60 | // an exception to warn them. 61 | tokenRepository.removeUserTokens(token.getUsername(), token.getTenantId()); 62 | 63 | throw new CookieTheftException( 64 | messages.getMessage( 65 | "PersistentTokenBasedRememberMeServices.cookieStolen", 66 | "Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack.")); 67 | } 68 | 69 | if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System 70 | .currentTimeMillis()) { 71 | throw new RememberMeAuthenticationException("Remember-me login has expired"); 72 | } 73 | 74 | // Token also matches, so login is valid. Update the token value, keeping the 75 | // *same* series number. 76 | if (logger.isDebugEnabled()) { 77 | logger.debug("Refreshing persistent login token for " + 78 | "tenantId '" + token.getTenantId() + "', " + 79 | "user '" + token.getUsername() + "', " + 80 | "series '" + token.getSeries() + "'"); 81 | } 82 | 83 | MultiTenantRememberMeToken newToken = new MultiTenantRememberMeToken( 84 | token.getTenantId(), token.getUsername(), token.getSeries(), generateTokenData(), new Date()); 85 | 86 | try { 87 | tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate()); 88 | addCookie(newToken, request, response); 89 | } 90 | catch (Exception e) { 91 | logger.error("Failed to update token: ", e); 92 | throw new RememberMeAuthenticationException( 93 | "Autologin failed due to data access problem"); 94 | } 95 | 96 | request.setAttribute(AuthorizedUserDetailsService.TENANT_ID_ATTRIBUTE, newToken.getTenantId()); 97 | return getUserDetailsService().loadUserByUsername(token.getUsername()); 98 | } 99 | 100 | @Override 101 | protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) { 102 | String username = successfulAuthentication.getName(); 103 | 104 | logger.debug("Creating new persistent login for user " + username); 105 | 106 | MultiTenantRememberMeToken token = new MultiTenantRememberMeToken( 107 | extractTenantId(successfulAuthentication), username, generateSeriesData(), generateTokenData(), new Date()); 108 | try { 109 | tokenRepository.createNewToken(token); 110 | addCookie(token, request, response); 111 | } 112 | catch (Exception e) { 113 | logger.error("Failed to save persistent token ", e); 114 | } 115 | } 116 | 117 | @Override 118 | public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { 119 | super.logout(request, response, authentication); 120 | 121 | if (authentication != null) { 122 | tokenRepository.removeUserTokens(authentication.getName(), extractTenantId(authentication)); 123 | } 124 | } 125 | 126 | protected String extractTenantId(Authentication authentication) { 127 | AuthorizedUserDetails userDetails = (AuthorizedUserDetails) authentication.getPrincipal(); 128 | return userDetails.getTenantId(); 129 | } 130 | 131 | protected String generateSeriesData() { 132 | byte[] newSeries = new byte[seriesLength]; 133 | random.nextBytes(newSeries); 134 | return new String(Base64.encode(newSeries)); 135 | } 136 | 137 | protected String generateTokenData() { 138 | byte[] newToken = new byte[tokenLength]; 139 | random.nextBytes(newToken); 140 | return new String(Base64.encode(newToken)); 141 | } 142 | 143 | private void addCookie(MultiTenantRememberMeToken token, HttpServletRequest request, 144 | HttpServletResponse response) { 145 | setCookie(new String[] { token.getSeries(), token.getTokenValue() }, 146 | getTokenValiditySeconds(), request, response); 147 | } 148 | 149 | public void setSeriesLength(int seriesLength) { 150 | this.seriesLength = seriesLength; 151 | } 152 | 153 | public void setTokenLength(int tokenLength) { 154 | this.tokenLength = tokenLength; 155 | } 156 | 157 | @Override 158 | public void setTokenValiditySeconds(int tokenValiditySeconds) { 159 | Assert.isTrue(tokenValiditySeconds > 0, 160 | "tokenValiditySeconds must be positive for this implementation"); 161 | super.setTokenValiditySeconds(tokenValiditySeconds); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /spring-best-practice-security-multi-tenancy/src/main/java/practice/MultiTenantRememberMeToken.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken; 4 | 5 | import java.util.Date; 6 | 7 | public class MultiTenantRememberMeToken extends PersistentRememberMeToken { 8 | 9 | private final String tenantId; 10 | 11 | public MultiTenantRememberMeToken(String tenantId, String username, String series, String tokenValue, Date date) { 12 | super(username, series, tokenValue, date); 13 | this.tenantId = tenantId; 14 | } 15 | 16 | public String getTenantId() { 17 | return tenantId; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /spring-best-practice-security-multi-tenancy/src/main/java/practice/MultiTenantTokenRepository.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import org.springframework.dao.DataAccessException; 4 | import org.springframework.dao.EmptyResultDataAccessException; 5 | import org.springframework.dao.IncorrectResultSizeDataAccessException; 6 | import org.springframework.jdbc.core.RowMapper; 7 | import org.springframework.jdbc.core.support.JdbcDaoSupport; 8 | 9 | import java.sql.ResultSet; 10 | import java.sql.SQLException; 11 | import java.util.Date; 12 | 13 | public class MultiTenantTokenRepository extends JdbcDaoSupport { 14 | 15 | /** Default SQL for creating the database table to store the tokens */ 16 | public static final String CREATE_TABLE_SQL = 17 | "create table persistent_logins (tenant_id varchar(64) not null, username varchar(64) not null, series varchar(64) primary key, " + 18 | "token varchar(64) not null, last_used timestamp not null)"; 19 | /** The default SQL used by the getTokenBySeries query */ 20 | public static final String DEF_TOKEN_BY_SERIES_SQL = "select tenant_id,username,series,token,last_used from persistent_logins where series = ?"; 21 | /** The default SQL used by createNewToken */ 22 | public static final String DEF_INSERT_TOKEN_SQL = "insert into persistent_logins (tenant_id, username, series, token, last_used) values(?,?,?,?,?)"; 23 | /** The default SQL used by updateToken */ 24 | public static final String DEF_UPDATE_TOKEN_SQL = "update persistent_logins set token = ?, last_used = ? where series = ?"; 25 | /** The default SQL used by removeUserTokens */ 26 | public static final String DEF_REMOVE_USER_TOKENS_SQL = "delete from persistent_logins where username = ? and tenant_id = ?"; 27 | 28 | private String tokensBySeriesSql = DEF_TOKEN_BY_SERIES_SQL; 29 | private String insertTokenSql = DEF_INSERT_TOKEN_SQL; 30 | private String updateTokenSql = DEF_UPDATE_TOKEN_SQL; 31 | private String removeUserTokensSql = DEF_REMOVE_USER_TOKENS_SQL; 32 | private boolean createTableOnStartup; 33 | 34 | @Override 35 | protected void initDao() { 36 | if (createTableOnStartup) { 37 | getJdbcTemplate().execute(CREATE_TABLE_SQL); 38 | } 39 | } 40 | 41 | public void createNewToken(MultiTenantRememberMeToken token) { 42 | getJdbcTemplate().update(insertTokenSql, token.getTenantId(), token.getUsername(), token.getSeries(), 43 | token.getTokenValue(), token.getDate()); 44 | } 45 | 46 | public void updateToken(String series, String tokenValue, Date lastUsed) { 47 | getJdbcTemplate().update(updateTokenSql, tokenValue, lastUsed, series); 48 | } 49 | 50 | public MultiTenantRememberMeToken getTokenForSeries(String seriesId) { 51 | try { 52 | return getJdbcTemplate().queryForObject(tokensBySeriesSql, 53 | new RowMapper() { 54 | public MultiTenantRememberMeToken mapRow(ResultSet rs, int rowNum) 55 | throws SQLException { 56 | return new MultiTenantRememberMeToken( 57 | rs.getString(1), // tenant_id 58 | rs.getString(2), // username 59 | rs.getString(3), // series 60 | rs.getString(4), // token 61 | rs.getTimestamp(5)); // last_used 62 | } 63 | }, seriesId); 64 | } 65 | catch (EmptyResultDataAccessException zeroResults) { 66 | if (logger.isDebugEnabled()) { 67 | logger.debug("Querying token for series '" + seriesId 68 | + "' returned no results.", zeroResults); 69 | } 70 | } 71 | catch (IncorrectResultSizeDataAccessException moreThanOne) { 72 | logger.error("Querying token for series '" + seriesId 73 | + "' returned more than one value. Series" + " should be unique"); 74 | } 75 | catch (DataAccessException e) { 76 | logger.error("Failed to load token for series " + seriesId, e); 77 | } 78 | 79 | return null; 80 | } 81 | 82 | public void removeUserTokens(String username, String tenantId) { 83 | getJdbcTemplate().update(removeUserTokensSql, username, tenantId); 84 | } 85 | 86 | public void setCreateTableOnStartup(boolean createTableOnStartup) { 87 | this.createTableOnStartup = createTableOnStartup; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /spring-best-practice-security-multi-tenancy/src/main/java/practice/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 7 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 8 | import org.springframework.security.config.annotation.web.builders.WebSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 10 | import org.springframework.security.core.userdetails.UserDetailsService; 11 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 12 | 13 | import javax.sql.DataSource; 14 | import java.util.UUID; 15 | 16 | @Configuration 17 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 18 | 19 | @Autowired 20 | private DataSource dataSource; 21 | 22 | @Autowired 23 | public void configureGlobal(UserDetailsService userDetailsService, AuthenticationManagerBuilder auth) throws Exception { 24 | // @formatter:off 25 | auth 26 | .userDetailsService(userDetailsService); 27 | // @formatter:on 28 | } 29 | 30 | @Override 31 | public void configure(WebSecurity web) throws Exception { 32 | // @formatter:off 33 | web.ignoring() 34 | .antMatchers("/webjars/**"); 35 | // @formatter:on 36 | } 37 | 38 | @Override 39 | protected void configure(HttpSecurity http) throws Exception { 40 | String rememberMeKey = UUID.randomUUID().toString(); 41 | 42 | //@formatter:off 43 | http 44 | .authorizeRequests() 45 | .antMatchers("/login**").permitAll() 46 | .anyRequest() 47 | .authenticated() 48 | .and() 49 | .formLogin() 50 | .loginPage("/login") 51 | .failureUrl("/login?error").permitAll() 52 | .and() 53 | .logout() 54 | .logoutRequestMatcher(new AntPathRequestMatcher("/logout", "GET")) 55 | .and() 56 | .rememberMe() 57 | .key(rememberMeKey) 58 | .rememberMeServices(new MultiTenantRememberMeServices(rememberMeKey, userDetailsService(), multiTenantTokenRepository())) 59 | .and() 60 | .csrf().disable() 61 | .headers().frameOptions().sameOrigin(); 62 | //@formatter:on 63 | } 64 | 65 | @Bean 66 | public MultiTenantTokenRepository multiTenantTokenRepository() { 67 | MultiTenantTokenRepository repository = new MultiTenantTokenRepository(); 68 | repository.setDataSource(dataSource); 69 | return repository; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /spring-best-practice-security-multi-tenancy/src/main/java/practice/User.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import org.hibernate.validator.constraints.Email; 4 | import org.springframework.data.jpa.domain.AbstractPersistable; 5 | 6 | import javax.persistence.Column; 7 | import javax.persistence.Entity; 8 | import javax.persistence.Table; 9 | import javax.persistence.UniqueConstraint; 10 | import javax.validation.constraints.NotNull; 11 | import javax.validation.constraints.Size; 12 | 13 | @Entity 14 | @Table(name = "user", uniqueConstraints = @UniqueConstraint(columnNames = {"tenant_id", "username"})) 15 | public class User extends AbstractPersistable { 16 | 17 | private static final long serialVersionUID = 1L; 18 | 19 | @NotNull 20 | @Size(max = 100) 21 | @Column(name = "tenant_id") 22 | private String tenantId; 23 | 24 | @NotNull 25 | @Size(max = 100) 26 | @Column(unique = true) 27 | private String username; 28 | 29 | @Size(max = 80) 30 | private String password; 31 | 32 | @Size(max = 255) 33 | private String name; 34 | 35 | @Email 36 | @Size(max = 255) 37 | @NotNull 38 | @Column(unique = true) 39 | private String email; 40 | 41 | public String getTenantId() { 42 | return tenantId; 43 | } 44 | 45 | public void setTenantId(String tenantId) { 46 | this.tenantId = tenantId; 47 | } 48 | 49 | public String getUsername() { 50 | return username; 51 | } 52 | 53 | public void setUsername(String username) { 54 | this.username = username; 55 | } 56 | 57 | public String getPassword() { 58 | return password; 59 | } 60 | 61 | public void setPassword(String password) { 62 | this.password = password; 63 | } 64 | 65 | public String getName() { 66 | return name; 67 | } 68 | 69 | public void setName(String name) { 70 | this.name = name; 71 | } 72 | 73 | public String getEmail() { 74 | return email; 75 | } 76 | 77 | public void setEmail(String email) { 78 | this.email = email; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /spring-best-practice-security-multi-tenancy/src/main/java/practice/UserRepository.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | 5 | public interface UserRepository extends JpaRepository { 6 | 7 | User findOneByTenantIdAndUsername(String tenantId, String username); 8 | } 9 | -------------------------------------------------------------------------------- /spring-best-practice-security-multi-tenancy/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.driverClassName=org.h2.Driver 2 | spring.datasource.url=jdbc:h2:./db/test 3 | spring.datasource.username=sa 4 | spring.datasource.password=sa 5 | spring.jpa.hibernate.ddl-auto=update 6 | 7 | spring.jpa.properties.hibernate.show_sql=true 8 | spring.jpa.properties.hibernate.format_sql=true 9 | -------------------------------------------------------------------------------- /spring-best-practice-security-multi-tenancy/src/main/resources/db/migration/V0.0.1__init.sql: -------------------------------------------------------------------------------- 1 | create table persistent_logins ( 2 | tenant_id varchar(64) not null, 3 | username varchar(64) not null, 4 | series varchar(64) primary key, 5 | token varchar(64) not null, 6 | last_used timestamp not null 7 | ); 8 | 9 | create table user ( 10 | id bigint generated by default as identity, 11 | tenant_id varchar(255), 12 | username varchar(255), 13 | password varchar(255), 14 | name varchar(255), 15 | email varchar(255), 16 | primary key (id) 17 | ); 18 | 19 | insert into user (id, tenant_id, username, password, name, email) values (null, 'meikun', 'yamada', 'yamada', 'Yamada Taro', 'Yamada@example.com'); 20 | insert into user (id, tenant_id, username, password, name, email) values (null, 'meikun', 'iwaki', 'iwaki', 'Iwaki Masami', 'iwaki@example.com'); 21 | insert into user (id, tenant_id, username, password, name, email) values (null, 'benkei', 'musashibo', 'musashibo', 'Musashibo Kazuma', 'musashibo@example.com'); 22 | -------------------------------------------------------------------------------- /spring-best-practice-security-multi-tenancy/src/main/resources/templates/hello.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 | 17 |

User Information

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
Tenant ID
User Name
Name
Email
36 |

Menu

37 | 41 |
42 |
43 |
44 | 45 | -------------------------------------------------------------------------------- /spring-best-practice-security-multi-tenancy/src/main/resources/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 | 17 |
18 |
19 | 20 | 21 |
22 |
23 | 24 | 25 |
26 |
27 | 28 | 29 |
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 |
Tenant IDUser NamePassword
meikunyamadayamada
meikuniwakiiwaki
benkeimusashibomusashibo
62 |
63 |
64 |
65 | 66 | -------------------------------------------------------------------------------- /spring-best-practice-security-multi-tenancy/src/test/java/practice/SpringBestPracticeSecurityMultiTenancyApplicationTests.java: -------------------------------------------------------------------------------- 1 | package practice; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.test.context.web.WebAppConfiguration; 6 | import org.springframework.boot.test.SpringApplicationConfiguration; 7 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 8 | 9 | @RunWith(SpringJUnit4ClassRunner.class) 10 | @SpringApplicationConfiguration(classes = Application.class) 11 | @WebAppConfiguration 12 | public class SpringBestPracticeSecurityMultiTenancyApplicationTests { 13 | 14 | @Test 15 | public void contextLoads() { 16 | } 17 | 18 | } 19 | --------------------------------------------------------------------------------