├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── create_admin_user.sh ├── pom.xml └── src ├── main ├── java │ └── me │ │ └── woemler │ │ └── springblog │ │ ├── Application.java │ │ ├── config │ │ ├── AdminUserInitializer.java │ │ ├── BlogPostInitializer.java │ │ ├── DataSourceConfig.java │ │ ├── EmbeddedDataSourceConfig.java │ │ ├── SecurityConfig.java │ │ └── WebAppConfig.java │ │ ├── controllers │ │ ├── AdminController.java │ │ ├── BlogController.java │ │ ├── CodeController.java │ │ ├── LoginController.java │ │ └── MediaController.java │ │ ├── models │ │ ├── BlogPost.java │ │ └── User.java │ │ ├── repositories │ │ ├── BlogOperations.java │ │ ├── BlogRepository.java │ │ ├── BlogRepositoryImpl.java │ │ ├── UserRepository.java │ │ └── UserRepositoryImpl.java │ │ └── services │ │ └── GitHubService.java ├── resources │ ├── application-dev.properties │ ├── application-prd.properties │ ├── application.properties │ ├── data-source.properties │ └── static │ │ ├── css │ │ ├── blog-style.css │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap.min.css │ │ ├── jquery-ui.css │ │ └── jquery.tagit.css │ │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ │ └── js │ │ ├── bootstrap.min.js │ │ ├── jquery-1.10.2.min.js │ │ ├── jquery-ui.min.js │ │ ├── less-1.5.0.min.js │ │ └── tag-it.min.js └── webapp │ └── WEB-INF │ ├── tags │ ├── adminBlogEditHelp.tag │ ├── adminSidebar.tag │ ├── blogPostList.tag │ ├── blogSidebar.tag │ ├── footer.tag │ └── header.tag │ └── views │ └── jsp │ ├── about.jsp │ ├── admin │ ├── admin.jsp │ ├── blogPostList.jsp │ ├── editBlogPost.jsp │ ├── editTag.jsp │ └── tagList.jsp │ ├── blogPost.jsp │ ├── code.jsp │ ├── home.jsp │ ├── login.jsp │ ├── media.jsp │ ├── tagDetails.jsp │ └── tagList.jsp └── test ├── java └── me │ └── woemler │ └── springblog │ └── test │ ├── RepositoryTests.java │ └── config │ ├── TestApplicationConfig.java │ ├── TestDataSourceConfig.java │ └── TestWebAppConfig.java └── resources └── test-blog.properties /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Spring-Blog 3 | ################# 4 | 5 | target/ 6 | 7 | ################# 8 | ## IntelliJ 9 | ################# 10 | *.iml 11 | .idea 12 | 13 | ################# 14 | ## Eclipse 15 | ################# 16 | 17 | *.pydevproject 18 | .project 19 | .metadata 20 | bin/ 21 | tmp/ 22 | *.tmp 23 | *.bak 24 | *.swp 25 | *~.nib 26 | local.properties 27 | .classpath 28 | .settings/ 29 | .loadpath 30 | 31 | # External tool builders 32 | .externalToolBuilders/ 33 | 34 | # Locally stored "Eclipse launch configurations" 35 | *.launch 36 | 37 | # CDT-specific 38 | .cproject 39 | 40 | # PDT-specific 41 | .buildpath 42 | 43 | 44 | ################# 45 | ## Visual Studio 46 | ################# 47 | 48 | ## Ignore Visual Studio temporary files, build results, and 49 | ## files generated by popular Visual Studio add-ons. 50 | 51 | # User-specific files 52 | *.suo 53 | *.user 54 | *.sln.docstates 55 | 56 | # Build results 57 | 58 | [Dd]ebug/ 59 | [Rr]elease/ 60 | x64/ 61 | build/ 62 | [Bb]in/ 63 | [Oo]bj/ 64 | 65 | # MSTest test Results 66 | [Tt]est[Rr]esult*/ 67 | [Bb]uild[Ll]og.* 68 | 69 | *_i.c 70 | *_p.c 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.pch 75 | *.pdb 76 | *.pgc 77 | *.pgd 78 | *.rsp 79 | *.sbr 80 | *.tlb 81 | *.tli 82 | *.tlh 83 | *.tmp 84 | *.tmp_proj 85 | *.log 86 | *.vspscc 87 | *.vssscc 88 | .builds 89 | *.pidb 90 | *.log 91 | *.scc 92 | 93 | # Visual C++ cache files 94 | ipch/ 95 | *.aps 96 | *.ncb 97 | *.opensdf 98 | *.sdf 99 | *.cachefile 100 | 101 | # Visual Studio profiler 102 | *.psess 103 | *.vsp 104 | *.vspx 105 | 106 | # Guidance Automation Toolkit 107 | *.gpState 108 | 109 | # ReSharper is a .NET coding add-in 110 | _ReSharper*/ 111 | *.[Rr]e[Ss]harper 112 | 113 | # TeamCity is a build add-in 114 | _TeamCity* 115 | 116 | # DotCover is a Code Coverage Tool 117 | *.dotCover 118 | 119 | # NCrunch 120 | *.ncrunch* 121 | .*crunch*.local.xml 122 | 123 | # Installshield output folder 124 | [Ee]xpress/ 125 | 126 | # DocProject is a documentation generator add-in 127 | DocProject/buildhelp/ 128 | DocProject/Help/*.HxT 129 | DocProject/Help/*.HxC 130 | DocProject/Help/*.hhc 131 | DocProject/Help/*.hhk 132 | DocProject/Help/*.hhp 133 | DocProject/Help/Html2 134 | DocProject/Help/html 135 | 136 | # Click-Once directory 137 | publish/ 138 | 139 | # Publish Web Output 140 | *.Publish.xml 141 | *.pubxml 142 | 143 | # NuGet Packages Directory 144 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 145 | #packages/ 146 | 147 | # Windows Azure Build Output 148 | csx 149 | *.build.csdef 150 | 151 | # Windows Store app package directory 152 | AppPackages/ 153 | 154 | # Others 155 | sql/ 156 | *.Cache 157 | ClientBin/ 158 | [Ss]tyle[Cc]op.* 159 | ~$* 160 | *~ 161 | *.dbmdl 162 | *.[Pp]ublish.xml 163 | *.pfx 164 | *.publishsettings 165 | 166 | # RIA/Silverlight projects 167 | Generated_Code/ 168 | 169 | # Backup & report files from converting an old project file to a newer 170 | # Visual Studio version. Backup files are not needed, because we have git ;-) 171 | _UpgradeReport_Files/ 172 | Backup*/ 173 | UpgradeLog*.XML 174 | UpgradeLog*.htm 175 | 176 | # SQL Server files 177 | App_Data/*.mdf 178 | App_Data/*.ldf 179 | 180 | ############# 181 | ## Windows detritus 182 | ############# 183 | 184 | # Windows image file caches 185 | Thumbs.db 186 | ehthumbs.db 187 | 188 | # Folder config file 189 | Desktop.ini 190 | 191 | # Recycle Bin used on file shares 192 | $RECYCLE.BIN/ 193 | 194 | # Mac crap 195 | .DS_Store 196 | 197 | 198 | ############# 199 | ## Python 200 | ############# 201 | 202 | *.py[co] 203 | 204 | # Packages 205 | *.egg 206 | *.egg-info 207 | dist/ 208 | build/ 209 | eggs/ 210 | parts/ 211 | var/ 212 | sdist/ 213 | develop-eggs/ 214 | .installed.cfg 215 | 216 | # Installer logs 217 | pip-log.txt 218 | 219 | # Unit test / coverage reports 220 | .coverage 221 | .tox 222 | 223 | #Translations 224 | *.mo 225 | 226 | #Mr Developer 227 | .mr.developer.cfg 228 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 William Oemler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Spring-Blog 2 | =========== 3 | 4 | A generic blog application built using Spring Boot and MongoDB. A good example application for illustrating Spring Framework features, such as: 5 | 6 | - Spring MVC and JSP 7 | - Spring Data with MongoDB repositories. 8 | - Spring Security CSRF and CORS protections. 9 | - Simple content management and media sharing tools. 10 | - Testing and deployment with Spring Boot. 11 | 12 | ### Requirements 13 | 14 | - JDK 7+ 15 | - Maven 2+ 16 | - MongoDB 2+ (embedded database provided for testing) 17 | 18 | ### Quick Start 19 | - Clone this repository. 20 | - Make the appropriate changes to the properties files in `src/main/resources`. 21 | - Build the project and run tests by running: 22 | - `mvn clean install` 23 | - Run the application in development mode with an embedded database by running: 24 | - `mvn spring-boot:run` or `java -jar target/spring-blog.jar` 25 | - Run the application in production mode with a permanent database by running: 26 | - `java -jar target/spring-blog.jar --spring.profiles.active=prd` 27 | 28 | ### License 29 | 30 | This project is licensed under the terms of the MIT License. 31 | 32 | >The MIT License (MIT) 33 | > 34 | >Copyright (c) 2017 William Oemler 35 | > 36 | >Permission is hereby granted, free of charge, to any person obtaining a copy 37 | of this software and associated documentation files (the "Software"), to deal 38 | in the Software without restriction, including without limitation the rights 39 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 40 | copies of the Software, and to permit persons to whom the Software is 41 | furnished to do so, subject to the following conditions: 42 | 43 | >The above copyright notice and this permission notice shall be included in all 44 | copies or substantial portions of the Software. 45 | 46 | >THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 47 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 48 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 49 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 50 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 51 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 52 | SOFTWARE. 53 | -------------------------------------------------------------------------------- /create_admin_user.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | read -p "Enter desired username: " user 3 | if [ -z "$user" ]; then 4 | echo "Username must not be empty!" 5 | break; 6 | fi 7 | read -s -p "Enter password: " pw 8 | if [ -z "$pw" ]; then 9 | echo "Password must not be empty!" 10 | break; 11 | fi 12 | echo "" 13 | read -s -p "Enter password again : " pw2 14 | if [ "$pw" != "$pw2" ]; then 15 | echo "Passwords do not match!" 16 | break; 17 | fi 18 | echo "" 19 | read -p "Enter email: " email 20 | if [ -z "$email" ]; then 21 | echo "Email must not be empty!" 22 | break; 23 | fi 24 | mvn -e -q -f pom.xml exec:java -Dexec.mainClass="me.woemler.springblog.security.CreateAdminUser" -Dexec.args="$user $pw $email" 25 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | me.woemler 6 | spring-blog 7 | 1.0-SNAPSHOT 8 | jar 9 | 10 | Spring Blog 11 | 12 | 13 | Athens-RELEASE 14 | 15 | 16 | 17 | 18 | 19 | io.spring.platform 20 | platform-bom 21 | ${spring.platform.version} 22 | pom 23 | import 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-web 35 | 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-tomcat 40 | provided 41 | 42 | 43 | 44 | org.apache.tomcat.embed 45 | tomcat-embed-jasper 46 | provided 47 | 48 | 49 | 50 | org.springframework.boot 51 | spring-boot-starter-logging 52 | 53 | 54 | 55 | org.springframework.boot 56 | spring-boot-starter-ws 57 | 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-starter-actuator 62 | 63 | 64 | 65 | org.springframework.boot 66 | spring-boot-devtools 67 | true 68 | 69 | 70 | 71 | org.springframework.boot 72 | spring-boot-starter-security 73 | 74 | 75 | 76 | org.springframework.boot 77 | spring-boot-starter-hateoas 78 | 79 | 80 | 81 | org.springframework.data 82 | spring-data-mongodb 83 | 84 | 85 | 86 | de.flapdoodle.embed 87 | de.flapdoodle.embed.mongo 88 | 1.50.5 89 | 90 | 91 | 92 | cz.jirutka.spring 93 | embedmongo-spring 94 | 1.3.1 95 | 96 | 97 | 98 | 99 | 100 | com.fasterxml.jackson.core 101 | jackson-annotations 102 | 103 | 104 | 105 | 106 | 107 | javax.servlet 108 | jstl 109 | 110 | 111 | 112 | org.pegdown 113 | pegdown 114 | 1.4.1 115 | 116 | 117 | 118 | com.google.code.gson 119 | gson 120 | 121 | 122 | 123 | 124 | 125 | org.hamcrest 126 | hamcrest-all 127 | test 128 | 129 | 130 | 131 | junit 132 | junit 133 | test 134 | 135 | 136 | hamcrest-core 137 | org.hamcrest 138 | 139 | 140 | 141 | 142 | 143 | org.mockito 144 | mockito-core 145 | test 146 | 147 | 148 | 149 | org.springframework 150 | spring-test 151 | test 152 | 153 | 154 | 155 | com.jayway.jsonpath 156 | json-path 157 | 1.2.0 158 | test 159 | 160 | 161 | 162 | com.jayway.jsonpath 163 | json-path-assert 164 | 1.2.0 165 | test 166 | 167 | 168 | 169 | 170 | 171 | spring-blog 172 | 173 | 174 | maven-surefire-plugin 175 | 2.18.1 176 | 177 | 178 | **/*Tests.java 179 | 180 | 181 | 182 | 183 | org.springframework.boot 184 | spring-boot-maven-plugin 185 | 186 | me.woemler.springblog.Application 187 | 188 | 189 | 190 | org.apache.maven.plugins 191 | maven-compiler-plugin 192 | 193 | 1.7 194 | 1.7 195 | 196 | 197 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /src/main/java/me/woemler/springblog/Application.java: -------------------------------------------------------------------------------- 1 | package me.woemler.springblog; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.builder.SpringApplicationBuilder; 6 | import org.springframework.boot.context.web.SpringBootServletInitializer; 7 | 8 | /** 9 | * @author woemler 10 | */ 11 | 12 | @SpringBootApplication 13 | public class Application extends SpringBootServletInitializer { 14 | 15 | @Override 16 | protected SpringApplicationBuilder configure(SpringApplicationBuilder application){ 17 | return application.sources(Application.class); 18 | } 19 | 20 | public static void main(String[] args){ 21 | SpringApplication.run(Application.class, args); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/me/woemler/springblog/config/AdminUserInitializer.java: -------------------------------------------------------------------------------- 1 | package me.woemler.springblog.config; 2 | 3 | import me.woemler.springblog.models.User; 4 | import me.woemler.springblog.repositories.UserRepository; 5 | 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.CommandLineRunner; 10 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 11 | import org.springframework.stereotype.Component; 12 | 13 | import java.util.Arrays; 14 | import java.util.Date; 15 | import java.util.HashSet; 16 | import java.util.UUID; 17 | 18 | /** 19 | * @author woemler 20 | */ 21 | @Component 22 | public class AdminUserInitializer implements CommandLineRunner { 23 | 24 | @Autowired private UserRepository userRepository; 25 | 26 | private static final Logger logger = LoggerFactory.getLogger(AdminUserInitializer.class); 27 | 28 | public void run(String[] args) throws Exception { 29 | 30 | BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); 31 | 32 | if (userRepository.loadUserByUsername("admin") != null){ 33 | logger.info(String.format("ERROR: User %s already exists!", "admin")); 34 | return; 35 | } 36 | 37 | String password = UUID.randomUUID().toString(); 38 | User user = new User(); 39 | user.setUsername("admin"); 40 | user.setPassword(encoder.encode(password)); 41 | user.setRegistrationDate(new Date()); 42 | user.setRoles(new HashSet<>(Arrays.asList(User.ROLE_USER, User.ROLE_ADMIN))); 43 | userRepository.insert(user); 44 | 45 | logger.info(String.format("SUCCESS: Created admin user with username %s and password %s", "admin", password)); 46 | 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/me/woemler/springblog/config/BlogPostInitializer.java: -------------------------------------------------------------------------------- 1 | package me.woemler.springblog.config; 2 | 3 | import org.pegdown.PegDownProcessor; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.CommandLineRunner; 8 | import org.springframework.core.env.Environment; 9 | import org.springframework.stereotype.Component; 10 | 11 | import java.util.Arrays; 12 | import java.util.Date; 13 | import java.util.HashSet; 14 | 15 | import me.woemler.springblog.models.BlogPost; 16 | import me.woemler.springblog.repositories.BlogRepository; 17 | 18 | /** 19 | * @author woemler 20 | */ 21 | @Component 22 | public class BlogPostInitializer implements CommandLineRunner { 23 | 24 | @Autowired private Environment env; 25 | @Autowired private BlogRepository blogRepository; 26 | 27 | private static final Logger logger = LoggerFactory.getLogger(BlogPostInitializer.class); 28 | 29 | @Override 30 | public void run(String... strings) throws Exception { 31 | if (Arrays.asList(env.getActiveProfiles()).contains("dev")){ 32 | String content = "Welcome to your new Spring Boot and MongoDB powered blog! If you are seeing this post, your application is currently in development mode. In this mode, the application uses an embedded MongoDB instance that will be created and destroyed with the application context. Any new posts you create will not persist beyond the life of the application. You can login the admin page of the blog application [here](/login). You can find the username and password for the default admin user in the application log:\n" + 33 | "\n" + 34 | " Created admin user with username admin and password dd191771-c9d6-4ddf-aa55-f3a7daabf32d\n" + 35 | "\n" + 36 | "\n" + 37 | "To switch to production mode, you can change the active profile in the `application.properties` file:\n" + 38 | "\n" + 39 | " spring.profiles.active=prd\n" + 40 | "\n" + 41 | "Or, you can set the profile from the command line when your start the application:\n" + 42 | "\n" + 43 | " java -jar blog.jar --spring.profiles.active=prd\n" + 44 | "\n" + 45 | "Be sure to update the properties in the `application.properties` and `application-prd.properties` files to suit your needs."; 46 | 47 | BlogPost post = new BlogPost(); 48 | post.setEnableComments(false); 49 | post.setPostDate(new Date()); 50 | post.setStatus(BlogPost.STATUS_ACTIVE); 51 | post.setSlug("welcome-to-your-new-blog"); 52 | post.setTags(new HashSet<>(Arrays.asList("welcome"))); 53 | post.setContent(content); 54 | post.setTitle("Welcome to your new blog!"); 55 | blogRepository.insert(post); 56 | logger.info("Created default blog post."); 57 | } else { 58 | logger.info("Application is not in development mode, no default posts created."); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/me/woemler/springblog/config/DataSourceConfig.java: -------------------------------------------------------------------------------- 1 | package me.woemler.springblog.config; 2 | 3 | import com.mongodb.Mongo; 4 | import com.mongodb.MongoClient; 5 | import com.mongodb.MongoCredential; 6 | import com.mongodb.ServerAddress; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.ComponentScan; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.context.annotation.Profile; 12 | import org.springframework.context.annotation.PropertySource; 13 | import org.springframework.core.env.Environment; 14 | import org.springframework.data.mongodb.config.AbstractMongoConfiguration; 15 | import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; 16 | 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | 20 | /** 21 | * @author woemler 22 | */ 23 | 24 | @Configuration 25 | @Profile({ "prd" }) 26 | @PropertySource({"classpath:data-source.properties"}) 27 | @ComponentScan(basePackages = { "me.woemler.springblog.services" }) 28 | @EnableMongoRepositories(basePackages = "me.woemler.springblog.repositories") 29 | public class DataSourceConfig extends AbstractMongoConfiguration { 30 | 31 | @Autowired private Environment env; 32 | 33 | @Override 34 | public String getDatabaseName(){ 35 | return env.getRequiredProperty("mongo.name"); 36 | } 37 | 38 | @Override 39 | @Bean 40 | public Mongo mongo() throws Exception { 41 | ServerAddress serverAddress = new ServerAddress(env.getRequiredProperty("mongo.host")); 42 | List credentials = new ArrayList<>(); 43 | return new MongoClient(serverAddress, credentials); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/me/woemler/springblog/config/EmbeddedDataSourceConfig.java: -------------------------------------------------------------------------------- 1 | package me.woemler.springblog.config; 2 | 3 | import com.mongodb.Mongo; 4 | 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.ComponentScan; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.context.annotation.Profile; 9 | import org.springframework.data.mongodb.core.MongoTemplate; 10 | import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; 11 | 12 | import java.io.IOException; 13 | 14 | import cz.jirutka.spring.embedmongo.EmbeddedMongoBuilder; 15 | 16 | /** 17 | * @author woemler 18 | */ 19 | @Configuration 20 | @Profile({ "dev", "default" }) 21 | @ComponentScan(basePackages = { "me.woemler.springblog.services" }) 22 | @EnableMongoRepositories(basePackages = "me.woemler.springblog.repositories") 23 | public class EmbeddedDataSourceConfig { 24 | 25 | @Bean(destroyMethod = "close") 26 | public Mongo mongo() throws IOException { 27 | return new EmbeddedMongoBuilder().build(); 28 | } 29 | 30 | @Bean 31 | public MongoTemplate mongoTemplate(Mongo mongo){ 32 | return new MongoTemplate(mongo, "blog"); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/me/woemler/springblog/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package me.woemler.springblog.config; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.context.annotation.Import; 7 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 8 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 10 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 11 | import org.springframework.security.core.userdetails.UserDetailsService; 12 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 13 | 14 | import me.woemler.springblog.repositories.UserRepository; 15 | 16 | /** 17 | * @author woemler 18 | * 19 | */ 20 | 21 | @Configuration 22 | @EnableWebSecurity 23 | @Import(DataSourceConfig.class) 24 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 25 | 26 | @SuppressWarnings("SpringJavaAutowiringInspection") 27 | @Autowired 28 | private UserRepository userRepository; 29 | 30 | @Autowired 31 | public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { 32 | auth 33 | .userDetailsService(userRepository) 34 | .passwordEncoder(new BCryptPasswordEncoder()); 35 | } 36 | 37 | @Override 38 | protected void configure(HttpSecurity http) throws Exception { 39 | http 40 | .authorizeRequests() 41 | .antMatchers("/", "/blog/**", "/static/**", "/about", "/code", "/media", "/error").permitAll() 42 | .antMatchers("/admin/**").hasRole("ADMIN") 43 | .anyRequest().authenticated() 44 | .and() 45 | .formLogin() 46 | .loginPage("/login") 47 | .permitAll() 48 | .defaultSuccessUrl("/admin") 49 | .failureUrl("/login") 50 | .and() 51 | .logout() 52 | .logoutUrl("/logout") 53 | .logoutSuccessUrl("/login"); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/me/woemler/springblog/config/WebAppConfig.java: -------------------------------------------------------------------------------- 1 | package me.woemler.springblog.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.data.web.config.EnableSpringDataWebSupport; 5 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 7 | 8 | /** 9 | * @author woemler 10 | */ 11 | 12 | @Configuration 13 | @EnableSpringDataWebSupport 14 | public class WebAppConfig extends WebMvcConfigurerAdapter { 15 | 16 | @Override 17 | public void addCorsMappings(CorsRegistry registry) { 18 | registry.addMapping("/api/**"); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/me/woemler/springblog/controllers/AdminController.java: -------------------------------------------------------------------------------- 1 | 2 | package me.woemler.springblog.controllers; 3 | 4 | import me.woemler.springblog.models.BlogPost; 5 | import me.woemler.springblog.repositories.BlogRepository; 6 | import org.pegdown.PegDownProcessor; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Controller; 11 | import org.springframework.ui.Model; 12 | import org.springframework.ui.ModelMap; 13 | import org.springframework.validation.BindingResult; 14 | import org.springframework.web.bind.annotation.*; 15 | import org.springframework.web.bind.support.SessionStatus; 16 | 17 | import javax.validation.Valid; 18 | import java.util.*; 19 | 20 | @Controller 21 | @RequestMapping("/admin") 22 | @SessionAttributes("blogPost") 23 | public class AdminController { 24 | 25 | private static Logger logger = LoggerFactory.getLogger(AdminController.class); 26 | 27 | @Autowired BlogRepository blogRepository; 28 | 29 | @ModelAttribute("blogPost") 30 | public BlogPost getBlogPost(){ 31 | return new BlogPost(); 32 | } 33 | 34 | //Admin Home 35 | @RequestMapping(value="") 36 | public String admin(ModelMap map){ 37 | Long postCount = blogRepository.count(); 38 | Integer tagCount = blogRepository.findAllTags().size(); 39 | map.addAttribute("postCount", postCount); 40 | map.addAttribute("tagCount", tagCount); 41 | return "admin/admin"; 42 | } 43 | 44 | //List Blog Posts 45 | @RequestMapping(value="/blog", method= RequestMethod.GET) 46 | public String blogAdmin(ModelMap map, SessionStatus status){ 47 | status.setComplete(); 48 | List postList = blogRepository.findAll(); 49 | map.addAttribute("postList", postList); 50 | return "admin/blogPostList"; 51 | } 52 | 53 | //Add new blog post 54 | @RequestMapping(value="/blog/new", method=RequestMethod.GET) 55 | public String newPost(ModelMap map){ 56 | BlogPost blogPost = new BlogPost(); 57 | map.addAttribute("blogPost", blogPost); 58 | return "admin/editBlogPost"; 59 | } 60 | 61 | //Save new post 62 | @RequestMapping(value="/blog/new", method=RequestMethod.POST) 63 | public String addPost(@Valid @ModelAttribute BlogPost blogPost, 64 | BindingResult result, 65 | @RequestParam(value="tagString", defaultValue="") String tagString, 66 | @RequestParam(value="enablePegdown", required=false) boolean enablePegdown, 67 | Model model, 68 | SessionStatus status) 69 | { 70 | if (result.hasErrors()){ 71 | return "admin/editBlogPost"; 72 | } 73 | Set tagSet = new HashSet<>(); 74 | for (String tag: tagString.split(",")){ 75 | tag = tag.replaceAll("\\s+", ""); 76 | if (tag != null && !tag.equals("")){ 77 | tagSet.add(tag); 78 | } 79 | } 80 | blogPost.setEnablePegdown(enablePegdown); 81 | blogPost.setPostDate(new Date()); 82 | blogPost.setTags(tagSet); 83 | blogRepository.save(blogPost); 84 | status.setComplete(); 85 | return "redirect:/admin/blog"; 86 | } 87 | 88 | //Edit existing blog post 89 | @RequestMapping(value="/blog/{id}", method=RequestMethod.GET) 90 | public String editPost(ModelMap map, @PathVariable("id") String postId){ 91 | BlogPost blogPost = blogRepository.findOne(postId); 92 | map.addAttribute("blogPost", blogPost); 93 | Set tags = blogPost.getTags(); 94 | String tagString = ""; 95 | for (String tag: tags){ 96 | tagString = tagString + " " + tag; 97 | } 98 | tagString = tagString.trim(); 99 | map.addAttribute("tagString", tagString); 100 | return "admin/editBlogPost"; 101 | } 102 | 103 | //Update post 104 | @RequestMapping(value="/blog/{id}", method=RequestMethod.POST) 105 | public String savePostChanges( 106 | @Valid @ModelAttribute BlogPost blogPost, 107 | BindingResult result, 108 | @RequestParam(value="tagString", defaultValue="") String tagString, 109 | @RequestParam("enablePegdown") boolean enablePegdown, 110 | Model model, 111 | SessionStatus status) 112 | { 113 | if (result.hasErrors()){ 114 | return "admin/editBlogPost"; 115 | } 116 | Set tagSet = new HashSet(); 117 | for (String tag: tagString.split(",")){ 118 | tag = tag.replaceAll("\\s+", ""); 119 | if (!tag.equals("") && tag != null){ 120 | tagSet.add(tag); 121 | } 122 | } 123 | if (enablePegdown){ 124 | blogPost.setContent(new PegDownProcessor().markdownToHtml(blogPost.getContent())); 125 | } 126 | blogPost.setTags(tagSet); 127 | blogPost.setPostDate(new Date()); 128 | blogRepository.save(blogPost); 129 | status.setComplete(); 130 | return "redirect:/admin/blog"; 131 | } 132 | 133 | //Delete blog post 134 | @RequestMapping(value="/blog/delete/{id}", method=RequestMethod.POST) 135 | public @ResponseBody String deleteBlogPost(@PathVariable("id") String id, SessionStatus status){ 136 | blogRepository.delete(id); 137 | status.setComplete(); 138 | return "The item was deleted succesfully"; 139 | } 140 | 141 | //Cancel post edit 142 | @RequestMapping(value="/blog/cancel", method=RequestMethod.GET) 143 | public String cancelBlogEdit(SessionStatus status){ 144 | status.setComplete(); 145 | return "redirect:/admin/blog"; 146 | } 147 | 148 | //Tag list 149 | @RequestMapping(value="/tags", method=RequestMethod.GET) 150 | public String adminTagList(ModelMap map, SessionStatus status){ 151 | status.setComplete(); 152 | List posts = blogRepository.findAll(); 153 | Map tagCounts = new HashMap<>(); 154 | for (BlogPost post: posts){ 155 | for (String tag: post.getTags()){ 156 | if (!tagCounts.containsKey(tag)){ 157 | tagCounts.put(tag, 0); 158 | } 159 | Integer count = tagCounts.get(tag); 160 | tagCounts.put(tag, count + 1); 161 | } 162 | } 163 | map.addAttribute("tags", tagCounts.keySet()); 164 | map.addAttribute("tagCounts", tagCounts); 165 | return "admin/tagList"; 166 | } 167 | 168 | } 169 | -------------------------------------------------------------------------------- /src/main/java/me/woemler/springblog/controllers/BlogController.java: -------------------------------------------------------------------------------- 1 | 2 | package me.woemler.springblog.controllers; 3 | 4 | import me.woemler.springblog.models.BlogPost; 5 | import me.woemler.springblog.repositories.BlogRepository; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.data.domain.Page; 10 | import org.springframework.data.domain.Pageable; 11 | import org.springframework.data.domain.Sort; 12 | import org.springframework.data.web.PageableDefault; 13 | import org.springframework.stereotype.Controller; 14 | import org.springframework.ui.ModelMap; 15 | import org.springframework.web.bind.annotation.PathVariable; 16 | import org.springframework.web.bind.annotation.RequestMapping; 17 | import org.springframework.web.bind.annotation.RequestMethod; 18 | 19 | import java.util.*; 20 | 21 | @Controller 22 | public class BlogController { 23 | 24 | private static Logger logger = LoggerFactory.getLogger(BlogController.class); 25 | 26 | @Autowired private BlogRepository blogRepository; 27 | 28 | @RequestMapping(value={"/", "", "/blog"}, method=RequestMethod.GET) 29 | public String home( 30 | @PageableDefault(size = 5, sort = {"postDate"}, direction = Sort.Direction.DESC) Pageable pageable, 31 | ModelMap map 32 | ) { 33 | Page posts = blogRepository.findByStatusOrderByPostDateAsc(BlogPost.STATUS_ACTIVE, pageable); 34 | List blogPosts = new ArrayList<>(posts.getContent()); 35 | map.addAttribute("currentPage", pageable.getPageNumber() + 1); 36 | map.addAttribute("numPages", posts.getTotalPages()); 37 | map.addAttribute("postCount", posts.getTotalElements()); 38 | map.addAttribute("blogPosts", blogPosts); 39 | return "home"; 40 | } 41 | 42 | @RequestMapping(value="/blog/{slug}") 43 | public String post(ModelMap map, @PathVariable("slug") String slug){ 44 | BlogPost post = blogRepository.findBySlug(slug); 45 | map.addAttribute("requestedPost", post); 46 | map.addAttribute("nextPost", null); 47 | map.addAttribute("previousPost", null); 48 | return "blogPost"; 49 | } 50 | 51 | @RequestMapping(value="/tags", method=RequestMethod.GET) 52 | public String listTags(ModelMap map){ 53 | List tags = blogRepository.findAllTags(); 54 | List posts = blogRepository.findAll(); 55 | Map tagCounts = new HashMap<>(); 56 | for (BlogPost post: posts){ 57 | for (String tag: post.getTags()){ 58 | if (!tagCounts.containsKey(tag)){ 59 | tagCounts.put(tag, 0); 60 | } 61 | Integer count = tagCounts.get(tag); 62 | tagCounts.put(tag, count + 1); 63 | } 64 | } 65 | map.addAttribute("tags", tagCounts.keySet()); 66 | map.addAttribute("tagCounts", tagCounts); 67 | return "tagList"; 68 | } 69 | 70 | @RequestMapping(value="/tags/{tag}", method=RequestMethod.GET) 71 | public String tagDetails(ModelMap map, @PathVariable("tag") String tag){ 72 | List blogPosts = blogRepository.findByTags(tag); 73 | map.addAttribute("tag", tag); 74 | map.addAttribute("blogPosts", blogPosts); 75 | return "tagDetails"; 76 | } 77 | 78 | //About page 79 | @RequestMapping(value="/about") 80 | public String about(ModelMap map){ 81 | return "about"; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/me/woemler/springblog/controllers/CodeController.java: -------------------------------------------------------------------------------- 1 | 2 | package me.woemler.springblog.controllers; 3 | 4 | import me.woemler.springblog.services.GitHubService; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.stereotype.Controller; 10 | import org.springframework.ui.ModelMap; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RequestMethod; 13 | 14 | import java.net.MalformedURLException; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | @Controller 19 | public class CodeController { 20 | 21 | private static Logger logger = LoggerFactory.getLogger(CodeController.class); 22 | 23 | @Autowired 24 | private GitHubService ghService; 25 | 26 | @Value("${github.username}") 27 | String githubUsername; 28 | 29 | @RequestMapping(value="/code", method=RequestMethod.GET) 30 | public String codePage(ModelMap map) 31 | throws MalformedURLException, Exception 32 | { 33 | List> repos = ghService.getReposByUser(githubUsername); 34 | map.addAttribute("repos", repos); 35 | return "code"; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/me/woemler/springblog/controllers/LoginController.java: -------------------------------------------------------------------------------- 1 | 2 | package me.woemler.springblog.controllers; 3 | 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.ui.ModelMap; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RequestMethod; 10 | 11 | @Controller 12 | public class LoginController { 13 | 14 | private static Logger logger = LoggerFactory.getLogger(LoginController.class); 15 | 16 | @RequestMapping(value="/login", method=RequestMethod.GET) 17 | public String login(ModelMap map){ 18 | return "login"; 19 | } 20 | 21 | @RequestMapping(value= {"/loginfailed"}, method=RequestMethod.GET) 22 | public String loginError(ModelMap map){ 23 | map.addAttribute("error", "true"); 24 | return "login"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/me/woemler/springblog/controllers/MediaController.java: -------------------------------------------------------------------------------- 1 | 2 | package me.woemler.springblog.controllers; 3 | 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.ui.ModelMap; 9 | import org.springframework.web.bind.annotation.RequestMapping; 10 | 11 | import javax.servlet.ServletContext; 12 | import java.io.File; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | /** 17 | * @author woemler 18 | * 19 | * This controller class handles requests for special media pages, such as a gallery of public image 20 | * thumbnails. 21 | */ 22 | 23 | @Controller 24 | public class MediaController { 25 | 26 | private static Logger logger = LoggerFactory.getLogger(MediaController.class); 27 | 28 | @Autowired 29 | private ServletContext servletContext; 30 | 31 | @RequestMapping(value="/media") 32 | public String media(ModelMap map){ 33 | String contextPath = servletContext.getContextPath(); 34 | String staticDir = "/static/img/public/"; 35 | String staticPath = servletContext.getRealPath(staticDir); 36 | List images = new ArrayList(); 37 | File staticFileDir = new File(staticPath); 38 | if (staticFileDir.list() != null && staticFileDir.list().length > 0){ 39 | for (String file: staticFileDir.list()){ 40 | if (file.matches("[a-zA-Z0-9._-]+\\.(jpg|png|gif|svg)$")){ 41 | images.add(contextPath + staticDir + file); 42 | } 43 | } 44 | } 45 | map.addAttribute("images", images); 46 | return "media"; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/me/woemler/springblog/models/BlogPost.java: -------------------------------------------------------------------------------- 1 | 2 | package me.woemler.springblog.models; 3 | 4 | import org.pegdown.PegDownProcessor; 5 | import org.springframework.data.annotation.Id; 6 | import org.springframework.data.annotation.PersistenceConstructor; 7 | import org.springframework.data.mongodb.core.index.Indexed; 8 | import org.springframework.data.mongodb.core.mapping.Document; 9 | 10 | import javax.validation.constraints.NotNull; 11 | import java.text.DateFormat; 12 | import java.text.SimpleDateFormat; 13 | import java.util.*; 14 | 15 | @Document(collection = "blog_posts") 16 | public class BlogPost { 17 | 18 | @Id private String postId; 19 | @NotNull private String title; 20 | @NotNull @Indexed(unique = true) private String slug; 21 | @NotNull private String content; 22 | private Date postDate = new Date(); 23 | private String status = BlogPost.STATUS_INACTIVE; 24 | private boolean enableComments = false; 25 | private boolean enablePegdown = true; 26 | private Set tags = new HashSet<>(); 27 | 28 | public BlogPost() { } 29 | 30 | @PersistenceConstructor 31 | public BlogPost(String postId, String title, String slug, String content, Date postDate, 32 | String status, boolean enableComments, Set tags) { 33 | this.postId = postId; 34 | this.title = title; 35 | this.slug = slug; 36 | this.content = content; 37 | this.postDate = postDate; 38 | this.status = status; 39 | this.enableComments = enableComments; 40 | this.tags = tags; 41 | } 42 | 43 | public String getPostId() { 44 | return postId; 45 | } 46 | 47 | public void setPostId(String postId) { 48 | this.postId = postId; 49 | } 50 | 51 | public String getTitle() { 52 | return title; 53 | } 54 | 55 | public void setTitle(String title) { 56 | this.title = title; 57 | } 58 | 59 | public String getSlug() { 60 | return slug; 61 | } 62 | 63 | public void setSlug(String slug) { 64 | this.slug = slug; 65 | } 66 | 67 | public String getContent() { 68 | return content; 69 | } 70 | 71 | public void setContent(String content) { 72 | this.content = content; 73 | } 74 | 75 | public Date getPostDate() { 76 | return postDate; 77 | } 78 | 79 | public void setPostDate(Date postDate) { 80 | this.postDate = postDate; 81 | } 82 | 83 | public String getStatus() { 84 | return status; 85 | } 86 | 87 | public void setStatus(String status) { 88 | this.status = status; 89 | } 90 | 91 | public boolean isEnableComments() { 92 | return enableComments; 93 | } 94 | 95 | public void setEnableComments(boolean enableComments) { 96 | this.enableComments = enableComments; 97 | } 98 | 99 | public boolean isEnablePegdown() { 100 | return enablePegdown; 101 | } 102 | 103 | public void setEnablePegdown(boolean enablePegdown) { 104 | this.enablePegdown = enablePegdown; 105 | } 106 | 107 | public Set getTags() { 108 | return tags; 109 | } 110 | 111 | public void setTags(Set tags) { 112 | this.tags = tags; 113 | } 114 | 115 | public String printPostDate(){ 116 | DateFormat df = new SimpleDateFormat("MMM d yyyy hh:mm aaa"); 117 | return df.format(this.postDate.getTime()); 118 | } 119 | 120 | public List getTagList(){ 121 | return new ArrayList<>(tags); 122 | } 123 | 124 | public String getMarkup(){ 125 | if (enablePegdown){ 126 | return new PegDownProcessor().markdownToHtml(content); 127 | } else { 128 | return content; 129 | } 130 | } 131 | 132 | /** 133 | * Return the first paragraph of a blog post 134 | */ 135 | public String getPostPreview(){ 136 | String[] paragraphs = this.getMarkup().split("

"); 137 | String paragraph = paragraphs[0] + "

"; 138 | if (paragraphs.length>1){ 139 | paragraph = paragraph + "

More...

"; 140 | } 141 | return paragraph; 142 | } 143 | 144 | public static final String STATUS_ACTIVE = "active"; 145 | public static final String STATUS_INACTIVE = "inactive"; 146 | 147 | @Override public String toString() { 148 | return "BlogPost{" + 149 | "postId='" + postId + '\'' + 150 | ", title='" + title + '\'' + 151 | ", slug='" + slug + '\'' + 152 | ", content='" + content + '\'' + 153 | ", postDate=" + postDate + 154 | ", status='" + status + '\'' + 155 | ", enableComments=" + enableComments + 156 | ", tags=" + tags + 157 | '}'; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/main/java/me/woemler/springblog/models/User.java: -------------------------------------------------------------------------------- 1 | package me.woemler.springblog.models; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.annotation.PersistenceConstructor; 5 | import org.springframework.data.mongodb.core.index.Indexed; 6 | import org.springframework.data.mongodb.core.mapping.Document; 7 | import org.springframework.security.core.GrantedAuthority; 8 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 9 | import org.springframework.security.core.userdetails.UserDetails; 10 | 11 | import javax.validation.constraints.NotNull; 12 | import java.util.*; 13 | 14 | /** 15 | * @author woemler 16 | */ 17 | 18 | @Document(collection = "users") 19 | public class User implements UserDetails { 20 | 21 | @Id private String userId; 22 | @NotNull @Indexed(unique = true) private String username; 23 | @NotNull private String password; 24 | @NotNull private String name; 25 | @NotNull private String email; 26 | @NotNull private Date registrationDate = new Date(); 27 | private Set roles = new HashSet<>(); 28 | private boolean accountNonExpired = true; 29 | private boolean accountNonLocked = true; 30 | private boolean enabled = true; 31 | private boolean credentialsNonExpired = true; 32 | 33 | public User() { } 34 | 35 | @PersistenceConstructor 36 | public User(String userId, String username, String password, String name, String email, 37 | Date registrationDate, Set roles, boolean accountNonExpired, boolean accountNonLocked, 38 | boolean enabled, boolean credentialsNonExpired) { 39 | this.userId = userId; 40 | this.username = username; 41 | this.password = password; 42 | this.name = name; 43 | this.email = email; 44 | this.registrationDate = registrationDate; 45 | this.roles = roles; 46 | this.accountNonExpired = accountNonExpired; 47 | this.accountNonLocked = accountNonLocked; 48 | this.enabled = enabled; 49 | this.credentialsNonExpired = credentialsNonExpired; 50 | } 51 | 52 | public String getUserId() { 53 | return userId; 54 | } 55 | 56 | public void setUserId(String userId) { 57 | this.userId = userId; 58 | } 59 | 60 | public String getUsername() { 61 | return username; 62 | } 63 | 64 | public void setUsername(String username) { 65 | this.username = username; 66 | } 67 | 68 | public String getPassword() { 69 | return password; 70 | } 71 | 72 | public void setPassword(String password) { 73 | this.password = password; 74 | } 75 | 76 | public String getName() { 77 | return name; 78 | } 79 | 80 | public void setName(String name) { 81 | this.name = name; 82 | } 83 | 84 | public String getEmail() { 85 | return email; 86 | } 87 | 88 | public void setEmail(String email) { 89 | this.email = email; 90 | } 91 | 92 | public Date getRegistrationDate() { 93 | return registrationDate; 94 | } 95 | 96 | public void setRegistrationDate(Date registrationDate) { 97 | this.registrationDate = registrationDate; 98 | } 99 | 100 | public Set getRoles() { 101 | return roles; 102 | } 103 | 104 | public void setRoles(Set roles) { 105 | this.roles = roles; 106 | } 107 | 108 | @Override 109 | public Collection getAuthorities() { 110 | if (roles == null){ 111 | return Collections.emptyList(); 112 | } 113 | Set authorities = new HashSet<>(); 114 | for (String role: roles){ 115 | authorities.add(new SimpleGrantedAuthority(role)); 116 | } 117 | return authorities; 118 | } 119 | 120 | @Override 121 | public boolean isAccountNonExpired() { 122 | return accountNonExpired; 123 | } 124 | 125 | @Override 126 | public boolean isAccountNonLocked() { 127 | return accountNonLocked; 128 | } 129 | 130 | @Override 131 | public boolean isCredentialsNonExpired() { 132 | return credentialsNonExpired; 133 | } 134 | 135 | @Override 136 | public boolean isEnabled() { 137 | return enabled; 138 | } 139 | 140 | public void setAccountNonExpired(boolean accountNonExpired) { 141 | this.accountNonExpired = accountNonExpired; 142 | } 143 | 144 | public void setAccountNonLocked(boolean accountNonLocked) { 145 | this.accountNonLocked = accountNonLocked; 146 | } 147 | 148 | public void setEnabled(boolean enabled) { 149 | this.enabled = enabled; 150 | } 151 | 152 | public void setCredentialsNonExpired(boolean credentialsNonExpired) { 153 | this.credentialsNonExpired = credentialsNonExpired; 154 | } 155 | 156 | public static final String ROLE_ADMIN = "ROLE_ADMIN"; 157 | public static final String ROLE_USER = "ROLE_USER"; 158 | 159 | @Override 160 | public String toString() { 161 | return "User{" + 162 | "userId=" + userId + 163 | ", username='" + username + '\'' + 164 | ", password='" + password + '\'' + 165 | ", name='" + name + '\'' + 166 | ", email='" + email + '\'' + 167 | ", registrationDate=" + registrationDate + 168 | ", roles=" + roles + 169 | '}'; 170 | } 171 | 172 | 173 | 174 | } 175 | -------------------------------------------------------------------------------- /src/main/java/me/woemler/springblog/repositories/BlogOperations.java: -------------------------------------------------------------------------------- 1 | package me.woemler.springblog.repositories; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * @author woemler 7 | */ 8 | public interface BlogOperations { 9 | List findTagsByFragment(String fragment); 10 | List findAllTags(); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/me/woemler/springblog/repositories/BlogRepository.java: -------------------------------------------------------------------------------- 1 | 2 | package me.woemler.springblog.repositories; 3 | 4 | import me.woemler.springblog.models.BlogPost; 5 | 6 | import org.springframework.data.domain.Page; 7 | import org.springframework.data.domain.Pageable; 8 | import org.springframework.data.mongodb.repository.MongoRepository; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * @author woemler 14 | */ 15 | 16 | public interface BlogRepository extends MongoRepository, BlogOperations { 17 | List findByStatusOrderByPostDateAsc(String status); 18 | Page findByStatusOrderByPostDateAsc(String status, Pageable pageable); 19 | BlogPost findBySlug(String slug); 20 | List findByTags(String tag); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/me/woemler/springblog/repositories/BlogRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package me.woemler.springblog.repositories; 2 | 3 | import com.mongodb.DBCollection; 4 | import me.woemler.springblog.models.BlogPost; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.data.mongodb.core.MongoOperations; 8 | import org.springframework.data.mongodb.core.MongoTemplate; 9 | import org.springframework.data.mongodb.core.query.Criteria; 10 | import org.springframework.data.mongodb.core.query.Query; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.regex.Pattern; 15 | 16 | /** 17 | * @author woemler 18 | */ 19 | public class BlogRepositoryImpl implements BlogOperations { 20 | 21 | @Autowired private MongoTemplate mongoTemplate; 22 | 23 | private DBCollection getCollection(){ 24 | return mongoTemplate.getCollection(mongoTemplate.getCollectionName(BlogPost.class)); 25 | } 26 | 27 | @Override 28 | public List findTagsByFragment(String fragment) { 29 | Pattern pattern = Pattern.compile("^.*?" + fragment + ".*$", Pattern.CASE_INSENSITIVE); 30 | List matched = new ArrayList<>(); 31 | for (String tag: this.findAllTags()){ 32 | if (pattern.matcher(tag).find()){ 33 | matched.add(tag); 34 | } 35 | } 36 | return matched; 37 | } 38 | 39 | @Override 40 | public List findAllTags() { 41 | return getCollection().distinct("tags"); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/me/woemler/springblog/repositories/UserRepository.java: -------------------------------------------------------------------------------- 1 | package me.woemler.springblog.repositories; 2 | 3 | import me.woemler.springblog.models.User; 4 | import org.springframework.data.mongodb.repository.MongoRepository; 5 | import org.springframework.security.core.userdetails.UserDetailsService; 6 | 7 | /** 8 | * @author woemler 9 | */ 10 | public interface UserRepository extends MongoRepository, UserDetailsService { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/me/woemler/springblog/repositories/UserRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package me.woemler.springblog.repositories; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.data.mongodb.core.MongoTemplate; 5 | import org.springframework.data.mongodb.core.query.Criteria; 6 | import org.springframework.data.mongodb.core.query.Query; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | import org.springframework.security.core.userdetails.UserDetailsService; 9 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 10 | 11 | import me.woemler.springblog.models.User; 12 | 13 | /** 14 | * @author woemler 15 | */ 16 | public class UserRepositoryImpl implements UserDetailsService { 17 | 18 | @Autowired private MongoTemplate mongoTemplate; 19 | 20 | @Override 21 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 22 | return mongoTemplate.findOne(new Query(Criteria.where("username").is(username)), User.class); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/me/woemler/springblog/services/GitHubService.java: -------------------------------------------------------------------------------- 1 | 2 | package me.woemler.springblog.services; 3 | 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.http.*; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.web.client.RestClientException; 10 | import org.springframework.web.client.RestTemplate; 11 | 12 | import java.io.IOException; 13 | import java.net.MalformedURLException; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | @Service 19 | public class GitHubService { 20 | 21 | private static Logger logger = LoggerFactory.getLogger(GitHubService.class); 22 | 23 | private RestTemplate restTemplate = new RestTemplate(); 24 | 25 | private HttpEntity createRequest(){ 26 | HttpHeaders headers = new HttpHeaders(); 27 | headers.add("Accept", MediaType.APPLICATION_JSON_VALUE); 28 | return new HttpEntity(headers); 29 | } 30 | 31 | private boolean isError(HttpStatus status) { 32 | HttpStatus.Series series = status.series(); 33 | return !status.equals(HttpStatus.NOT_FOUND) && (HttpStatus.Series.SERVER_ERROR.equals(series) || HttpStatus.Series.CLIENT_ERROR 34 | .equals(series)); 35 | } 36 | 37 | public List> getReposByUser(String user) 38 | throws MalformedURLException, Exception 39 | { 40 | List> repos = new ArrayList(); 41 | String url = "https://api.github.com/users/" + user + "/repos"; 42 | ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, createRequest(), String.class); 43 | try { 44 | if (isError(response.getStatusCode())) { 45 | throw new RestClientException("[" + response.getStatusCode() + "] " + response.toString()); 46 | } else { 47 | ObjectMapper objectMapper = new ObjectMapper(); 48 | return objectMapper.readValue(response.getBody(), objectMapper.getTypeFactory().constructCollectionType(List.class, Map.class)); 49 | 50 | } 51 | } catch (IOException e){ 52 | throw new RuntimeException(e); 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/resources/application-dev.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woemler/spring-blog/3b0a6257714440566ceccb3ebf6530a2ad987561/src/main/resources/application-dev.properties -------------------------------------------------------------------------------- /src/main/resources/application-prd.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woemler/spring-blog/3b0a6257714440566ceccb3ebf6530a2ad987561/src/main/resources/application-prd.properties -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Application properties 2 | app.blog.title=My Spring Blog 3 | app.blog.subtitle=Fun with Spring Boot and MongoDB. 4 | 5 | # Spring Boot 6 | spring.profiles.active=dev 7 | spring.mvc.view.prefix: /WEB-INF/views/jsp/ 8 | spring.mvc.view.suffix: .jsp 9 | 10 | # API 11 | springblog.api.url=/api 12 | 13 | # Security 14 | #security.user.name=admin 15 | #security.user.password=test 16 | 17 | # GitHub 18 | github.username=woemler 19 | 20 | # Disqus 21 | disqus.shortname=springblog 22 | -------------------------------------------------------------------------------- /src/main/resources/data-source.properties: -------------------------------------------------------------------------------- 1 | # MongoDB 2 | mongo.username=test 3 | mongo.password=test 4 | mongo.host=localhost 5 | mongo.name=blog 6 | mongo.port=27017 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/resources/static/css/blog-style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | padding-bottom: 20px; 4 | } 5 | 6 | .well{ 7 | border: none; 8 | } 9 | 10 | .navbar-inverse{ 11 | /*background-color:#222;*/ 12 | background-color:#080808; 13 | } 14 | 15 | .navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{ 16 | background-color:#222; 17 | } 18 | 19 | .jumbotron { 20 | background-color: #333; 21 | padding-top:10px; 22 | padding-bottom:10px 23 | } 24 | 25 | .page-header, .page-header h2 { 26 | border: none; 27 | color: #f5f5f5; 28 | } 29 | 30 | .page-header small{ 31 | color: #cccccc 32 | } 33 | 34 | .page-header-subtitle{ 35 | color: #cccccc; 36 | font-family:'Consolas'; 37 | font-size: 0.8em; 38 | } 39 | 40 | .navbar-brand{ 41 | font-family:'Consolas' !important; 42 | color:#cc4800 !important; 43 | } 44 | 45 | .blog-post { 46 | 47 | } 48 | 49 | .blog-title { 50 | 51 | } 52 | 53 | .blog-title :hover { 54 | text-decoration: none; 55 | cursor: pointer; 56 | } 57 | 58 | .blog-date { 59 | 60 | } 61 | 62 | .blog-markup { 63 | 64 | } 65 | 66 | .blog-footer { 67 | 68 | } 69 | 70 | .blog-comments { 71 | 72 | } 73 | 74 | /* 75 | #logoutLink:hover { 76 | text-decoration: none; 77 | cursor:pointer; 78 | } 79 | */ 80 | -------------------------------------------------------------------------------- /src/main/resources/static/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | .btn-default,.btn-primary,.btn-success,.btn-info,.btn-warning,.btn-danger{text-shadow:0 -1px 0 rgba(0,0,0,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 1px rgba(0,0,0,0.075)}.btn-default:active,.btn-primary:active,.btn-success:active,.btn-info:active,.btn-warning:active,.btn-danger:active,.btn-default.active,.btn-primary.active,.btn-success.active,.btn-info.active,.btn-warning.active,.btn-danger.active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:active,.btn.active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-gradient(linear,left 0,left 100%,from(#fff),to(#e6e6e6));background-image:-webkit-linear-gradient(top,#fff,0%,#e6e6e6,100%);background-image:-moz-linear-gradient(top,#fff 0,#e6e6e6 100%);background-image:linear-gradient(to bottom,#fff 0,#e6e6e6 100%);background-repeat:repeat-x;border-color:#e0e0e0;border-color:#ccc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#ffe6e6e6',GradientType=0)}.btn-default:active,.btn-default.active{background-color:#e6e6e6;border-color:#e0e0e0}.btn-primary{background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#3071a9));background-image:-webkit-linear-gradient(top,#428bca,0%,#3071a9,100%);background-image:-moz-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);background-repeat:repeat-x;border-color:#2d6ca2;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3071a9',GradientType=0)}.btn-primary:active,.btn-primary.active{background-color:#3071a9;border-color:#2d6ca2}.btn-success{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5cb85c),to(#449d44));background-image:-webkit-linear-gradient(top,#5cb85c,0%,#449d44,100%);background-image:-moz-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);background-repeat:repeat-x;border-color:#419641;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff449d44',GradientType=0)}.btn-success:active,.btn-success.active{background-color:#449d44;border-color:#419641}.btn-warning{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f0ad4e),to(#ec971f));background-image:-webkit-linear-gradient(top,#f0ad4e,0%,#ec971f,100%);background-image:-moz-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);background-repeat:repeat-x;border-color:#eb9316;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffec971f',GradientType=0)}.btn-warning:active,.btn-warning.active{background-color:#ec971f;border-color:#eb9316}.btn-danger{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9534f),to(#c9302c));background-image:-webkit-linear-gradient(top,#d9534f,0%,#c9302c,100%);background-image:-moz-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);background-repeat:repeat-x;border-color:#c12e2a;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc9302c',GradientType=0)}.btn-danger:active,.btn-danger.active{background-color:#c9302c;border-color:#c12e2a}.btn-info{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5bc0de),to(#31b0d5));background-image:-webkit-linear-gradient(top,#5bc0de,0%,#31b0d5,100%);background-image:-moz-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);background-repeat:repeat-x;border-color:#2aabd2;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff31b0d5',GradientType=0)}.btn-info:active,.btn-info.active{background-color:#31b0d5;border-color:#2aabd2}.thumbnail,.img-thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus,.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{background-color:#357ebd;background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#357ebd));background-image:-webkit-linear-gradient(top,#428bca,0%,#357ebd,100%);background-image:-moz-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.navbar{background-image:-webkit-gradient(linear,left 0,left 100%,from(#fff),to(#f8f8f8));background-image:-webkit-linear-gradient(top,#fff,0%,#f8f8f8,100%);background-image:-moz-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);background-repeat:repeat-x;border-radius:4px;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff',endColorstr='#fff8f8f8',GradientType=0);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075);box-shadow:inset 0 1px 0 rgba(255,255,255,0.15),0 1px 5px rgba(0,0,0,0.075)}.navbar .navbar-nav>.active>a{background-color:#f8f8f8}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,0.25)}.navbar-inverse{background-image:-webkit-gradient(linear,left 0,left 100%,from(#3c3c3c),to(#222));background-image:-webkit-linear-gradient(top,#3c3c3c,0%,#222,100%);background-image:-moz-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c',endColorstr='#ff222222',GradientType=0)}.navbar-inverse .navbar-nav>.active>a{background-color:#222}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.navbar-static-top,.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}.alert{text-shadow:0 1px 0 rgba(255,255,255,0.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05);box-shadow:inset 0 1px 0 rgba(255,255,255,0.25),0 1px 2px rgba(0,0,0,0.05)}.alert-success{background-image:-webkit-gradient(linear,left 0,left 100%,from(#dff0d8),to(#c8e5bc));background-image:-webkit-linear-gradient(top,#dff0d8,0%,#c8e5bc,100%);background-image:-moz-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);background-repeat:repeat-x;border-color:#b2dba1;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffc8e5bc',GradientType=0)}.alert-info{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9edf7),to(#b9def0));background-image:-webkit-linear-gradient(top,#d9edf7,0%,#b9def0,100%);background-image:-moz-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);background-repeat:repeat-x;border-color:#9acfea;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffb9def0',GradientType=0)}.alert-warning{background-image:-webkit-gradient(linear,left 0,left 100%,from(#fcf8e3),to(#f8efc0));background-image:-webkit-linear-gradient(top,#fcf8e3,0%,#f8efc0,100%);background-image:-moz-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);background-repeat:repeat-x;border-color:#f5e79e;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fff8efc0',GradientType=0)}.alert-danger{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f2dede),to(#e7c3c3));background-image:-webkit-linear-gradient(top,#f2dede,0%,#e7c3c3,100%);background-image:-moz-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);background-repeat:repeat-x;border-color:#dca7a7;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffe7c3c3',GradientType=0)}.progress{background-image:-webkit-gradient(linear,left 0,left 100%,from(#ebebeb),to(#f5f5f5));background-image:-webkit-linear-gradient(top,#ebebeb,0%,#f5f5f5,100%);background-image:-moz-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb',endColorstr='#fff5f5f5',GradientType=0)}.progress-bar{background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#3071a9));background-image:-webkit-linear-gradient(top,#428bca,0%,#3071a9,100%);background-image:-moz-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3071a9',GradientType=0)}.progress-bar-success{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5cb85c),to(#449d44));background-image:-webkit-linear-gradient(top,#5cb85c,0%,#449d44,100%);background-image:-moz-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c',endColorstr='#ff449d44',GradientType=0)}.progress-bar-info{background-image:-webkit-gradient(linear,left 0,left 100%,from(#5bc0de),to(#31b0d5));background-image:-webkit-linear-gradient(top,#5bc0de,0%,#31b0d5,100%);background-image:-moz-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de',endColorstr='#ff31b0d5',GradientType=0)}.progress-bar-warning{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f0ad4e),to(#ec971f));background-image:-webkit-linear-gradient(top,#f0ad4e,0%,#ec971f,100%);background-image:-moz-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e',endColorstr='#ffec971f',GradientType=0)}.progress-bar-danger{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9534f),to(#c9302c));background-image:-webkit-linear-gradient(top,#d9534f,0%,#c9302c,100%);background-image:-moz-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f',endColorstr='#ffc9302c',GradientType=0)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.075);box-shadow:0 1px 2px rgba(0,0,0,0.075)}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#3278b3));background-image:-webkit-linear-gradient(top,#428bca,0%,#3278b3,100%);background-image:-moz-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);background-repeat:repeat-x;border-color:#3278b3;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff3278b3',GradientType=0)}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,0.05);box-shadow:0 1px 2px rgba(0,0,0,0.05)}.panel-default>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f5f5f5),to(#e8e8e8));background-image:-webkit-linear-gradient(top,#f5f5f5,0%,#e8e8e8,100%);background-image:-moz-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5',endColorstr='#ffe8e8e8',GradientType=0)}.panel-primary>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#428bca),to(#357ebd));background-image:-webkit-linear-gradient(top,#428bca,0%,#357ebd,100%);background-image:-moz-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca',endColorstr='#ff357ebd',GradientType=0)}.panel-success>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#dff0d8),to(#d0e9c6));background-image:-webkit-linear-gradient(top,#dff0d8,0%,#d0e9c6,100%);background-image:-moz-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8',endColorstr='#ffd0e9c6',GradientType=0)}.panel-info>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#d9edf7),to(#c4e3f3));background-image:-webkit-linear-gradient(top,#d9edf7,0%,#c4e3f3,100%);background-image:-moz-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7',endColorstr='#ffc4e3f3',GradientType=0)}.panel-warning>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#fcf8e3),to(#faf2cc));background-image:-webkit-linear-gradient(top,#fcf8e3,0%,#faf2cc,100%);background-image:-moz-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3',endColorstr='#fffaf2cc',GradientType=0)}.panel-danger>.panel-heading{background-image:-webkit-gradient(linear,left 0,left 100%,from(#f2dede),to(#ebcccc));background-image:-webkit-linear-gradient(top,#f2dede,0%,#ebcccc,100%);background-image:-moz-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede',endColorstr='#ffebcccc',GradientType=0)}.well{background-image:-webkit-gradient(linear,left 0,left 100%,from(#e8e8e8),to(#f5f5f5));background-image:-webkit-linear-gradient(top,#e8e8e8,0%,#f5f5f5,100%);background-image:-moz-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);background-repeat:repeat-x;border-color:#dcdcdc;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8',endColorstr='#fff5f5f5',GradientType=0);-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 3px rgba(0,0,0,0.05),0 1px 0 rgba(255,255,255,0.1)} -------------------------------------------------------------------------------- /src/main/resources/static/css/jquery-ui.css: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery UI CSS Framework 3 | * 4 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 5 | * Dual licensed under the MIT or GPL Version 2 licenses. 6 | * http://jquery.org/license 7 | * 8 | * http://docs.jquery.com/UI/Theming/API 9 | */ 10 | 11 | /* Layout helpers 12 | ----------------------------------*/ 13 | .ui-helper-hidden { display: none; } 14 | .ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,1px); } 15 | .ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; } 16 | .ui-helper-clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } 17 | .ui-helper-clearfix { display: inline-block; } 18 | /* required comment for clearfix to work in Opera \*/ 19 | * html .ui-helper-clearfix { height:1%; } 20 | .ui-helper-clearfix { display:block; } 21 | /* end clearfix */ 22 | .ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); } 23 | 24 | 25 | /* Interaction Cues 26 | ----------------------------------*/ 27 | .ui-state-disabled { cursor: default !important; } 28 | 29 | 30 | /* Icons 31 | ----------------------------------*/ 32 | 33 | /* states and images */ 34 | .ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; } 35 | 36 | 37 | /* Misc visuals 38 | ----------------------------------*/ 39 | 40 | /* Overlays */ 41 | .ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } 42 | 43 | 44 | /* 45 | * jQuery UI CSS Framework 1.8.16 46 | * 47 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 48 | * Dual licensed under the MIT or GPL Version 2 licenses. 49 | * http://jquery.org/license 50 | * 51 | * http://docs.jquery.com/UI/Theming/API 52 | * 53 | * To view and modify this theme, visit http://jquit.com/builder/#gcc=ffffff&gcb=d4d4d4&gct=525252&gci=d4d4d4&pcc=d4d4d4&acb=525252&o=ffffff&ghc=3399ff&ght=ffffff&ghi=ffffff&gdc=d4d4d4&gdb=d4d4d4&gdt=525252&tdc=3399ff&tdb=3399ff&tdt=ffffff&tdi=ffffff&ddc=d4d4d4&ddb=d4d4d4&ddt=525252&pxd=000000&asc=3399ff&ast=ffffff&goc=66B3FF&gob=66B3FF&got=ffffff&toc=66B3FF&tob=66B3FF&tot=ffffff&toi=ffffff&doc=66B3FF&dob=66B3FF&dot=ffffff&pxa=525252&gac=3399ff&gab=3399ff&gat=ffffff&tac=ffffff&tab=d4d4d4&tat=525252&tai=d4d4d4&dac=3399ff&dab=3399ff&dat=ffffff&dic=d4d4d4&dib=3399ff&dit=ffffff&gic=efdca9&gib=efdca9&git=525252&gii=525252&ec=c31d1d&eb=c31d1d&et=ffffff&ei=ffffff 54 | */ 55 | 56 | 57 | /* Component containers 58 | ----------------------------------*/ 59 | .ui-widget { font-family: Helvetica, Arial, sans-serif; font-size: 1.1em; } 60 | .ui-widget .ui-widget { font-size: 1em; } 61 | .ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Helvetica, Arial, sans-serif; font-size: 1em; } 62 | .ui-widget-content { border: 1px solid #dddddd; background: #ffffff; color: #444444; } 63 | .ui-widget-content a { color: #444444; } 64 | .ui-widget-header { border: 1px solid #dddddd; background: #dddddd; color: #444444; font-weight: bold; } 65 | .ui-widget-header a { color: #444444; } 66 | 67 | /* Interaction Cues 68 | ----------------------------------*/ 69 | .ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #cccccc; background: #ffffff; color: #444444; } 70 | .ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #444444; } 71 | .ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #ff0084; background: #ffffff; color: #222222; } 72 | .ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #222222; } 73 | .ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #222222; } 74 | .ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: normal; } 75 | .ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; } 76 | .ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; } 77 | 78 | /* positioning */ 79 | .ui-icon-carat-1-n { background-position: 0 0; } 80 | .ui-icon-carat-1-ne { background-position: -16px 0; } 81 | .ui-icon-carat-1-e { background-position: -32px 0; } 82 | .ui-icon-carat-1-se { background-position: -48px 0; } 83 | .ui-icon-carat-1-s { background-position: -64px 0; } 84 | .ui-icon-carat-1-sw { background-position: -80px 0; } 85 | .ui-icon-carat-1-w { background-position: -96px 0; } 86 | .ui-icon-carat-1-nw { background-position: -112px 0; } 87 | .ui-icon-carat-2-n-s { background-position: -128px 0; } 88 | .ui-icon-carat-2-e-w { background-position: -144px 0; } 89 | .ui-icon-triangle-1-n { background-position: 0 -16px; } 90 | .ui-icon-triangle-1-ne { background-position: -16px -16px; } 91 | .ui-icon-triangle-1-e { background-position: -32px -16px; } 92 | .ui-icon-triangle-1-se { background-position: -48px -16px; } 93 | .ui-icon-triangle-1-s { background-position: -64px -16px; } 94 | .ui-icon-triangle-1-sw { background-position: -80px -16px; } 95 | .ui-icon-triangle-1-w { background-position: -96px -16px; } 96 | .ui-icon-triangle-1-nw { background-position: -112px -16px; } 97 | .ui-icon-triangle-2-n-s { background-position: -128px -16px; } 98 | .ui-icon-triangle-2-e-w { background-position: -144px -16px; } 99 | .ui-icon-arrow-1-n { background-position: 0 -32px; } 100 | .ui-icon-arrow-1-ne { background-position: -16px -32px; } 101 | .ui-icon-arrow-1-e { background-position: -32px -32px; } 102 | .ui-icon-arrow-1-se { background-position: -48px -32px; } 103 | .ui-icon-arrow-1-s { background-position: -64px -32px; } 104 | .ui-icon-arrow-1-sw { background-position: -80px -32px; } 105 | .ui-icon-arrow-1-w { background-position: -96px -32px; } 106 | .ui-icon-arrow-1-nw { background-position: -112px -32px; } 107 | .ui-icon-arrow-2-n-s { background-position: -128px -32px; } 108 | .ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } 109 | .ui-icon-arrow-2-e-w { background-position: -160px -32px; } 110 | .ui-icon-arrow-2-se-nw { background-position: -176px -32px; } 111 | .ui-icon-arrowstop-1-n { background-position: -192px -32px; } 112 | .ui-icon-arrowstop-1-e { background-position: -208px -32px; } 113 | .ui-icon-arrowstop-1-s { background-position: -224px -32px; } 114 | .ui-icon-arrowstop-1-w { background-position: -240px -32px; } 115 | .ui-icon-arrowthick-1-n { background-position: 0 -48px; } 116 | .ui-icon-arrowthick-1-ne { background-position: -16px -48px; } 117 | .ui-icon-arrowthick-1-e { background-position: -32px -48px; } 118 | .ui-icon-arrowthick-1-se { background-position: -48px -48px; } 119 | .ui-icon-arrowthick-1-s { background-position: -64px -48px; } 120 | .ui-icon-arrowthick-1-sw { background-position: -80px -48px; } 121 | .ui-icon-arrowthick-1-w { background-position: -96px -48px; } 122 | .ui-icon-arrowthick-1-nw { background-position: -112px -48px; } 123 | .ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } 124 | .ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } 125 | .ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } 126 | .ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } 127 | .ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } 128 | .ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } 129 | .ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } 130 | .ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } 131 | .ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } 132 | .ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } 133 | .ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } 134 | .ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } 135 | .ui-icon-arrowreturn-1-w { background-position: -64px -64px; } 136 | .ui-icon-arrowreturn-1-n { background-position: -80px -64px; } 137 | .ui-icon-arrowreturn-1-e { background-position: -96px -64px; } 138 | .ui-icon-arrowreturn-1-s { background-position: -112px -64px; } 139 | .ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } 140 | .ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } 141 | .ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } 142 | .ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } 143 | .ui-icon-arrow-4 { background-position: 0 -80px; } 144 | .ui-icon-arrow-4-diag { background-position: -16px -80px; } 145 | .ui-icon-extlink { background-position: -32px -80px; } 146 | .ui-icon-newwin { background-position: -48px -80px; } 147 | .ui-icon-refresh { background-position: -64px -80px; } 148 | .ui-icon-shuffle { background-position: -80px -80px; } 149 | .ui-icon-transfer-e-w { background-position: -96px -80px; } 150 | .ui-icon-transferthick-e-w { background-position: -112px -80px; } 151 | .ui-icon-folder-collapsed { background-position: 0 -96px; } 152 | .ui-icon-folder-open { background-position: -16px -96px; } 153 | .ui-icon-document { background-position: -32px -96px; } 154 | .ui-icon-document-b { background-position: -48px -96px; } 155 | .ui-icon-note { background-position: -64px -96px; } 156 | .ui-icon-mail-closed { background-position: -80px -96px; } 157 | .ui-icon-mail-open { background-position: -96px -96px; } 158 | .ui-icon-suitcase { background-position: -112px -96px; } 159 | .ui-icon-comment { background-position: -128px -96px; } 160 | .ui-icon-person { background-position: -144px -96px; } 161 | .ui-icon-print { background-position: -160px -96px; } 162 | .ui-icon-trash { background-position: -176px -96px; } 163 | .ui-icon-locked { background-position: -192px -96px; } 164 | .ui-icon-unlocked { background-position: -208px -96px; } 165 | .ui-icon-bookmark { background-position: -224px -96px; } 166 | .ui-icon-tag { background-position: -240px -96px; } 167 | .ui-icon-home { background-position: 0 -112px; } 168 | .ui-icon-flag { background-position: -16px -112px; } 169 | .ui-icon-calendar { background-position: -32px -112px; } 170 | .ui-icon-cart { background-position: -48px -112px; } 171 | .ui-icon-pencil { background-position: -64px -112px; } 172 | .ui-icon-clock { background-position: -80px -112px; } 173 | .ui-icon-disk { background-position: -96px -112px; } 174 | .ui-icon-calculator { background-position: -112px -112px; } 175 | .ui-icon-zoomin { background-position: -128px -112px; } 176 | .ui-icon-zoomout { background-position: -144px -112px; } 177 | .ui-icon-search { background-position: -160px -112px; } 178 | .ui-icon-wrench { background-position: -176px -112px; } 179 | .ui-icon-gear { background-position: -192px -112px; } 180 | .ui-icon-heart { background-position: -208px -112px; } 181 | .ui-icon-star { background-position: -224px -112px; } 182 | .ui-icon-link { background-position: -240px -112px; } 183 | .ui-icon-cancel { background-position: 0 -128px; } 184 | .ui-icon-plus { background-position: -16px -128px; } 185 | .ui-icon-plusthick { background-position: -32px -128px; } 186 | .ui-icon-minus { background-position: -48px -128px; } 187 | .ui-icon-minusthick { background-position: -64px -128px; } 188 | .ui-icon-close { background-position: -80px -128px; } 189 | .ui-icon-closethick { background-position: -96px -128px; } 190 | .ui-icon-key { background-position: -112px -128px; } 191 | .ui-icon-lightbulb { background-position: -128px -128px; } 192 | .ui-icon-scissors { background-position: -144px -128px; } 193 | .ui-icon-clipboard { background-position: -160px -128px; } 194 | .ui-icon-copy { background-position: -176px -128px; } 195 | .ui-icon-contact { background-position: -192px -128px; } 196 | .ui-icon-image { background-position: -208px -128px; } 197 | .ui-icon-video { background-position: -224px -128px; } 198 | .ui-icon-script { background-position: -240px -128px; } 199 | .ui-icon-alert { background-position: 0 -144px; } 200 | .ui-icon-info { background-position: -16px -144px; } 201 | .ui-icon-notice { background-position: -32px -144px; } 202 | .ui-icon-help { background-position: -48px -144px; } 203 | .ui-icon-check { background-position: -64px -144px; } 204 | .ui-icon-bullet { background-position: -80px -144px; } 205 | .ui-icon-radio-off { background-position: -96px -144px; } 206 | .ui-icon-radio-on { background-position: -112px -144px; } 207 | .ui-icon-pin-w { background-position: -128px -144px; } 208 | .ui-icon-pin-s { background-position: -144px -144px; } 209 | .ui-icon-play { background-position: 0 -160px; } 210 | .ui-icon-pause { background-position: -16px -160px; } 211 | .ui-icon-seek-next { background-position: -32px -160px; } 212 | .ui-icon-seek-prev { background-position: -48px -160px; } 213 | .ui-icon-seek-end { background-position: -64px -160px; } 214 | .ui-icon-seek-start { background-position: -80px -160px; } 215 | /* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ 216 | .ui-icon-seek-first { background-position: -80px -160px; } 217 | .ui-icon-stop { background-position: -96px -160px; } 218 | .ui-icon-eject { background-position: -112px -160px; } 219 | .ui-icon-volume-off { background-position: -128px -160px; } 220 | .ui-icon-volume-on { background-position: -144px -160px; } 221 | .ui-icon-power { background-position: 0 -176px; } 222 | .ui-icon-signal-diag { background-position: -16px -176px; } 223 | .ui-icon-signal { background-position: -32px -176px; } 224 | .ui-icon-battery-0 { background-position: -48px -176px; } 225 | .ui-icon-battery-1 { background-position: -64px -176px; } 226 | .ui-icon-battery-2 { background-position: -80px -176px; } 227 | .ui-icon-battery-3 { background-position: -96px -176px; } 228 | .ui-icon-circle-plus { background-position: 0 -192px; } 229 | .ui-icon-circle-minus { background-position: -16px -192px; } 230 | .ui-icon-circle-close { background-position: -32px -192px; } 231 | .ui-icon-circle-triangle-e { background-position: -48px -192px; } 232 | .ui-icon-circle-triangle-s { background-position: -64px -192px; } 233 | .ui-icon-circle-triangle-w { background-position: -80px -192px; } 234 | .ui-icon-circle-triangle-n { background-position: -96px -192px; } 235 | .ui-icon-circle-arrow-e { background-position: -112px -192px; } 236 | .ui-icon-circle-arrow-s { background-position: -128px -192px; } 237 | .ui-icon-circle-arrow-w { background-position: -144px -192px; } 238 | .ui-icon-circle-arrow-n { background-position: -160px -192px; } 239 | .ui-icon-circle-zoomin { background-position: -176px -192px; } 240 | .ui-icon-circle-zoomout { background-position: -192px -192px; } 241 | .ui-icon-circle-check { background-position: -208px -192px; } 242 | .ui-icon-circlesmall-plus { background-position: 0 -208px; } 243 | .ui-icon-circlesmall-minus { background-position: -16px -208px; } 244 | .ui-icon-circlesmall-close { background-position: -32px -208px; } 245 | .ui-icon-squaresmall-plus { background-position: -48px -208px; } 246 | .ui-icon-squaresmall-minus { background-position: -64px -208px; } 247 | .ui-icon-squaresmall-close { background-position: -80px -208px; } 248 | .ui-icon-grip-dotted-vertical { background-position: 0 -224px; } 249 | .ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } 250 | .ui-icon-grip-solid-vertical { background-position: -32px -224px; } 251 | .ui-icon-grip-solid-horizontal { background-position: -48px -224px; } 252 | .ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } 253 | .ui-icon-grip-diagonal-se { background-position: -80px -224px; } 254 | 255 | 256 | 257 | 258 | /* Misc visuals 259 | ----------------------------------*/ 260 | 261 | /* Corner radius */ 262 | .ui-corner-all, .ui-corner-top, .ui-corner-left, .ui-corner-tl { -moz-border-radius-topleft: 0; -webkit-border-top-left-radius: 0; -khtml-border-top-left-radius: 0; border-top-left-radius: 0; } 263 | .ui-corner-all, .ui-corner-top, .ui-corner-right, .ui-corner-tr { -moz-border-radius-topright: 0; -webkit-border-top-right-radius: 0; -khtml-border-top-right-radius: 0; border-top-right-radius: 0; } 264 | .ui-corner-all, .ui-corner-bottom, .ui-corner-left, .ui-corner-bl { -moz-border-radius-bottomleft: 0; -webkit-border-bottom-left-radius: 0; -khtml-border-bottom-left-radius: 0; border-bottom-left-radius: 0; } 265 | .ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 0; -webkit-border-bottom-right-radius: 0; -khtml-border-bottom-right-radius: 0; border-bottom-right-radius: 0; } 266 | 267 | /* Overlays */ 268 | .ui-widget-overlay { background: #eeeeee; opacity: .80;filter:Alpha(Opacity=80); } 269 | .ui-widget-shadow { margin: -4px 0 0 -4px; padding: 4px; background: #aaaaaa; opacity: .60;filter:Alpha(Opacity=60); -moz-border-radius: 0px; -khtml-border-radius: 0px; -webkit-border-radius: 0px; border-radius: 0px; }/* 270 | * jQuery UI Resizable 1.8.16 271 | * 272 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 273 | * Dual licensed under the MIT or GPL Version 2 licenses. 274 | * http://jquery.org/license 275 | * 276 | * http://docs.jquery.com/UI/Resizable#theming 277 | */ 278 | .ui-resizable { position: relative;} 279 | .ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block; } 280 | .ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; } 281 | .ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; } 282 | .ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; } 283 | .ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; } 284 | .ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; } 285 | .ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; } 286 | .ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; } 287 | .ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; } 288 | .ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/* 289 | * jQuery UI Selectable 1.8.16 290 | * 291 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 292 | * Dual licensed under the MIT or GPL Version 2 licenses. 293 | * http://jquery.org/license 294 | * 295 | * http://docs.jquery.com/UI/Selectable#theming 296 | */ 297 | .ui-selectable-helper { position: absolute; z-index: 100; border:1px dotted black; } 298 | /* 299 | * jQuery UI Accordion 1.8.16 300 | * 301 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 302 | * Dual licensed under the MIT or GPL Version 2 licenses. 303 | * http://jquery.org/license 304 | * 305 | * http://docs.jquery.com/UI/Accordion#theming 306 | */ 307 | /* IE/Win - Fix animation bug - #4615 */ 308 | .ui-accordion { width: 100%; } 309 | .ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; } 310 | .ui-accordion .ui-accordion-li-fix { display: inline; } 311 | .ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; } 312 | .ui-accordion .ui-accordion-header a { display: block; font-size: 1em; padding: .5em .5em .5em .7em; } 313 | .ui-accordion-icons .ui-accordion-header a { padding-left: 2.2em; } 314 | .ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: 2px; top: 50%; margin-top: -8px; } 315 | .ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; zoom: 1; } 316 | .ui-accordion .ui-accordion-content-active { display: block; } 317 | /* 318 | * jQuery UI Autocomplete 1.8.16 319 | * 320 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 321 | * Dual licensed under the MIT or GPL Version 2 licenses. 322 | * http://jquery.org/license 323 | * 324 | * http://docs.jquery.com/UI/Autocomplete#theming 325 | */ 326 | .ui-autocomplete { position: absolute; cursor: default; } 327 | 328 | /* workarounds */ 329 | * html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */ 330 | 331 | /* 332 | * jQuery UI Menu 1.8.16 333 | * 334 | * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 335 | * Dual licensed under the MIT or GPL Version 2 licenses. 336 | * http://jquery.org/license 337 | * 338 | * http://docs.jquery.com/UI/Menu#theming 339 | */ 340 | .ui-menu { 341 | list-style:none; 342 | padding: 2px; 343 | margin: 0; 344 | display:block; 345 | float: left; 346 | } 347 | .ui-menu .ui-menu { 348 | margin-top: -3px; 349 | } 350 | .ui-menu .ui-menu-item { 351 | margin:0; 352 | padding: 0; 353 | zoom: 1; 354 | float: left; 355 | clear: left; 356 | width: 100%; 357 | } 358 | .ui-menu .ui-menu-item a { 359 | text-decoration:none; 360 | display:block; 361 | padding:.2em .4em; 362 | line-height:1.5; 363 | zoom:1; 364 | } 365 | .ui-menu .ui-menu-item a.ui-state-hover, 366 | .ui-menu .ui-menu-item a.ui-state-active { 367 | font-weight: normal; 368 | margin: -1px; 369 | } 370 | /* 371 | * jQuery UI Button 1.8.16 372 | * 373 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 374 | * Dual licensed under the MIT or GPL Version 2 licenses. 375 | * http://jquery.org/license 376 | * 377 | * http://docs.jquery.com/UI/Button#theming 378 | */ 379 | .ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */ 380 | .ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */ 381 | button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */ 382 | .ui-button-icons-only { width: 3.4em; } 383 | button.ui-button-icons-only { width: 3.7em; } 384 | 385 | /*button text element */ 386 | .ui-button .ui-button-text { display: block; line-height: 1.4; } 387 | .ui-button-text-only .ui-button-text { padding: .4em 1em; } 388 | .ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; } 389 | .ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; } 390 | .ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; } 391 | .ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; } 392 | /* no icon support for input elements, provide padding by default */ 393 | input.ui-button { padding: .4em 1em; } 394 | 395 | /*button icon element(s) */ 396 | /*.ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; } 397 | .ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; } 398 | .ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .5em; } 399 | .ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; } 400 | .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; } 401 | */ 402 | 403 | /*button icon element(s) */ 404 | .ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -12px; } 405 | .ui-button-icon-only .ui-icon { left: 50%; margin-left: -12px; } 406 | .ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .35em; } 407 | .ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .35em; } 408 | .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .35em; } 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | /*button sets*/ 424 | .ui-buttonset { margin-right: 7px; } 425 | .ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; } 426 | 427 | /* workarounds */ 428 | button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */ 429 | /* 430 | * jQuery UI Dialog 1.8.16 431 | * 432 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 433 | * Dual licensed under the MIT or GPL Version 2 licenses. 434 | * http://jquery.org/license 435 | * 436 | * http://docs.jquery.com/UI/Dialog#theming 437 | */ 438 | .ui-dialog { position: absolute; padding: .2em; width: 300px; overflow: hidden; } 439 | .ui-dialog .ui-dialog-titlebar { padding: .4em 1em; position: relative; } 440 | .ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .1em 0; } 441 | .ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .3em; top: 50%; width: 19px; margin: -10px 0 0 0; padding: 1px; height: 18px; } 442 | .ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; } 443 | .ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; } 444 | .ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; } 445 | .ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; } 446 | .ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; } 447 | .ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; } 448 | .ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 2px; bottom: 2px; } 449 | .ui-draggable .ui-dialog-titlebar { cursor: move; } 450 | /* 451 | * jQuery UI Slider 1.8.16 452 | * 453 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 454 | * Dual licensed under the MIT or GPL Version 2 licenses. 455 | * http://jquery.org/license 456 | * 457 | * http://docs.jquery.com/UI/Slider#theming 458 | */ 459 | .ui-slider { position: relative; text-align: left; } 460 | .ui-slider .ui-slider-handle { position: absolute; z-index: 2; width: 1.2em; height: 1.2em; cursor: default; } 461 | .ui-slider .ui-slider-range { position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; } 462 | 463 | .ui-slider-horizontal { height: .8em; } 464 | .ui-slider-horizontal .ui-slider-handle { top: -.3em; margin-left: -.6em; } 465 | .ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; } 466 | .ui-slider-horizontal .ui-slider-range-min { left: 0; } 467 | .ui-slider-horizontal .ui-slider-range-max { right: 0; } 468 | 469 | .ui-slider-vertical { width: .8em; height: 100px; } 470 | .ui-slider-vertical .ui-slider-handle { left: -.3em; margin-left: 0; margin-bottom: -.6em; } 471 | .ui-slider-vertical .ui-slider-range { left: 0; width: 100%; } 472 | .ui-slider-vertical .ui-slider-range-min { bottom: 0; } 473 | .ui-slider-vertical .ui-slider-range-max { top: 0; }/* 474 | * jQuery UI Tabs 1.8.16 475 | * 476 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 477 | * Dual licensed under the MIT or GPL Version 2 licenses. 478 | * http://jquery.org/license 479 | * 480 | * http://docs.jquery.com/UI/Tabs#theming 481 | */ 482 | .ui-tabs { position: relative; padding: .2em; zoom: 1; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ 483 | .ui-tabs .ui-tabs-nav { margin: 0; padding: .2em .2em 0; } 484 | .ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 1px; margin: 0 .2em 1px 0; border-bottom: 0 !important; padding: 0; white-space: nowrap; } 485 | .ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; } 486 | .ui-tabs .ui-tabs-nav li.ui-tabs-selected { margin-bottom: 0; padding-bottom: 1px; } 487 | .ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; } 488 | .ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */ 489 | .ui-tabs .ui-tabs-panel { display: block; border-width: 0; padding: 1em 1.4em; background: none; } 490 | .ui-tabs .ui-tabs-hide { display: none !important; } 491 | /* 492 | * jQuery UI Datepicker 1.8.16 493 | * 494 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 495 | * Dual licensed under the MIT or GPL Version 2 licenses. 496 | * http://jquery.org/license 497 | * 498 | * http://docs.jquery.com/UI/Datepicker#theming 499 | */ 500 | .ui-datepicker { width: 17em; padding: .2em .2em 0; display: none; } 501 | .ui-datepicker .ui-datepicker-header { position:relative; padding:.2em 0; } 502 | .ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 2px; width: 1.8em; height: 1.8em; } 503 | .ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { top: 1px; } 504 | .ui-datepicker .ui-datepicker-prev { left:2px; } 505 | .ui-datepicker .ui-datepicker-next { right:2px; } 506 | .ui-datepicker .ui-datepicker-prev-hover { left:1px; } 507 | .ui-datepicker .ui-datepicker-next-hover { right:1px; } 508 | .ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px; } 509 | .ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; } 510 | .ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; } 511 | .ui-datepicker select.ui-datepicker-month-year {width: 100%;} 512 | .ui-datepicker select.ui-datepicker-month, 513 | .ui-datepicker select.ui-datepicker-year { width: 49%;} 514 | .ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; } 515 | .ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: normal; border: 0; } 516 | .ui-datepicker td { border: 0; padding: 1px; } 517 | .ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; } 518 | .ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; } 519 | .ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; } 520 | .ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; } 521 | 522 | /* with multiple calendars */ 523 | .ui-datepicker.ui-datepicker-multi { width:auto; } 524 | .ui-datepicker-multi .ui-datepicker-group { float:left; } 525 | .ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; } 526 | .ui-datepicker-multi-2 .ui-datepicker-group { width:50%; } 527 | .ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; } 528 | .ui-datepicker-multi-4 .ui-datepicker-group { width:25%; } 529 | .ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; } 530 | .ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; } 531 | .ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; } 532 | .ui-datepicker-row-break { clear:both; width:100%; font-size:0em; } 533 | 534 | /* RTL support */ 535 | .ui-datepicker-rtl { direction: rtl; } 536 | .ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; } 537 | .ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; } 538 | .ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; } 539 | .ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; } 540 | .ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; } 541 | .ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; } 542 | .ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; } 543 | .ui-datepicker-rtl .ui-datepicker-group { float:right; } 544 | .ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; } 545 | .ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; } 546 | 547 | /* IE6 IFRAME FIX (taken from datepicker 1.5.3 */ 548 | .ui-datepicker-cover { 549 | display: none; /*sorry for IE5*/ 550 | display/**/: block; /*sorry for IE5*/ 551 | position: absolute; /*must have*/ 552 | z-index: -1; /*must have*/ 553 | filter: mask(); /*must have*/ 554 | top: -4px; /*must have*/ 555 | left: -4px; /*must have*/ 556 | width: 200px; /*must have*/ 557 | height: 200px; /*must have*/ 558 | }/* 559 | * jQuery UI Progressbar 1.8.16 560 | * 561 | * Copyright 2011, AUTHORS.txt (http://jqueryui.com/about) 562 | * Dual licensed under the MIT or GPL Version 2 licenses. 563 | * http://jquery.org/license 564 | * 565 | * http://docs.jquery.com/UI/Progressbar#theming 566 | */ 567 | .ui-progressbar { height:2em; text-align: left; } 568 | .ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; } 569 | 570 | /* General */ 571 | html, body{background-color:#ffffff;} 572 | .ui-widget{font-family:"Segoe UI", Helvetica, Verdana;} 573 | 574 | /* Interaction states 575 | ----------------------------------*/ 576 | .ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border:2px solid #dddddd;background:inherit; font-weight: bold; text-decoration:none; } 577 | .ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { text-decoration: none; } 578 | .ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { font-weight: bold; color: #ffffff; border-width:2px;} 579 | .ui-state-hover a, .ui-state-hover a:hover { color: #ffffff; text-decoration: none; } 580 | .ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #dddddd; font-weight: bold; color: #ff0084;} 581 | .ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #ff0084; text-decoration: none; } 582 | .ui-widget :active { outline: none; } 583 | 584 | /* Accordion */ 585 | .ui-accordion-header{border:2px solid;} 586 | .ui-accordion .ui-accordion-header{margin-top:2px;} 587 | .ui-accordion-content{border:2px solid;border-top:none;margin-bottom:3px !important;} 588 | .ui-accordion .ui-accordion-header a { font-size: 1.0em; padding: .6em .3em .5em 2.9em; font-weight:bold;} 589 | .ui-accordion > .ui-state-active a, .ui-accordion > div > h3.ui-state-active a { padding-bottom:.8em;} 590 | /*.ui-accordion .ui-accordion-header .ui-icon{margin-top:-12px;}*/ 591 | .ui-accordion > .ui-state-active, .ui-accordion > div > h3.ui-state-active{border-bottom:none !important;} 592 | .ui-accordion-header > .ui-icon{margin:4px;} 593 | 594 | /* Tabs */ 595 | .ui-tabs{padding:0;border:none;position:relative;top:-3px;} 596 | .ui-tabs-nav{padding:0px 0px 0px 0px !important;border:none;border-bottom:2px solid;background-color:#ffffff;} 597 | .ui-tabs-nav .ui-state-default{border:none;padding:0px !important;margin-right:2px !important;background:none !important;} 598 | .ui-tabs-nav .ui-state-default a{border:2px solid;position:relative;top:2px;font-weight:bold;margin-bottom:4px;height:16px;} 599 | /*.ui-tabs-nav .ui-tabs-selected a{border:2px solid;height:1.8em;border-bottom:none !important;}*/ 600 | .ui-tabs-nav .ui-state-active a{border:2px solid;border-bottom:none !important;margin-bottom:0;height:22px;} 601 | .ui-tabs .ui-widget-content{border:2px solid !important;border-top:none !important;} 602 | .ui-tabs .ui-widget-content .ui-tabs {border:none !important;} 603 | .ui-tabs-nav .ui-state-hover{border:none;} 604 | 605 | 606 | /* Dialog */ 607 | .ui-dialog{border-width:2px;padding:0;} 608 | .ui-dialog-titlebar {border:none;border-bottom-width:2px;padding:.6em 1em .6em 1em !important;font-weight:bold;font-size:1.1em;} 609 | .ui-dialog-buttonpane{border-top-width:2px !important;margin-top:0 !important;} 610 | .ui-dialog .ui-dialog-titlebar-close{width:24px;height:24px;margin:-13px 0px 0px 0px;padding:0;} 611 | .ui-dialog-titlebar-close > .ui-icon{margin:4px !important;} 612 | 613 | /* Datepicker*/ 614 | .ui-datepicker {border:2px solid;padding:0 !important;} 615 | .ui-datepicker .ui-datepicker-header{border:none;padding:.4em 1em .4em 1em;} 616 | .ui-datepicker .ui-datepicker-header .ui-state-default, .ui-datepicker .ui-datepicker-header .ui-state-hover{background:none;border:none;padding:0;} 617 | .ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next{width:24px;height:24px;margin:0;} 618 | .ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span{position:relative;top:0;left:0;margin:0;} 619 | .ui-datepicker .ui-datepicker-prev-hover { left:2px;top:2px; } 620 | .ui-datepicker .ui-datepicker-next-hover { right:2px;top:2px; } 621 | .ui-datepicker table {margin:0px;border:1px solid;} 622 | .ui-datepicker-calendar .ui-state-default, .ui-datepicker-calendar .ui-state-hover, .ui-datepicker-calendar .ui-state-highlight, .ui-datepicker-calendar .ui-state-active{border-width:2px;} 623 | .ui-datepicker .ui-icon{margin:4px !important;} 624 | 625 | /* Button */ 626 | .ui-button{border-width:2px !important;border-bottom-style:solid;font-weight:bold !important;} 627 | 628 | .ui-buttonset .ui-button { margin-left: 0; margin-right: -1px; } 629 | 630 | /* Additional states */ 631 | .ui-state-highlight{border:2px solid;} 632 | .ui-state-error{border:2px solid;} 633 | /*.ui-state-highlight .ui-icon, .ui-state-error .ui-icon{position:relative;top:-4px;}*/ 634 | 635 | /* Slider */ 636 | .ui-slider{border:none;} 637 | .ui-slider .ui-slider-handle{width:.8em;height:.8em;border:none;margin:-.4em;} 638 | .ui-slider-horizontal .ui-slider-handle{margin-top:0; top:0;} 639 | .ui-slider-vertical .ui-slider-handle{margin-left:0;left:0;} 640 | 641 | /* Progressbar */ 642 | .ui-progressbar{border:none;height:0.8em;} 643 | .ui-progressbar .ui-progressbar-value{border:none;} 644 | 645 | /* Autocomplete */ 646 | .ui-autocomplete {border:solid 2px #bbbbbb;padding:0;} 647 | .ui-autocomplete .ui-menu-item a{padding:5px;border:none;margin:0 !important;} 648 | 649 | /* Icon states */ 650 | .ui-icon { width: 16px; height: 16px; } 651 | /* General */ 652 | .ui-icon, .ui-widget-content .ui-icon, .ui-widget-header .ui-icon {background-image:url(images/ui-icons_d4d4d4_0.png)} 653 | .ui-state-default .ui-icon {background-image:url(images/ui-icons_525252_0.png)} 654 | .ui-state-active .ui-icon, .ui-state-hover .ui-icon{background-image:url(images/ui-icons_ffffff_0.png);} 655 | .ui-widget-overlay{background:#ffffff;} 656 | .ui-widget-content{color:#525252;} 657 | 658 | 659 | 660 | 661 | 662 | /* Button states */ 663 | .ui-state-default, .ui-widget-content .ui-state-default{background-color:#d4d4d4;border-color:#d4d4d4;color:#525252;} 664 | .ui-state-focus, .ui-widget-content .ui-state-focus{background-color:#66B3FF;border-color:#66B3FF;color:#ffffff;} 665 | .ui-state-hover, .ui-widget-content .ui-state-hover{background-color:#66B3FF;border-color:#66B3FF;color:#ffffff;} 666 | .ui-state-active, .ui-widget-content .ui-state-active{background-color:#3399ff;border-color:#3399ff;color:#ffffff;} 667 | 668 | 669 | /* Accordion */ 670 | .ui-accordion-content{border-color:#d4d4d4;background:#ffffff} 671 | .ui-accordion-header.ui-state-active, .ui-accordion-header.ui-state-active.ui-state-hover{background-color:#ffffff !important;color:#525252;border-color:#d4d4d4 !important;background-image:url(images/ui-icons_d4d4d4_0.png) !important;background-position: -230px -213px !important;} 672 | .ui-accordion-header.ui-state-active a{ color: #525252 !important;} 673 | .ui-accordion-header.ui-state-default{border-color:#3399ff;background:#3399ff;} 674 | .ui-accordion-header.ui-state-default a{ color: #ffffff;} 675 | .ui-accordion-header.ui-state-hover{background:#66B3FF;border-color:#66B3FF;} 676 | .ui-accordion-header.ui-state-hover a{color:#ffffff;} 677 | .ui-accordion-header.ui-state-active .ui-icon{background-image:url(images/ui-icons_d4d4d4_0.png) !important;} 678 | .ui-accordion-header.ui-state-default .ui-icon{background-image:url(images/ui-icons_ffffff_0.png);} 679 | .ui-accordion-header.ui-state-hover .ui-icon{background-image:url(images/ui-icons_66B3FF_0.png);} 680 | .ui-accordion-header{background-image:url(images/ui-icons_ffffff_0.png) !important;background-position: -230px -214px !important;background-repeat:no-repeat !important;} 681 | .ui-accordion-header.ui-state-hover{background-image:url(images/ui-icons_ffffff_0.png) !important;background-position: -230px -166px !important;} 682 | 683 | 684 | 685 | /* Tabs states */ 686 | .ui-tabs-nav > .ui-state-default a{border-color:#3399ff;background:#3399ff;color:#ffffff;} 687 | .ui-tabs-nav > .ui-state-active a{border-color:#d4d4d4 !important;background:#ffffff !important;color:#525252 !important;} 688 | .ui-tabs-nav > .ui-state-hover a{background:#66B3FF;border-color:#66B3FF;color:#ffffff;} 689 | .ui-tabs-nav > .ui-state-focus a{background:#66B3FF;border-color:inherit;color:#ffffff;} 690 | .ui-tabs .ui-widget-content{border-color:#d4d4d4 !important;background-color:#ffffff;} 691 | .ui-tabs-nav{border-color:#d4d4d4;} 692 | 693 | /* Dialog states */ 694 | .ui-dialog{border-color:#3399ff;background-color:#ffffff !important;} 695 | .ui-dialog-titlebar{border-color:#3399ff;color:#ffffff;background-color:#3399ff;} 696 | .ui-dialog-buttonpane{border-color:#3399ff !important;background-color:#ffffff;} 697 | .ui-dialog-content{background-color:#ffffff !important;} 698 | .ui-dialog-titlebar .ui-icon{background-image:url(images/ui-icons_ffffff_0.png);} 699 | .ui-dialog-titlebar .ui-state-hover .ui-icon{background-image:url(images/ui-icons_3399ff_0.png) !important;} 700 | .ui-dialog-titlebar-close{background-image:url(images/ui-icons_ffffff_0.png) !important;background-position: -232px -216px !important;} 701 | .ui-dialog-titlebar-close.ui-state-hover{background:url(images/ui-icons_ffffff_0.png) !important;background-position: -232px -168px !important;} 702 | 703 | 704 | 705 | 706 | 707 | /* Slider */ 708 | .ui-slider{background:#d4d4d4;} 709 | .ui-slider .ui-slider-range{background:#3399ff;} 710 | .ui-slider .ui-state-default{background-color:#000000;} 711 | .ui-slider .ui-state-active{background-color:#525252 !important;} 712 | .ui-slider .ui-state-focus{background-color:#000000;} 713 | .ui-slider .ui-state-hover{background-color:#525252;} 714 | 715 | /* Progressbar */ 716 | .ui-progressbar{background:#d4d4d4;} 717 | .ui-progressbar .ui-progressbar-value{background-color:#3399ff;} 718 | 719 | /* Autocomplete */ 720 | 721 | .ui-autocomplete {border-color:#525252;} 722 | .ui-autocomplete a{background-color:#ffffff;color:#525252;} 723 | .ui-autocomplete .ui-state-hover{background:#3399ff;color:#ffffff;} 724 | 725 | /* Datepicker */ 726 | .ui-datepicker {border-color:#3399ff;} 727 | .ui-datepicker .ui-datepicker-header{background-color:#3399ff;color:#ffffff;} 728 | .ui-datepicker table {border-color:#ffffff; } 729 | .ui-datepicker-calendar{background:#ffffff;} 730 | .ui-datepicker-calendar .ui-state-default{background-color:#d4d4d4;border-color:#d4d4d4;color:#525252;} 731 | .ui-datepicker-calendar .ui-state-hover{background-color:#66B3FF !important;border-color:#66B3FF !important;color:#ffffff !important;} 732 | .ui-datepicker-calendar .ui-state-highlight{background-color:#d4d4d4;border-color:#3399ff;color:#ffffff;} 733 | .ui-datepicker-calendar .ui-state-active{background-color:#3399ff;border-color:#3399ff;color:#ffffff;} 734 | .ui-datepicker .ui-icon {background-image:url(images/ui-icons_ffffff_0.png);} 735 | 736 | .ui-datepicker .ui-state-hover .ui-icon {background-image:url(images/ui-icons_3399ff_0.png)} 737 | .ui-datepicker-next, .ui-datepicker-prev{background-image:url(images/ui-icons_ffffff_0.png);background-position: -232px -216px;} 738 | .ui-datepicker-next-hover, .ui-datepicker-prev-hover{background-image:url(images/ui-icons_ffffff_0.png) !important;background-position: -232px -168px !important;opacity:1;} 739 | .ui-datepicker-next.ui-state-disabled, .ui-datepicker-prev.ui-state-disabled{background-image:url(images/ui-icons_ffffff_0.png);background-position: -232px -216px;} 740 | 741 | 742 | 743 | /* Highlight */ 744 | .ui-state-highlight{border-color:#efdca9;color:#525252;background:#efdca9;} 745 | .ui-state-highlight .ui-icon{background-image:url(images/ui-icons_525252_0.png);} 746 | 747 | /* Error */ 748 | .ui-state-error{border-color:#c31d1d;color:#ffffff;background:#c31d1d;} 749 | .ui-state-error .ui-icon{background-image:url(images/ui-icons_ffffff_0.png);} 750 | 751 | 752 | -------------------------------------------------------------------------------- /src/main/resources/static/css/jquery.tagit.css: -------------------------------------------------------------------------------- 1 | ul.tagit { 2 | padding: 1px 5px; 3 | overflow: auto; 4 | margin-left: inherit; /* usually we don't want the regular ul margins. */ 5 | margin-right: inherit; 6 | } 7 | ul.tagit li { 8 | display: block; 9 | float: left; 10 | margin: 2px 5px 2px 0; 11 | } 12 | ul.tagit li.tagit-choice { 13 | position: relative; 14 | line-height: inherit; 15 | } 16 | input.tagit-hidden-field { 17 | display: none; 18 | } 19 | ul.tagit li.tagit-choice-read-only { 20 | padding: .2em .5em .2em .5em; 21 | } 22 | 23 | ul.tagit li.tagit-choice-editable { 24 | padding: .2em 18px .2em .5em; 25 | } 26 | 27 | ul.tagit li.tagit-new { 28 | padding: .25em 4px .25em 0; 29 | } 30 | 31 | ul.tagit li.tagit-choice a.tagit-label { 32 | cursor: pointer; 33 | text-decoration: none; 34 | } 35 | ul.tagit li.tagit-choice .tagit-close { 36 | cursor: pointer; 37 | position: absolute; 38 | right: .1em; 39 | top: 50%; 40 | margin-top: -8px; 41 | line-height: 17px; 42 | } 43 | 44 | /* used for some custom themes that don't need image icons */ 45 | ul.tagit li.tagit-choice .tagit-close .text-icon { 46 | display: none; 47 | } 48 | 49 | ul.tagit li.tagit-choice input { 50 | display: block; 51 | float: left; 52 | margin: 2px 5px 2px 0; 53 | } 54 | ul.tagit input[type="text"] { 55 | -moz-box-sizing: border-box; 56 | -webkit-box-sizing: border-box; 57 | box-sizing: border-box; 58 | 59 | -moz-box-shadow: none; 60 | -webkit-box-shadow: none; 61 | box-shadow: none; 62 | 63 | border: none; 64 | margin: 0; 65 | padding: 0; 66 | width: inherit; 67 | background-color: inherit; 68 | outline: none; 69 | } 70 | -------------------------------------------------------------------------------- /src/main/resources/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woemler/spring-blog/3b0a6257714440566ceccb3ebf6530a2ad987561/src/main/resources/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /src/main/resources/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woemler/spring-blog/3b0a6257714440566ceccb3ebf6530a2ad987561/src/main/resources/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /src/main/resources/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woemler/spring-blog/3b0a6257714440566ceccb3ebf6530a2ad987561/src/main/resources/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * bootstrap.js v3.0.0 by @fat and @mdo 3 | * Copyright 2013 Twitter Inc. 4 | * http://www.apache.org/licenses/LICENSE-2.0 5 | */ 6 | if(!jQuery)throw new Error("Bootstrap requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]}}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one(a.support.transition.end,function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b()})}(window.jQuery),+function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function c(){f.trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one(a.support.transition.end,c).emulateTransitionEnd(150):c())};var d=a.fn.alert;a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("bs.alert");e||d.data("bs.alert",e=new c(this)),"string"==typeof b&&e[b].call(d)})},a.fn.alert.Constructor=c,a.fn.alert.noConflict=function(){return a.fn.alert=d,this},a(document).on("click.bs.alert.data-api",b,c.prototype.close)}(window.jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d)};b.DEFAULTS={loadingText:"loading..."},b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.is("input")?"val":"html",e=c.data();a+="Text",e.resetText||c.data("resetText",c[d]()),c[d](e[a]||this.options[a]),setTimeout(function(){"loadingText"==a?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons"]');if(a.length){var b=this.$element.find("input").prop("checked",!this.$element.hasClass("active")).trigger("change");"radio"===b.prop("type")&&a.find(".active").removeClass("active")}this.$element.toggleClass("active")};var c=a.fn.button;a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof c&&c;e||d.data("bs.button",e=new b(this,f)),"toggle"==c?e.toggle():c&&e.setState(c)})},a.fn.button.Constructor=b,a.fn.button.noConflict=function(){return a.fn.button=c,this},a(document).on("click.bs.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle"),b.preventDefault()})}(window.jQuery),+function(a){"use strict";var b=function(b,c){this.$element=a(b),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},b.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},b.prototype.getActiveIndex=function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},b.prototype.to=function(b){var c=this,d=this.getActiveIndex();return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},b.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},b.prototype.next=function(){return this.sliding?void 0:this.slide("next")},b.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},b.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}this.sliding=!0,f&&this.pause();var j=a.Event("slide.bs.carousel",{relatedTarget:e[0],direction:g});if(!e.hasClass("active")){if(this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var b=a(i.$indicators.children()[i.getActiveIndex()]);b&&b.addClass("active")})),a.support.transition&&this.$element.hasClass("slide")){if(this.$element.trigger(j),j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid")},0)}).emulateTransitionEnd(600)}else{if(this.$element.trigger(j),j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return f&&this.cycle(),this}};var c=a.fn.carousel;a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),g="string"==typeof c?c:f.slide;e||d.data("bs.carousel",e=new b(this,f)),"number"==typeof c?e.to(c):g?e[g]():f.interval&&e.pause().cycle()})},a.fn.carousel.Constructor=b,a.fn.carousel.noConflict=function(){return a.fn.carousel=c,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(b){var c,d=a(this),e=a(d.attr("data-target")||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,"")),f=a.extend({},e.data(),d.data()),g=d.attr("data-slide-to");g&&(f.interval=!1),e.carousel(f),(g=d.attr("data-slide-to"))&&e.data("bs.carousel").to(g),b.preventDefault()}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var b=a(this);b.carousel(b.data())})})}(window.jQuery),+function(a){"use strict";var b=function(c,d){this.$element=a(c),this.options=a.extend({},b.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.DEFAULTS={toggle:!0},b.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},b.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var b=a.Event("show.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.$parent&&this.$parent.find("> .panel > .in");if(c&&c.length){var d=c.data("bs.collapse");if(d&&d.transitioning)return;c.collapse("hide"),d||c.data("bs.collapse",null)}var e=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[e](0),this.transitioning=1;var f=function(){this.$element.removeClass("collapsing").addClass("in")[e]("auto"),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return f.call(this);var g=a.camelCase(["scroll",e].join("-"));this.$element.one(a.support.transition.end,a.proxy(f,this)).emulateTransitionEnd(350)[e](this.$element[0][g])}}},b.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?(this.$element[c](0).one(a.support.transition.end,a.proxy(d,this)).emulateTransitionEnd(350),void 0):d.call(this)}}},b.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var c=a.fn.collapse;a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c);e||d.data("bs.collapse",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.collapse.Constructor=b,a.fn.collapse.noConflict=function(){return a.fn.collapse=c,this},a(document).on("click.bs.collapse.data-api","[data-toggle=collapse]",function(b){var c,d=a(this),e=d.attr("data-target")||b.preventDefault()||(c=d.attr("href"))&&c.replace(/.*(?=#[^\s]+$)/,""),f=a(e),g=f.data("bs.collapse"),h=g?"toggle":d.data(),i=d.attr("data-parent"),j=i&&a(i);g&&g.transitioning||(j&&j.find('[data-toggle=collapse][data-parent="'+i+'"]').not(d).addClass("collapsed"),d[f.hasClass("in")?"addClass":"removeClass"]("collapsed")),f.collapse(h)})}(window.jQuery),+function(a){"use strict";function b(){a(d).remove(),a(e).each(function(b){var d=c(a(this));d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown")),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown"))})}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}var d=".dropdown-backdrop",e="[data-toggle=dropdown]",f=function(b){a(b).on("click.bs.dropdown",this.toggle)};f.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){if("ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a(''}),b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),b.prototype.constructor=b,b.prototype.getDefaults=function(){return b.DEFAULTS},b.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},b.prototype.hasContent=function(){return this.getTitle()||this.getContent()},b.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},b.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},b.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var c=a.fn.popover;a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof c&&c;e||d.data("bs.popover",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.noConflict=function(){return a.fn.popover=c,this}}(window.jQuery),+function(a){"use strict";function b(c,d){var e,f=a.proxy(this.process,this);this.$element=a(c).is("body")?a(window):a(c),this.$body=a("body"),this.$scrollElement=this.$element.on("scroll.bs.scroll-spy.data-api",f),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||(e=a(c).attr("href"))&&e.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.offsets=a([]),this.targets=a([]),this.activeTarget=null,this.refresh(),this.process()}b.DEFAULTS={offset:10},b.prototype.refresh=function(){var b=this.$element[0]==window?"offset":"position";this.offsets=a([]),this.targets=a([]);var c=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#\w/.test(e)&&a(e);return f&&f.length&&[[f[b]().top+(!a.isWindow(c.$scrollElement.get(0))&&c.$scrollElement.scrollTop()),e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){c.offsets.push(this[0]),c.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,d=c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(b>=d)return g!=(a=f.last()[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parents(".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate")};var c=a.fn.scrollspy;a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=c,this},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(window.jQuery),+function(a){"use strict";var b=function(b){this.element=a(b)};b.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.attr("data-target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.parent("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},b.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one(a.support.transition.end,e).emulateTransitionEnd(150):e(),f.removeClass("in")};var c=a.fn.tab;a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new b(this)),"string"==typeof c&&e[c]()})},a.fn.tab.Constructor=b,a.fn.tab.noConflict=function(){return a.fn.tab=c,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})}(window.jQuery),+function(a){"use strict";var b=function(c,d){this.options=a.extend({},b.DEFAULTS,d),this.$window=a(window).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(c),this.affixed=this.unpin=null,this.checkPosition()};b.RESET="affix affix-top affix-bottom",b.DEFAULTS={offset:0},b.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},b.prototype.checkPosition=function(){if(this.$element.is(":visible")){var c=a(document).height(),d=this.$window.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top()),"function"==typeof h&&(h=f.bottom());var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=c-h?"bottom":null!=g&&g>=d?"top":!1;this.affixed!==i&&(this.unpin&&this.$element.css("top",""),this.affixed=i,this.unpin="bottom"==i?e.top-d:null,this.$element.removeClass(b.RESET).addClass("affix"+(i?"-"+i:"")),"bottom"==i&&this.$element.offset({top:document.body.offsetHeight-h-this.$element.height()}))}};var c=a.fn.affix;a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof c&&c;e||d.data("bs.affix",e=new b(this,f)),"string"==typeof c&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.noConflict=function(){return a.fn.affix=c,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(window.jQuery); -------------------------------------------------------------------------------- /src/main/resources/static/js/tag-it.min.js: -------------------------------------------------------------------------------- 1 | (function(b){b.widget("ui.tagit",{options:{allowDuplicates:!1,caseSensitive:!0,fieldName:"tags",placeholderText:null,readOnly:!1,removeConfirmation:!1,tagLimit:null,availableTags:[],autocomplete:{},showAutocompleteOnFocus:!1,allowSpaces:!1,singleField:!1,singleFieldDelimiter:",",singleFieldNode:null,animate:!0,tabIndex:null,beforeTagAdded:null,afterTagAdded:null,beforeTagRemoved:null,afterTagRemoved:null,onTagClicked:null,onTagLimitExceeded:null,onTagAdded:null,onTagRemoved:null,tagSource:null},_create:function(){var a= 2 | this;this.element.is("input")?(this.tagList=b("
    ").insertAfter(this.element),this.options.singleField=!0,this.options.singleFieldNode=this.element,this.element.addClass("tagit-hidden-field")):this.tagList=this.element.find("ul, ol").andSelf().last();this.tagInput=b('').addClass("ui-widget-content");this.options.readOnly&&this.tagInput.attr("disabled","disabled");this.options.tabIndex&&this.tagInput.attr("tabindex",this.options.tabIndex);this.options.placeholderText&&this.tagInput.attr("placeholder", 3 | this.options.placeholderText);this.options.autocomplete.source||(this.options.autocomplete.source=function(a,e){var d=a.term.toLowerCase(),c=b.grep(this.options.availableTags,function(a){return 0===a.toLowerCase().indexOf(d)});this.options.allowDuplicates||(c=this._subtractArray(c,this.assignedTags()));e(c)});this.options.showAutocompleteOnFocus&&(this.tagInput.focus(function(b,d){a._showAutocomplete()}),"undefined"===typeof this.options.autocomplete.minLength&&(this.options.autocomplete.minLength= 4 | 0));b.isFunction(this.options.autocomplete.source)&&(this.options.autocomplete.source=b.proxy(this.options.autocomplete.source,this));b.isFunction(this.options.tagSource)&&(this.options.tagSource=b.proxy(this.options.tagSource,this));this.tagList.addClass("tagit").addClass("ui-widget ui-widget-content ui-corner-all").append(b('
  • ').append(this.tagInput)).click(function(d){var c=b(d.target);c.hasClass("tagit-label")?(c=c.closest(".tagit-choice"),c.hasClass("removed")||a._trigger("onTagClicked", 5 | d,{tag:c,tagLabel:a.tagLabel(c)})):a.tagInput.focus()});var c=!1;if(this.options.singleField)if(this.options.singleFieldNode){var d=b(this.options.singleFieldNode),f=d.val().split(this.options.singleFieldDelimiter);d.val("");b.each(f,function(b,d){a.createTag(d,null,!0);c=!0})}else this.options.singleFieldNode=b(''),this.tagList.after(this.options.singleFieldNode);c||this.tagList.children("li").each(function(){b(this).hasClass("tagit-new")|| 6 | (a.createTag(b(this).text(),b(this).attr("class"),!0),b(this).remove())});this.tagInput.keydown(function(c){if(c.which==b.ui.keyCode.BACKSPACE&&""===a.tagInput.val()){var d=a._lastTag();!a.options.removeConfirmation||d.hasClass("remove")?a.removeTag(d):a.options.removeConfirmation&&d.addClass("remove ui-state-highlight")}else a.options.removeConfirmation&&a._lastTag().removeClass("remove ui-state-highlight");if(c.which===b.ui.keyCode.COMMA&&!1===c.shiftKey||c.which===b.ui.keyCode.ENTER||c.which== 7 | b.ui.keyCode.TAB&&""!==a.tagInput.val()||c.which==b.ui.keyCode.SPACE&&!0!==a.options.allowSpaces&&('"'!=b.trim(a.tagInput.val()).replace(/^s*/,"").charAt(0)||'"'==b.trim(a.tagInput.val()).charAt(0)&&'"'==b.trim(a.tagInput.val()).charAt(b.trim(a.tagInput.val()).length-1)&&0!==b.trim(a.tagInput.val()).length-1))c.which===b.ui.keyCode.ENTER&&""===a.tagInput.val()||c.preventDefault(),a.options.autocomplete.autoFocus&&a.tagInput.data("autocomplete-open")||(a.tagInput.autocomplete("close"),a.createTag(a._cleanedInput()))}).blur(function(b){a.tagInput.data("autocomplete-open")|| 8 | a.createTag(a._cleanedInput())});if(this.options.availableTags||this.options.tagSource||this.options.autocomplete.source)d={select:function(b,c){a.createTag(c.item.value);return!1}},b.extend(d,this.options.autocomplete),d.source=this.options.tagSource||d.source,this.tagInput.autocomplete(d).bind("autocompleteopen.tagit",function(b,c){a.tagInput.data("autocomplete-open",!0)}).bind("autocompleteclose.tagit",function(b,c){a.tagInput.data("autocomplete-open",!1)}),this.tagInput.autocomplete("widget").addClass("tagit-autocomplete")}, 9 | destroy:function(){b.Widget.prototype.destroy.call(this);this.element.unbind(".tagit");this.tagList.unbind(".tagit");this.tagInput.removeData("autocomplete-open");this.tagList.removeClass("tagit ui-widget ui-widget-content ui-corner-all tagit-hidden-field");this.element.is("input")?(this.element.removeClass("tagit-hidden-field"),this.tagList.remove()):(this.element.children("li").each(function(){b(this).hasClass("tagit-new")?b(this).remove():(b(this).removeClass("tagit-choice ui-widget-content ui-state-default ui-state-highlight ui-corner-all remove tagit-choice-editable tagit-choice-read-only"), 10 | b(this).text(b(this).children(".tagit-label").text()))}),this.singleFieldNode&&this.singleFieldNode.remove());return this},_cleanedInput:function(){return b.trim(this.tagInput.val().replace(/^"(.*)"$/,"$1"))},_lastTag:function(){return this.tagList.find(".tagit-choice:last:not(.removed)")},_tags:function(){return this.tagList.find(".tagit-choice:not(.removed)")},assignedTags:function(){var a=this,c=[];this.options.singleField?(c=b(this.options.singleFieldNode).val().split(this.options.singleFieldDelimiter), 11 | ""===c[0]&&(c=[])):this._tags().each(function(){c.push(a.tagLabel(this))});return c},_updateSingleTagsField:function(a){b(this.options.singleFieldNode).val(a.join(this.options.singleFieldDelimiter)).trigger("change")},_subtractArray:function(a,c){for(var d=[],f=0;f=this.options.tagLimit)return this._trigger("onTagLimitExceeded",null,{duringInitialization:d}),!1;var g=b(this.options.onTagClicked?'':'').text(a),e=b("
  • ").addClass("tagit-choice ui-widget-content ui-state-default ui-corner-all").addClass(c).append(g); 14 | this.options.readOnly?e.addClass("tagit-choice-read-only"):(e.addClass("tagit-choice-editable"),c=b("").addClass("ui-icon ui-icon-close"),c=b('\u00d7').addClass("tagit-close").append(c).click(function(a){f.removeTag(e)}),e.append(c));this.options.singleField||(g=g.html(),e.append(''));!1!==this._trigger("beforeTagAdded",null,{tag:e,tagLabel:this.tagLabel(e), 15 | duringInitialization:d})&&(this.options.singleField&&(g=this.assignedTags(),g.push(a),this._updateSingleTagsField(g)),this._trigger("onTagAdded",null,e),this.tagInput.val(""),this.tagInput.parent().before(e),this._trigger("afterTagAdded",null,{tag:e,tagLabel:this.tagLabel(e),duringInitialization:d}),this.options.showAutocompleteOnFocus&&!d&&setTimeout(function(){f._showAutocomplete()},0))},removeTag:function(a,c){c="undefined"===typeof c?this.options.animate:c;a=b(a);this._trigger("onTagRemoved", 16 | null,a);if(!1!==this._trigger("beforeTagRemoved",null,{tag:a,tagLabel:this.tagLabel(a)})){if(this.options.singleField){var d=this.assignedTags(),f=this.tagLabel(a),d=b.grep(d,function(a){return a!=f});this._updateSingleTagsField(d)}if(c){a.addClass("removed");var d=this._effectExists("blind")?["blind",{direction:"horizontal"},"fast"]:["fast"],g=this;d.push(function(){a.remove();g._trigger("afterTagRemoved",null,{tag:a,tagLabel:g.tagLabel(a)})});a.fadeOut("fast").hide.apply(a,d).dequeue()}else a.remove(), 17 | this._trigger("afterTagRemoved",null,{tag:a,tagLabel:this.tagLabel(a)})}},removeTagByLabel:function(a,b){var d=this._findTagByLabel(a);if(!d)throw"No such tag exists with the name '"+a+"'";this.removeTag(d,b)},removeAll:function(){var a=this;this._tags().each(function(b,d){a.removeTag(d,!1)})}})})(jQuery); 18 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/tags/adminBlogEditHelp.tag: -------------------------------------------------------------------------------- 1 | <%@tag description="Shows PegDown syntax help" pageEncoding="UTF-8"%> 2 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 3 | <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> 4 | 5 | 6 |
    7 | 8 | 9 |

    PegDown Syntax

    10 |

    11 |

      12 |
    • # This is an H1
    • 13 |
    • ## This is an H2
    • 14 |
    • etc...
    • 15 |
    •  
    • 16 | 17 |
    • > This is a blockquote
    • 18 |
    • >> This is a nested quote
    • 19 |
    •     This is a code block
    • 20 |
    • `This is a code snippet`
    • 21 |
    •  
    • 22 | 23 |
    • - This is an unordered list item
    • 24 |
    • 1. This is an ordered list item
    • 25 |
    •  
    • 26 | 27 |
    • Blank lines create paragraphs
    • 28 |
    • ----This is a horizontal rule
    • 29 |
    • _this has emphasis_
    • 30 |
    • __this is strong__
    • 31 |
    • \ Backslashes escape special characters
    • 32 |
    •  
    • 33 | 34 |
    • [Link](http://www.url.com)
    • 35 |
    • [Link](http://www.url.com *Title*)
    • 36 |
    • [Link](/local/url/)
    • 37 |
    • <http://www.url.com>
    • 38 |
    • <someone@email.com>
    • 39 |
    • ![Alt text](/path/to/img.jpg)
    • 40 |
    • ![Alt text](/path/to/img.jpg *Title*)
    • 41 |
    •  
    • 42 | 43 |
    • More details here.
    • 44 | 45 |
    46 |

    47 | 48 |

    Status

    49 |

    50 | Posts with a status of "Draft" will not show up on the main page, but can be accessed by direct URL link. Posts with a status of "Live" can be accessed by any means. 51 |

    52 | 53 |

    Comments

    54 |

    55 | Check "Enable Comments" to allow commenting in posts. Posts that do not have this option checked will not show the commenting tools in the post details, and will not show post counts in summary. 56 |

    57 | 58 | 59 |
    -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/tags/adminSidebar.tag: -------------------------------------------------------------------------------- 1 | <%@tag description="Creates a list of blog post summaries" pageEncoding="UTF-8"%> 2 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 3 | <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> 4 | 5 | 6 |
    7 |

    Help

    8 |

    Admin help information will go here.

    9 |
    -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/tags/blogPostList.tag: -------------------------------------------------------------------------------- 1 | <%@tag description="Creates a list of blog post summaries" pageEncoding="UTF-8"%> 2 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 3 | <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> 4 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> 5 | 6 | 7 | <%@ attribute name="blogPosts" required="true" type="java.util.ArrayList" description="List of blog post objects" %> 8 | 9 | 10 | 11 | 12 | 13 | 14 |
    15 |

    ${post.title}

    16 |

    ${post.printPostDate()} GMT

    17 | 18 |

    ${post.getPostPreview()}

    19 | 20 | 38 |
    39 |
    40 |
    41 |
    42 |
    43 | 44 | 54 | 55 |
    56 | 57 |

    No posts to display.

    58 |
    59 |
    60 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/tags/blogSidebar.tag: -------------------------------------------------------------------------------- 1 | <%@tag description="Creates a list of blog post summaries" pageEncoding="UTF-8"%> 2 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 3 | <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> 4 | 5 | 6 |

    7 | 8 | 9 | Fork me on GitHub! 10 | 11 |

    12 | 13 |

    Links

    14 | 19 | 20 |

    More Links

    21 | 26 | 27 |

    Even More Links

    28 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/tags/footer.tag: -------------------------------------------------------------------------------- 1 | <%@tag description="Page footer" pageEncoding="UTF-8"%> 2 | 3 |
    4 |
    5 |

    © Company 2013

    6 |
    7 |
    8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/tags/header.tag: -------------------------------------------------------------------------------- 1 | <%@tag description="Page header" pageEncoding="UTF-8"%> 2 | 3 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 4 | <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> 5 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> 6 | <%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %> 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | <spring:eval expression="@environment.getProperty('app.blog.title')" /> 25 | 26 | 27 | 28 | 29 | 61 | 62 |
    63 |
    64 | 65 | 66 | 67 | 70 | 71 | 72 | 75 |

    76 |
    77 |
    78 | 79 | 80 |
    81 |
    82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/jsp/about.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib prefix="blog-tags" tagdir="/WEB-INF/tags" %> 2 | 3 | 4 | 5 | 8 | 9 |
    10 |
    11 |
    12 |

    About

    13 | 14 |

    This Blog

    15 |

    16 | Site info goes here. 17 |

    18 | 19 |

    The Author

    20 |

    21 | Personal bio goes here. 22 |

    23 | 24 |

    This Application

    25 |

    26 | This site is powered by a custom Java Spring MVC web application, 27 | built from scratch. It uses Spring 3, Hibernate 28 | 4, and Bootstrap 3 to create a simple, modern, and responsive web 29 | application. This application is open-source, you can find the 30 | project repository 31 | here. Please feel free to contribute, fork, or redistribute 32 | the source code as you see fit. 33 |

    34 |
    35 | 36 |
    37 | 38 |
    39 |
    40 |
    41 | 42 | 43 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/jsp/admin/admin.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib prefix="blog-tags" tagdir="/WEB-INF/tags" %> 2 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 3 | 4 | 5 | 6 |
    7 |
    8 | 9 |
    10 | 11 |

    Admin Tools

    12 | 13 |
      14 | 15 | 16 |
    • 17 | Blog Posts 18 |
      19 | ${postCount} 20 | 21 |
      22 |
    • 23 | 24 | 25 |
    • 26 | Tags 27 |
      28 | ${tagCount} 29 | 30 |
      31 |
    • 32 | 33 |
    34 | 35 |
    36 | 37 |
    38 | 39 |
    40 | 41 |
    42 |
    43 | 44 | 45 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/jsp/admin/blogPostList.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib prefix="blog-tags" tagdir="/WEB-INF/tags" %> 2 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 3 | <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> 4 | 5 | 6 | 7 |
    8 |
    9 | 10 |
    11 | 12 |

    Blog Posts

    13 | 14 | 15 |

    16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 44 | 45 | 46 | 47 | 48 |
    TitleSlugDateStatusComments
    ${post.title}${post.slug}${post.printPostDate()}${post.status} 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
    49 |
    50 | 51 | No posts available. 52 | 53 |
    54 | 55 |
    56 | 57 |
    58 | 59 |
    60 | 61 |
    62 |
    63 | 64 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/jsp/admin/editBlogPost.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %> 2 | <%@ taglib prefix="blog-tags" tagdir="/WEB-INF/tags" %> 3 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 4 | 5 | 6 | 7 | 8 | 9 | 10 |
    11 |
    12 | 13 |
    14 | 15 |

    Add/Edit Blog Post

    16 | 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 | 62 | 63 | 64 | 65 |
      66 | 67 |
      68 | 69 | 70 | 74 | 75 | 76 | 77 | 82 | 83 |

      84 | 85 | 86 |
      87 |
      88 | 89 | Reset 90 | Cancel 91 |
      92 |
      93 | 94 |
      95 | 96 |
      97 |
      98 | 99 |
      100 | 101 |
      102 | 103 |
      104 | 105 |
      106 |
      107 | 108 | 109 | 110 | 111 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/jsp/admin/editTag.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %> 2 | <%@ taglib prefix="blog-tags" tagdir="/WEB-INF/tags" %> 3 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 4 | 5 | 6 | 7 |
      8 |
      9 | 10 |
      11 | 12 |

      Add/Edit Tag

      13 | 14 | 15 |
      16 |
      17 | 18 |
      19 | 20 |
      21 | 22 |
      23 | 24 |
      25 | 26 |
      27 | 28 | 29 |
      30 |
      31 | Save 32 | Reset 33 | Cancel 34 |
      35 |
      36 | 37 |
      38 | 39 |
      40 |
      41 | 42 |
      43 | 44 |
      45 | 46 |
      47 | 48 |
      49 |
      50 | 51 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/jsp/admin/tagList.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib prefix="blog-tags" tagdir="/WEB-INF/tags" %> 2 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 3 | 4 | 5 | 6 |
      7 |
      8 | 9 |
      10 | 11 |

      Tags

      12 | 13 | 14 |

      15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
      TagCount
      ${tag.tagName}${tagCounts.get(tag.tagId)}
      31 |
      32 | 33 | No tags available. 34 | 35 |
      36 | 37 |
      38 | 39 |
      40 | 41 |
      42 | 43 |
      44 |
      45 | 46 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/jsp/blogPost.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib prefix="blog-tags" tagdir="/WEB-INF/tags" %> 2 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 3 | 4 | 5 | 6 | 9 | 10 |
      11 |
      12 | 13 |
      14 | 15 | 16 |
      17 |

      ${requestedPost.title}

      18 |

      ${requestedPost.printPostDate()} GMT

      19 |

      20 | Tags:  21 | 22 | ${tag}  23 | 24 |

      25 |

      ${requestedPost.getMarkup()}

      26 | 27 | 28 | 46 | 47 | 48 | 49 | 50 |

      51 |

      Comments

      52 | <%----%> 53 |

      54 |
      55 |
      56 | 57 |
      58 | 59 |
      60 | 61 |
      62 | 63 |
      64 | 65 |
      66 |
      67 | 68 | 69 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/jsp/code.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib prefix="blog-tags" tagdir="/WEB-INF/tags" %> 2 | 3 | 4 | 5 | 8 | 9 |
      10 |
      11 |
      12 |

      Code

      13 | 14 | 15 | 16 | 17 |

      My GitHub Projects

      18 | 19 |
      20 |

      21 | ${repo.title} 22 | 23 | 24 | 25 | Download 26 | 27 | 28 |

      29 |

      ${repo.description}

      30 |
      31 |
      32 |
      33 | 34 |
      35 |
      36 | 37 |
      38 | 39 |
      40 |
      41 |
      42 | 43 | 44 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/jsp/home.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib prefix="blog-tags" tagdir="/WEB-INF/tags" %> 2 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 3 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> 4 | <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> 5 | 6 | 7 | 8 | 12 | 13 |
      14 |
      15 | 16 |
      17 | 18 | 19 | 20 | 21 | 22 |
      23 |
        24 | 25 | 26 | 27 |
      • «
      • 28 |
        29 | 30 |
      • «
      • 31 |
        32 |
        33 | 34 | 35 | 36 | 37 | 38 |
      • ${page}
      • 39 |
        40 | 41 |
      • ${page}
      • 42 |
        43 |
        44 |
        45 | 46 | 47 | 48 | 49 |
      • »
      • 50 |
        51 | 52 |
      • »
      • 53 |
        54 |
        55 | 56 |
      57 |
      58 | 59 |
      60 | 61 |
      62 | 63 |
      64 | 65 |
      66 |
      67 | 68 | 69 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/jsp/login.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib prefix="blog-tags" tagdir="/WEB-INF/tags" %> 2 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 3 | 4 | 5 | 6 | 7 |
      8 |
      9 |
      10 |
      11 |
      12 |
      13 |

      Login

      14 | 15 |
      16 | 17 | 18 |
      19 | 20 |
      21 | 22 | 23 |
      24 | 25 | 26 | Cancel 27 | 28 | 29 |

      30 |
      31 |
      32 |

      Login Failed:

      33 | Error: ${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message} 34 |
      35 |
      36 |
      37 | 38 | 39 |

      40 |
      41 |
      42 |

      Logout Successful

      43 |
      44 |
      45 |
      46 | 47 | 48 | 49 |
      50 |
      51 |
      52 |
      53 |
      54 | 55 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/jsp/media.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib prefix="blog-tags" tagdir="/WEB-INF/tags" %> 2 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 3 | 4 | 5 | 6 | 9 | 10 |
      11 |
      12 |
      13 |

      Media

      14 | 15 |

      Images

      16 | 17 | 18 | 19 | 20 |
      21 | 22 |
      23 |
      24 |
      25 | 26 |
      No images to display.
      27 |
      28 |
      29 |
      30 | 31 |
      32 | 33 |
      34 |
      35 |
      36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/jsp/tagDetails.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib prefix="blog-tags" tagdir="/WEB-INF/tags" %> 2 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 3 | 4 | 5 | 6 |
      7 |
      8 | 9 |
      10 | 11 |

      Tag: ${tag.tagName}

      12 | 13 | 14 | 15 |
      16 | 17 |
      18 | 19 |
      20 | 21 |
      22 |
      23 | 24 | 25 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/jsp/tagList.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib prefix="blog-tags" tagdir="/WEB-INF/tags" %> 2 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 3 | 4 | 5 | 6 |
      7 |
      8 | 9 |
      10 | 11 |

      Tags

      12 | 13 |
        14 | 15 |
      • 16 |

        ${tag.tagName} 17 | ${tagCounts.get(tag.tagId)}

        18 |
      • 19 |
        20 |
      21 | 22 |
      23 | 24 |
      25 | 26 |
      27 | 28 |
      29 |
      30 | 31 | 32 | -------------------------------------------------------------------------------- /src/test/java/me/woemler/springblog/test/RepositoryTests.java: -------------------------------------------------------------------------------- 1 | package me.woemler.springblog.test; 2 | 3 | import me.woemler.springblog.models.BlogPost; 4 | import me.woemler.springblog.repositories.BlogRepository; 5 | import me.woemler.springblog.test.config.TestApplicationConfig; 6 | import me.woemler.springblog.test.config.TestDataSourceConfig; 7 | import org.junit.Before; 8 | import org.junit.FixMethodOrder; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.test.context.ContextConfiguration; 13 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 14 | import org.springframework.util.Assert; 15 | 16 | import java.util.*; 17 | 18 | /** 19 | * @author woemler 20 | */ 21 | 22 | @RunWith(SpringJUnit4ClassRunner.class) 23 | @ContextConfiguration(classes = {TestApplicationConfig.class, TestDataSourceConfig.class}) 24 | @FixMethodOrder 25 | public class RepositoryTests { 26 | 27 | @SuppressWarnings("SpringJavaAutowiringInspection") @Autowired BlogRepository blogRepository; 28 | 29 | @Before 30 | public void setup(){ 31 | blogRepository.deleteAll(); 32 | BlogPost post1 = 33 | new BlogPost(null, "Test Post1", "test-post1", "This is a test blog post with active comments.", 34 | new Date(), BlogPost.STATUS_ACTIVE, true, new HashSet<>(Arrays.asList("news"))); 35 | BlogPost post2 = 36 | new BlogPost(null, "Test Post2", "test-post2", "This is a test blog post that is inactive.", 37 | new Date(), BlogPost.STATUS_INACTIVE, true, new HashSet<>(Arrays.asList("test"))); 38 | BlogPost post3 = new BlogPost(null, "Test Post3", "test-post3", 39 | "This is a test blog post with inactive comments.", 40 | new Date(), BlogPost.STATUS_ACTIVE, false, new HashSet<>(Arrays.asList("test", "news"))); 41 | blogRepository.save(post1); 42 | blogRepository.save(post2); 43 | blogRepository.save(post3); 44 | } 45 | 46 | @Test 47 | public void findAll(){ 48 | 49 | List posts = blogRepository.findAll(); 50 | Assert.notNull(posts); 51 | Assert.notEmpty(posts); 52 | Assert.isTrue(posts.size() == 3); 53 | BlogPost post = posts.get(0); 54 | Assert.isTrue(post.getPostId() != null); 55 | System.out.println(post.toString()); 56 | 57 | List tags = blogRepository.findAllTags(); 58 | Assert.notNull(tags); 59 | Assert.notEmpty(tags); 60 | Assert.isTrue(tags.size() == 2); 61 | 62 | } 63 | 64 | @Test 65 | public void findBlogPostBySlug(){ 66 | BlogPost post = blogRepository.findBySlug("test-post3"); 67 | Assert.notNull(post); 68 | Assert.isTrue(post.getSlug().equals("test-post3")); 69 | post.getTags().size(); 70 | Set tags = post.getTags(); 71 | Assert.notNull(tags); 72 | Assert.notEmpty(tags); 73 | Assert.isTrue(tags.size() == 2); 74 | } 75 | 76 | @Test 77 | public void findAllTags(){ 78 | List tags = blogRepository.findAllTags(); 79 | Assert.notNull(tags); 80 | Assert.notEmpty(tags); 81 | Assert.isTrue(tags.size() == 2); 82 | Assert.isTrue(tags.get(0).equals("test") || tags.get(0).equals("news")); 83 | } 84 | 85 | @Test 86 | public void findTagByFragment(){ 87 | List tags = blogRepository.findTagsByFragment("te"); 88 | System.out.println(tags.toString()); 89 | Assert.notNull(tags); 90 | Assert.notEmpty(tags); 91 | Assert.isTrue(tags.size() == 1); 92 | Assert.isTrue(tags.get(0).equals("test")); 93 | } 94 | 95 | @Test 96 | public void createPost(){ 97 | BlogPost post = new BlogPost(null, "JUnit is great!", "junit-is-great", "Don't you think so?", 98 | new Date(), BlogPost.STATUS_ACTIVE, false, new HashSet<>(Arrays.asList("junit"))); 99 | blogRepository.save(post); 100 | Assert.notNull(post.getPostId()); 101 | Assert.isTrue(blogRepository.count() == 4); 102 | Assert.isTrue(blogRepository.findAllTags().size() == 3); 103 | } 104 | 105 | @Test 106 | public void updatePost(){ 107 | createPost(); 108 | BlogPost post = blogRepository.findBySlug("junit-is-great"); 109 | Assert.notNull(post); 110 | post.setSlug("junit-post"); 111 | blogRepository.save(post); 112 | post = blogRepository.findBySlug("junit-is-great"); 113 | Assert.isNull(post); 114 | post = blogRepository.findBySlug("junit-post"); 115 | Assert.notNull(post); 116 | } 117 | 118 | @Test 119 | public void deletePost(){ 120 | createPost(); 121 | BlogPost post = blogRepository.findBySlug("junit-is-great"); 122 | blogRepository.delete(post.getPostId()); 123 | post = blogRepository.findBySlug("junit-is-great"); 124 | Assert.isNull(post); 125 | Assert.isTrue(blogRepository.count() == 3); 126 | Assert.isTrue(blogRepository.findAllTags().size() == 2); 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /src/test/java/me/woemler/springblog/test/config/TestApplicationConfig.java: -------------------------------------------------------------------------------- 1 | package me.woemler.springblog.test.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.context.annotation.PropertySource; 5 | 6 | /** 7 | * @author woemler 8 | */ 9 | 10 | @Configuration 11 | @PropertySource("classpath:test-blog.properties") 12 | public class TestApplicationConfig { 13 | } 14 | -------------------------------------------------------------------------------- /src/test/java/me/woemler/springblog/test/config/TestDataSourceConfig.java: -------------------------------------------------------------------------------- 1 | package me.woemler.springblog.test.config; 2 | 3 | import com.mongodb.Mongo; 4 | import com.mongodb.MongoClient; 5 | import com.mongodb.MongoCredential; 6 | import com.mongodb.ServerAddress; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.context.annotation.Import; 11 | import org.springframework.core.env.Environment; 12 | import org.springframework.data.mongodb.config.AbstractMongoConfiguration; 13 | import org.springframework.data.mongodb.core.MongoTemplate; 14 | import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; 15 | 16 | import java.io.IOException; 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | 20 | import cz.jirutka.spring.embedmongo.EmbeddedMongoBuilder; 21 | 22 | /** 23 | * @author woemler 24 | */ 25 | 26 | @Configuration 27 | @Import({ TestApplicationConfig.class }) 28 | @EnableMongoRepositories(basePackages = "me.woemler.springblog.repositories") 29 | public class TestDataSourceConfig { 30 | 31 | @Bean(destroyMethod = "close") 32 | public Mongo mongo() throws IOException { 33 | return new EmbeddedMongoBuilder().build(); 34 | } 35 | 36 | @Bean 37 | public MongoTemplate mongoTemplate(Mongo mongo){ 38 | return new MongoTemplate(mongo, "blog-test"); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/me/woemler/springblog/test/config/TestWebAppConfig.java: -------------------------------------------------------------------------------- 1 | package me.woemler.springblog.test.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.ComponentScan; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.data.web.config.EnableSpringDataWebSupport; 7 | import org.springframework.hateoas.config.EnableEntityLinks; 8 | import org.springframework.hateoas.config.EnableHypermediaSupport; 9 | import org.springframework.http.MediaType; 10 | import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; 11 | import org.springframework.web.servlet.config.annotation.EnableWebMvc; 12 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 13 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 14 | import org.springframework.web.servlet.view.InternalResourceViewResolver; 15 | 16 | /** 17 | * @author woemler 18 | */ 19 | 20 | @Configuration 21 | @EnableWebMvc 22 | @EnableHypermediaSupport(type = { EnableHypermediaSupport.HypermediaType.HAL }) 23 | @EnableEntityLinks 24 | @ComponentScan(basePackages = { "me.woemler.springblog.controllers", "me.woemler.springblog.services" }) 25 | @EnableSpringDataWebSupport 26 | public class TestWebAppConfig extends WebMvcConfigurerAdapter { 27 | 28 | @Override 29 | public void addResourceHandlers(ResourceHandlerRegistry registry){ 30 | registry.addResourceHandler("/static/**").addResourceLocations("/static/"); 31 | } 32 | 33 | @Bean 34 | public InternalResourceViewResolver jspViewResolver(){ 35 | InternalResourceViewResolver resolver = new InternalResourceViewResolver(); 36 | resolver.setPrefix("/WEB-INF/views/"); 37 | resolver.setSuffix(".jsp"); 38 | return resolver; 39 | } 40 | 41 | @Override 42 | public void configureContentNegotiation(ContentNegotiationConfigurer configurer){ 43 | configurer.defaultContentType(MediaType.APPLICATION_JSON); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/test/resources/test-blog.properties: -------------------------------------------------------------------------------- 1 | 2 | --------------------------------------------------------------------------------