diff --git a/.bowerrc b/.bowerrc new file mode 100644 index 0000000..950ad51 --- /dev/null +++ b/.bowerrc @@ -0,0 +1,4 @@ +{ + "allow_root": true, + "directory": "src/main/resources/static/bower_components" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 69b29ee..bdd8046 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,9 @@ build bin scripts +classes +out *.iml +*.ipr +*.iws +src/main/resources/static/bower_components diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..82da1e6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2015, Jiankun LEI. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. Neither the name of rumps nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 312e369..d521a7c 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,97 @@ SpringBlog ===== -SpringBlog is a very simple blog system implemented with Spring MVC. -It's one of my learning projects which took me two days to develop the first minimal and runnable version. I've put it on production -for my personal website [http://raysmond.com](http://raysmond.com). +中文开发和部署文档请查看:http://raysmond.com/posts/springblog-guide + +SpringBlog is a very simple and clean-design blog system implemented with Spring Boot. +It's one of my learning projects to explore awesome features in Spring Boot web programming. You can check my blog +site for demo [https://raysmond.com](http://raysmond.com). + +There's no demo online. Here's the screenshot of my previous blog homepage. +![](http://7b1fa0.com1.z0.glb.clouddn.com/screencapture-blog-raysmond-8080-1480663084590.png) SpringBlog is powered by many powerful frameworks and third-party projects: -- Spring MVC + Spring JPA + Hibernate - Powerful frameworks +- Spring Boot and many of Spring familiy (e.g. Spring MVC, Spring JPA, Spring Secruity and etc) +- Hibernate + MySQL - [HikariCP](https://github.com/brettwooldridge/HikariCP) - A solid high-performance JDBC connection pool - [Bootstrap](https://getbootstrap.com) - A very popular and responsive front-end framework - [Pegdown](https://github.com/sirthias/pegdown) - A pure-java markdown processor -- [ACE Editor](http://ace.c9.io/) - A high performance code editor -- [Pygments](http://pygments.org/) - Python syntax highlighter -- [Jade4j](https://github.com/neuland/jade4j) - [Jade](http://jade-lang.com/) is an elegant template language -- [Webjars](http://www.webjars.org/) - A client-side web libraries packaged into JAR files. A easy way to manage JavaScript and CSS vendors in Gradle. +- [ACE Editor](http://ace.c9.io/) - A high performance code editor which I use to write posts and code. +- [Redis](http://redis.io/) - A very powerful in-memory data cache server. +- EhCache +- Thymeleaf (Spring MVC) ## Development -Make sure Gradle is installed in your machine. Try `gradle -v` command. Otherwise install in from [http://www.gradle.org/](http://www.gradle.org/). +Before development, please install the following service software: + +- [MySQL](https://www.mysql.com) -Before development, please install MySQL5+ first and edit `persistence.properties` according to your database configurations. +Edit the spring config profile `src/main/resources/application.yml` according to your settings. + +And start MySQL and Redis first before running the application. + +``` +# If you're using Ubuntu server +# Install MySQL +apt-get install mysql-server +service mysql start +mysql -u root -p +>> create database spring_blog; ``` -# Install artifacts to your local repository -./gradlew build +This is a Gradle project. Make sure Gradle is installed in your machine. +Try `gradle -v` command. Otherwise install in from [http://www.gradle.org/](http://www.gradle.org/). +I recommend you import the source code into Intellij IDE to edit the code. + +``` # Start the web application -./gradlew jettyRun +./gradlew bootRun ``` -View `http://localhost:8080/SpringBlog` on your browser. \ No newline at end of file +## Development + +**How to import the project into Intellij IDEA and run from the IDE?** + +``` +git clone https://github.com/Raysmond/SpringBlog.git +cd SpringBlog + +bower install +``` + +1. Clone the project +2. Download all dependencies +3. Open the project in Intellij IDEA. +4. Run `SpringBlogApplication.java` as Java application. +5. Preview: http://localhost:8080 + Admin: http://localhost:8080/admin , the default admin account is: `admin`, password: `admin` + + +> Lombok is required to run the project. You can install the plugin in Intellij IDEA. +> Reference: https://github.com/mplushnikov/lombok-intellij-plugin + + +- Build application jar `./gradlew build`, then upload the distribution jar (e.g. `build/libs/SpringBlog-0.1.jar`) to your remote server. +- Upload `application-production.yml` to your server and change it according to your server settings. +- Run it (Java8 is a must) + + ``` + java -jar SpringBlog-0.1.jar --spring.profiles.active=prod + # OR with external spring profile file + java -jar SpringBlog-0.1.jar --spring.config.location=application-production.yml + ``` + +## TODO + +- [x] Upgrade frontend framework to Bootstrap4 +- [ ] Replace Jade with Thymeleaf(HTML) +- [ ] Frontend building tools, e.g. webpack +- [x] Use hibernate 2nd level cache (EHCache?) +- [ ] Markdown preview while editing +- [ ] Html editor + +## License +Modified BSD license. Copyright (c) 2015 - 2018, Jiankun LEI (Raysmond). diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..2b4f287 --- /dev/null +++ b/bower.json @@ -0,0 +1,18 @@ +{ + "name": "springblog", + "version": "0.0.0", + "appPath": "src/main/resources/static", + "testPath": "src/test/javascript/spec", + "dependencies": { + "jquery": "3.2.1", + "bootstrap": "4.0.0", + "fontawesome": "4.7.0", + "marked": "0.3.5", + "to-markdown": "1.3.0", + "highlightjs": "9.12.0", + "ace-builds": "1.2.9", + "contents": "4.0.2", + "sticky-kit": "1.1.2" + }, + "devDependencies": {} +} diff --git a/build.gradle b/build.gradle index c25ca43..e9012dc 100644 --- a/build.gradle +++ b/build.gradle @@ -1,71 +1,71 @@ apply plugin: 'java' -apply plugin: 'war' apply plugin: 'idea' apply plugin: 'eclipse' -apply plugin: 'org.akhikhl.gretty' -//apply plugin: 'spring-boot' +apply plugin: 'org.springframework.boot' -// JDK 8 +group = 'com.raysmond.blog' +version = '0.0.1-SNAPSHOT' sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -version = '0.1' repositories { mavenLocal() jcenter() mavenCentral() - + maven { url 'http://maven.aliyun.com/nexus/content/groups/public' } maven { url 'http://repo.spring.io/libs-release' } - maven { url "http://repo.springsource.org/repo" } // for springloaded + maven { url 'http://repo.springsource.org/repo' } } - +bootRun { + systemProperties = System.properties +} +buildscript { + ext { + springBootVersion = '1.5.10.RELEASE' + } + repositories { + mavenLocal() + jcenter() + mavenCentral() + maven { url 'http://maven.aliyun.com/nexus/content/groups/public' } + } + dependencies { + classpath('org.springframework.boot:spring-boot-gradle-plugin:1.5.10.RELEASE') + } +} dependencies { // spring boot - compile("org.springframework.boot:spring-boot-starter-web") - - // spring framework - compile 'org.springframework:spring-context:4.2.1.RELEASE' - compile 'org.springframework:spring-webmvc:4.1.7.RELEASE' - compile 'org.springframework.security:spring-security-config:3.2.7.RELEASE' - compile 'org.springframework.security:spring-security-web:3.2.7.RELEASE' - - compile 'org.thymeleaf:thymeleaf-spring4:+' - compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity3:+' - - //persistence - compile 'com.zaxxer:HikariCP:+' - compile 'org.springframework:spring-orm:+' - compile 'org.hibernate:hibernate-entitymanager:+' - compile 'javax.el:javax.el-api:+' - compile 'org.hsqldb:hsqldb:+' + compile 'org.springframework.boot:spring-boot-starter-web' + compile 'org.springframework.boot:spring-boot-starter-thymeleaf' + compile 'com.domingosuarez.boot:spring-boot-starter-jade4j:0.3.1' + compile 'org.springframework.boot:spring-boot-starter-redis:1.4.7.RELEASE' + compile 'org.springframework.boot:spring-boot-starter-data-jpa' + compile 'org.springframework.boot:spring-boot-starter-security' + compile 'org.springframework.boot:spring-boot-starter-logging' + compile 'org.springframework.boot:spring-boot-devtools' + compile 'org.springframework.boot:spring-boot-starter-actuator' + compile 'org.springframework.boot:spring-boot-starter-cache' // view + compile 'net.sourceforge.nekohtml:nekohtml:1.9.22' + compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity4:2+' compile 'de.neuland-bfi:spring-jade4j:0.4.2' compile 'org.pegdown:pegdown:1.6.0' + compile 'com.vladsch.flexmark:flexmark-all:0.28.38' // python compile 'org.python:jython-standalone:2.7.0' compile 'org.pygments:pygments:2.0.2' - // spring data - compile 'org.springframework.data:spring-data-jpa:+' - - // MySQL + // mysql/hibernate compile 'mysql:mysql-connector-java:+' - - // Validation - compile 'org.hibernate:hibernate-validator:+' - - // Logging - compile 'ch.qos.logback:logback-classic:1.1.3' - compile 'org.slf4j:slf4j-api:+' - compile 'org.apache.commons:commons-lang3:+' - - // @Inject - compile 'javax.inject:javax.inject:+' + compile 'org.hibernate:hibernate-core:5.2.12.Final' + compile 'org.hibernate:hibernate-entitymanager:5.2.12.Final' + compile 'org.hibernate:hibernate-validator' + compile 'org.hibernate:hibernate-ehcache:5.2.12.Final' + compile 'com.zaxxer:HikariCP:2.7.6' + compile 'net.sf.ehcache:ehcache' // JSON compile 'com.fasterxml.jackson.core:jackson-databind:2.4.1.3' @@ -73,56 +73,13 @@ dependencies { // Utilities compile 'com.google.guava:guava:+' - - // static resources, ref. http://www.webjars.org/ - compile 'org.webjars:jquery:2.1.4' - compile 'org.webjars:bootstrap:3.3.5' - compile 'org.webjars:font-awesome:4.3.0-3' - compile 'org.webjars:ace:1.2.0' + compile 'org.modelmapper:modelmapper:+' + compile 'org.projectlombok:lombok:+' + compile 'org.apache.commons:commons-lang3:+' // test - testCompile("org.springframework.boot:spring-boot-starter-test") testCompile 'junit:junit:+' - testCompile 'org.mockito:mockito-core:+' - testCompile 'org.assertj:assertj-core:+' - testCompile 'org.hamcrest:hamcrest-core:+' - testCompile 'org.hamcrest:hamcrest-library:+' - testCompile 'org.objenesis:objenesis:+' - testCompile 'org.springframework:spring-test:+' - - compile 'javax.servlet:jstl:1.2' - - //include in compile only, exclude in the war - providedCompile 'javax.servlet:servlet-api:2.5' -} - -//Gretty Embedded Jetty -buildscript { - ext { - springBootVersion = '1.2.6.RELEASE' - } - - repositories { - mavenLocal() - jcenter() - - maven { url "http://repo.spring.io/release" } - maven { url "http://repo.spring.io/milestone" } - maven { url "http://repo.spring.io/snapshot" } - } - - dependencies { - classpath 'org.akhikhl.gretty:gretty:+' - classpath 'org.springframework:springloaded:1.2.4.RELEASE' - classpath "org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}" - } + testCompile('org.springframework.boot:spring-boot-starter-test') + testCompile('org.springframework.security:spring-security-test') } -// Don't use Jetty8, even it's a servlet 3.0+ container, -// but not support non-jar WebApplicationInitializer scanning. -// It will cause "No Spring WebApplicationInitializer types detected on classpath" -gretty { - port = 8080 - contextPath = 'SpringBlog' - servletContainer = 'jetty9' //tomcat7 or tomcat8 -} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3ba529a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: '3' +services: + mysql: + image: mysql + environment: + - MYSQL_USER=root + - MYSQL_DATABASE=spring_blog_prod + - MYSQL_ROOT_PASSWORD=123456 + webapp: + image: java:8 + depends_on: + - mysql + - redis + volumes: + - ./application-prod.yml:/app/application-prod.yml + - ./build/libs/SpringBlog-0.0.1-SNAPSHOT.jar:/app/app.jar + ports: + - "9000:8080" +# command: "java -jar /app/app.jar --spring.profiles.active=prod --spring.config.location=/app/application-prod.yml" + command: "java -jar /app/app.jar --spring.profiles.active=prod" + + redis: + image: redis diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index fd7e590..1a958be 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c19d5c5..56155af 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sun Sep 06 14:44:43 CST 2015 +#Thu Jan 18 22:04:34 CST 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.6-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-3.5.1-all.zip diff --git a/gradlew b/gradlew index 91a7e26..4453cce 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh ############################################################################## ## @@ -6,12 +6,30 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,31 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -90,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -114,6 +113,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` @@ -154,11 +154,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index aec9973..e95643d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -8,14 +8,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,10 +46,9 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +59,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 160ae68..0000000 --- a/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'SpringBlog' \ No newline at end of file diff --git a/src/main/java/com/raysmond/blog/BlogApp.java b/src/main/java/com/raysmond/blog/BlogApp.java deleted file mode 100644 index 9dc5b57..0000000 --- a/src/main/java/com/raysmond/blog/BlogApp.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.raysmond.blog; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity; - -// TODO use Spring Boot for running -@SpringBootApplication -public class BlogApp { - - public static void main(String[] args) { - SpringApplication.run(BlogApp.class, args); - } -} diff --git a/src/main/java/com/raysmond/blog/CacheConfiguration.java b/src/main/java/com/raysmond/blog/CacheConfiguration.java new file mode 100644 index 0000000..7c07443 --- /dev/null +++ b/src/main/java/com/raysmond/blog/CacheConfiguration.java @@ -0,0 +1,28 @@ +package com.raysmond.blog; + +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.ehcache.EhCacheCacheManager; +import org.springframework.cache.ehcache.EhCacheManagerFactoryBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; + +@Configuration +@EnableCaching +public class CacheConfiguration { + @Bean + public EhCacheManagerFactoryBean ehCacheManagerFactory() { + EhCacheManagerFactoryBean cacheManagerFactoryBean = new EhCacheManagerFactoryBean(); + cacheManagerFactoryBean.setConfigLocation(new ClassPathResource("ehcache.xml")); + cacheManagerFactoryBean.setShared(true); + return cacheManagerFactoryBean; + } + + @Bean + public EhCacheCacheManager ehCacheCacheManager() { + EhCacheCacheManager cacheManager = new EhCacheCacheManager(); + cacheManager.setCacheManager(ehCacheManagerFactory().getObject()); + cacheManager.setTransactionAware(true); + return cacheManager; + } +} \ No newline at end of file diff --git a/src/main/java/com/raysmond/blog/Constants.java b/src/main/java/com/raysmond/blog/Constants.java new file mode 100644 index 0000000..8436e0f --- /dev/null +++ b/src/main/java/com/raysmond/blog/Constants.java @@ -0,0 +1,18 @@ +package com.raysmond.blog; + +/** + * @author: Raysmond + */ +public final class Constants { + + public static final String ENV_PRODUCTION = "prod"; + + public static final String ENV_DEVELOPMENT = "dev"; + + public static final String DEFAULT_ADMIN_EMAIL = "admin"; + + public static final String DEFAULT_ADMIN_PASSWORD = "admin"; + + public static final String ABOUT_PAGE_PERMALINK = "about"; + +} diff --git a/src/main/java/com/raysmond/blog/config/SecurityConfig.java b/src/main/java/com/raysmond/blog/SecurityConfig.java similarity index 59% rename from src/main/java/com/raysmond/blog/config/SecurityConfig.java rename to src/main/java/com/raysmond/blog/SecurityConfig.java index 913e478..95ee67e 100644 --- a/src/main/java/com/raysmond/blog/config/SecurityConfig.java +++ b/src/main/java/com/raysmond/blog/SecurityConfig.java @@ -1,63 +1,58 @@ -package com.raysmond.blog.config; +package com.raysmond.blog; import com.raysmond.blog.services.UserService; -import org.springframework.context.annotation.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.StandardPasswordEncoder; import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices; @Configuration -@EnableWebMvcSecurity -class SecurityConfig extends WebSecurityConfigurerAdapter { - - @Bean - public UserService userService() { - return new UserService(); - } +public class SecurityConfig extends WebSecurityConfigurerAdapter { + @Autowired + private UserService userService; @Bean public TokenBasedRememberMeServices rememberMeServices() { - return new TokenBasedRememberMeServices("remember-me-key", userService()); + return new TokenBasedRememberMeServices("remember-me-key", userService); } @Bean public PasswordEncoder passwordEncoder() { return new StandardPasswordEncoder(); - } + } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth - .eraseCredentials(true) - .userDetailsService(userService()) - .passwordEncoder(passwordEncoder()); + .eraseCredentials(true) + .userDetailsService(userService) + .passwordEncoder(passwordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http - .authorizeRequests() - .antMatchers("/admin","/admin/**").authenticated() + .authorizeRequests() + .antMatchers("/admin/**").authenticated() .anyRequest().permitAll() - //.antMatchers("/", "/favicon.ico", "/resources/**", "/webjars/**" , "/signup").permitAll() - // .anyRequest().authenticated() .and() - .formLogin() - .loginPage("/signin") + .formLogin() + .loginPage("/login") .permitAll() - .failureUrl("/signin?error=1") + .failureUrl("/login?error=1") .loginProcessingUrl("/authenticate") .and() - .logout() + .logout() .logoutUrl("/logout") .permitAll() - .logoutSuccessUrl("/signin?logout") + .logoutSuccessUrl("/login?logout") .and() - .rememberMe() + .rememberMe() .rememberMeServices(rememberMeServices()) .key("remember-me-key"); } diff --git a/src/main/java/com/raysmond/blog/SpringBlogApplication.java b/src/main/java/com/raysmond/blog/SpringBlogApplication.java new file mode 100644 index 0000000..a6a3892 --- /dev/null +++ b/src/main/java/com/raysmond/blog/SpringBlogApplication.java @@ -0,0 +1,18 @@ +package com.raysmond.blog; + +import com.google.common.collect.ImmutableMap; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; + + +@SpringBootApplication +//@EnableCaching +public class SpringBlogApplication { + + public static void main(String[] args) { + SpringApplication app = new SpringApplication(SpringBlogApplication.class); + app.setDefaultProperties(ImmutableMap.of("spring.profiles.default", Constants.ENV_DEVELOPMENT)); + app.run(args); + } +} diff --git a/src/main/java/com/raysmond/blog/WebConfig.java b/src/main/java/com/raysmond/blog/WebConfig.java new file mode 100644 index 0000000..ee41c1a --- /dev/null +++ b/src/main/java/com/raysmond/blog/WebConfig.java @@ -0,0 +1,87 @@ +package com.raysmond.blog; + +import com.raysmond.blog.support.web.ViewHelper; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +import javax.annotation.PostConstruct; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import static com.raysmond.blog.Constants.ENV_DEVELOPMENT; +import static com.raysmond.blog.Constants.ENV_PRODUCTION; + +import lombok.extern.slf4j.Slf4j; + +/** + * @author Raysmond . + */ +@Configuration +@Slf4j +public class WebConfig extends WebMvcConfigurerAdapter { + @Autowired + private ViewHelper viewHelper; + + @Autowired + private Environment env; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(viewObjectAddingInterceptor()); + super.addInterceptors(registry); + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + if (env.acceptsProfiles(ENV_DEVELOPMENT)) { + log.debug("Register CORS configuration"); + registry.addMapping("/api/**") + .allowedOrigins("http://localhost:8080") + .allowedMethods("*") + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(3600); + } + } + + @PostConstruct + public void registerJadeViewHelpers() { + viewHelper.setApplicationEnv(this.getApplicationEnv()); + } + + @Bean + public HandlerInterceptor viewObjectAddingInterceptor() { + return new HandlerInterceptorAdapter() { + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + viewHelper.setStartTime(System.currentTimeMillis()); + + return true; + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + ModelAndView view) { + CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); + if (token != null) { + view.addObject(token.getParameterName(), token); + } + } + }; + } + + public String getApplicationEnv() { + return this.env.acceptsProfiles(ENV_PRODUCTION) ? ENV_PRODUCTION : ENV_DEVELOPMENT; + } +} diff --git a/src/main/java/com/raysmond/blog/admin/controllers/AdminController.java b/src/main/java/com/raysmond/blog/admin/controllers/AdminController.java index 6a0741e..2f8f072 100644 --- a/src/main/java/com/raysmond/blog/admin/controllers/AdminController.java +++ b/src/main/java/com/raysmond/blog/admin/controllers/AdminController.java @@ -1,22 +1,60 @@ package com.raysmond.blog.admin.controllers; +import com.raysmond.blog.forms.SettingsForm; +import com.raysmond.blog.services.AppSetting; +import com.raysmond.blog.support.web.MessageHelper; +import com.raysmond.blog.utils.DTOUtil; + +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import javax.validation.Valid; /** - * Created by Raysmond on 9/25/15. + * @author Raysmond */ @Controller -@RequestMapping(value = "/admin") +@RequestMapping("admin") public class AdminController { - @RequestMapping(value = "") - public String index(){ - return "admin/index"; + private AppSetting appSetting; + + @Autowired + public AdminController(AppSetting appSetting) { + this.appSetting = appSetting; + } + + @GetMapping("") + public String index() { + return "admin/home/index"; } - @RequestMapping(value = "settings") - public String settings(){ - return "admin/settings"; + @GetMapping(value = "settings") + public String settings(Model model) { + SettingsForm settingsForm = DTOUtil.map(appSetting, SettingsForm.class); + + model.addAttribute("settings", settingsForm); + return "admin/home/settings"; + } + + @PostMapping(value = "settings") + public String updateSettings(@Valid SettingsForm settingsForm, Errors errors, RedirectAttributes ra) { + if (errors.hasErrors()) { + return "admin/settings"; + } else { + appSetting.setSiteName(settingsForm.getSiteName()); + appSetting.setSiteSlogan(settingsForm.getSiteSlogan()); + appSetting.setPageSize(settingsForm.getPageSize()); + + MessageHelper.addSuccessAttribute(ra, "Update settings successfully."); + + return "redirect:settings"; + } } } diff --git a/src/main/java/com/raysmond/blog/admin/controllers/PostController.java b/src/main/java/com/raysmond/blog/admin/controllers/PostController.java index 75d5daf..ee3482d 100644 --- a/src/main/java/com/raysmond/blog/admin/controllers/PostController.java +++ b/src/main/java/com/raysmond/blog/admin/controllers/PostController.java @@ -1,110 +1,124 @@ package com.raysmond.blog.admin.controllers; -import com.raysmond.blog.config.interceptors.RequestProcessingTimeInterceptor; import com.raysmond.blog.forms.PostForm; import com.raysmond.blog.models.Post; -import com.raysmond.blog.models.User; import com.raysmond.blog.models.support.PostFormat; import com.raysmond.blog.models.support.PostStatus; +import com.raysmond.blog.models.support.PostType; import com.raysmond.blog.repositories.PostRepository; import com.raysmond.blog.repositories.UserRepository; -import com.raysmond.blog.services.MarkdownService; import com.raysmond.blog.services.PostService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.raysmond.blog.utils.DTOUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.validation.BindingResult; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import javax.validation.Valid; import java.security.Principal; +import static org.springframework.web.bind.annotation.RequestMethod.*; + /** - * Created by Raysmond on 9/25/15. - * - * TODO place all view templates into sub dirs under /admin/ + * @author Raysmond */ -@Controller(value = "adminPostController") -@RequestMapping(value = "/admin/posts") +@Controller("adminPostController") +@RequestMapping("admin/posts") public class PostController { + private static final int PAGE_SIZE = 20; @Autowired - private PostRepository posts; - + private PostRepository postRepository; @Autowired private PostService postService; - @Autowired - private UserRepository users; + private UserRepository userRepository; - private static final int PAGE_SIZE = 50; + @GetMapping("") + public String index(@RequestParam(defaultValue = "0") int page, Model model) { + Page posts = postRepository.findAll(new PageRequest(page, PAGE_SIZE, Sort.Direction.DESC, "id")); - @RequestMapping(value = "") - public String index(@RequestParam(defaultValue = "0") int page, Model model){ - Page _posts = posts.findAll(new PageRequest(page, PAGE_SIZE, Sort.Direction.DESC, "id")); - model.addAttribute("totalPages", _posts.getTotalPages()); + model.addAttribute("totalPages", posts.getTotalPages()); model.addAttribute("page", page); - model.addAttribute("posts", _posts); - return "admin/posts_index"; + model.addAttribute("posts", posts); + + return "admin/posts/index"; } - @RequestMapping(value = "new") - public String newPost(Model model){ - model.addAttribute("postForm", new PostForm()); + @GetMapping("new") + public String newPost(Model model) { + PostForm postForm = DTOUtil.map(new Post(), PostForm.class); + postForm.setPostTags(""); + + model.addAttribute("postForm", postForm); model.addAttribute("postFormats", PostFormat.values()); - return "admin/posts_new"; + model.addAttribute("postTypes", PostType.values()); + model.addAttribute("postStatus", PostStatus.values()); + + return "admin/posts/new"; } @RequestMapping(value = "{postId:[0-9]+}/edit") - public String editPost(@PathVariable Long postId, Model model){ - Post post = posts.findOne(postId); + public String editPost(@PathVariable Long postId, Model model) { + Post post = postRepository.findOne(postId); + PostForm postForm = DTOUtil.map(post, PostForm.class); + + postForm.setPostTags(postService.getTagNames(post.getTags())); + model.addAttribute("post", post); - model.addAttribute("postForm", new PostForm(post)); + model.addAttribute("postForm", postForm); model.addAttribute("postFormats", PostFormat.values()); - return "admin/posts_edit"; + model.addAttribute("postTypes", PostType.values()); + model.addAttribute("postStatus", PostStatus.values()); + + return "admin/posts/edit"; } - @RequestMapping(value = "{postId:[0-9]+}/delete", method = {RequestMethod.DELETE, RequestMethod.POST}) - public String deletePost(@PathVariable Long postId){ - posts.delete(postId); + @RequestMapping(value = "{postId:[0-9]+}/delete", method = {DELETE, POST}) + public String deletePost(@PathVariable Long postId) { + postService.deletePost(postRepository.findOne(postId)); return "redirect:/admin/posts"; } - @RequestMapping(value = "", method = RequestMethod.POST) - public String create(Principal principal, @Valid PostForm postForm, BindingResult bindingResult, Model model){ - if (bindingResult.hasErrors()) { + @RequestMapping(value = "", method = POST) + public String create(Principal principal, @Valid PostForm postForm, Errors errors, Model model) { + if (errors.hasErrors()) { model.addAttribute("postFormats", PostFormat.values()); - return "admin/posts_new"; + model.addAttribute("postStatus", PostStatus.values()); + + return "admin/posts/new"; } else { - User user = users.findByEmail(principal.getName()); - Post post = postForm.toPost(); + Post post = DTOUtil.map(postForm, Post.class); + post.setUser(userRepository.findByEmail(principal.getName())); + post.setTags(postService.parseTagNames(postForm.getPostTags())); - postService.createPost(user, post.getTitle(), post.getContent(), post.getPostFormat(), PostStatus.PUBLISHED); + postService.createPost(post); return "redirect:/admin/posts"; } } - @RequestMapping(value = "{postId:[0-9]+}", method = {RequestMethod.PUT, RequestMethod.POST}) - public String update(@PathVariable Long postId, @Valid PostForm postForm, BindingResult bindingResult, Model model){ - if (bindingResult.hasErrors()){ + @RequestMapping(value = "{postId:[0-9]+}", method = {PUT, POST}) + public String update(@PathVariable Long postId, @Valid PostForm postForm, Errors errors, Model model) { + if (errors.hasErrors()) { model.addAttribute("postFormats", PostFormat.values()); + model.addAttribute("postStatus", PostStatus.values()); + return "admin/posts_edit"; } else { - Post post = posts.findOne(postId); - post.setTitle(postForm.getTitle()); - post.setContent(postForm.getContent()); - post.setPostFormat(postForm.getPostFormat()); + Post post = postRepository.findOne(postId); + DTOUtil.mapTo(postForm, post); + post.setTags(postService.parseTagNames(postForm.getPostTags())); postService.updatePost(post); + return "redirect:/admin/posts"; } } diff --git a/src/main/java/com/raysmond/blog/admin/controllers/UserController.java b/src/main/java/com/raysmond/blog/admin/controllers/UserController.java new file mode 100644 index 0000000..c7b6c48 --- /dev/null +++ b/src/main/java/com/raysmond/blog/admin/controllers/UserController.java @@ -0,0 +1,70 @@ +package com.raysmond.blog.admin.controllers; + +import com.raysmond.blog.error.NotFoundException; +import com.raysmond.blog.forms.UserForm; +import com.raysmond.blog.models.User; +import com.raysmond.blog.repositories.UserRepository; +import com.raysmond.blog.services.UserService; +import com.raysmond.blog.support.web.MessageHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.util.Assert; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import javax.validation.Valid; + +import static org.springframework.web.bind.annotation.RequestMethod.POST; + +/** + * @author Raysmond + */ +@Controller("adminUserController") +@RequestMapping("admin/users") +public class UserController { + + private UserService userService; + private UserRepository userRepository; + + @Autowired + public UserController(UserService userService, UserRepository userRepository) { + this.userService = userService; + this.userRepository = userRepository; + } + + @RequestMapping("profile") + public String profile(Model model) { + model.addAttribute("user", userService.currentUser()); + + return "admin/users/profile"; + } + + @RequestMapping(value = "{userId:[0-9]+}", method = POST) + public String update(@PathVariable Long userId, @Valid UserForm userForm, Errors errors, RedirectAttributes ra) { + User user = userRepository.findOne(userId); + + if (null == user) { + throw new NotFoundException("User " + userId + " is not found."); + } + + if (errors.hasErrors()) { + // do something + + return "admin/users/profile"; + } + + if (!userForm.getNewPassword().isEmpty()) { + + if (!userService.changePassword(user, userForm.getPassword(), userForm.getNewPassword())) + MessageHelper.addErrorAttribute(ra, "Change password failed."); + else + MessageHelper.addSuccessAttribute(ra, "Change password successfully."); + + } + + return "redirect:profile"; + } +} diff --git a/src/main/java/com/raysmond/blog/config/ApplicationConfig.java b/src/main/java/com/raysmond/blog/config/ApplicationConfig.java deleted file mode 100644 index fc39cc1..0000000 --- a/src/main/java/com/raysmond/blog/config/ApplicationConfig.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.raysmond.blog.config; - -import static org.springframework.context.annotation.ComponentScan.Filter; - -import com.raysmond.blog.BlogApp; -import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.ClassPathResource; -import org.springframework.stereotype.Controller; - - -@Configuration -@ComponentScan(basePackageClasses = BlogApp.class, excludeFilters = @Filter({Controller.class, Configuration.class})) -class ApplicationConfig { - @Bean - public static PropertyPlaceholderConfigurer propertyPlaceholderConfigurer() { - PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer(); - ppc.setLocation(new ClassPathResource("/persistence.properties")); - return ppc; - } - -} diff --git a/src/main/java/com/raysmond/blog/config/Config.java b/src/main/java/com/raysmond/blog/config/Config.java deleted file mode 100644 index f1e7976..0000000 --- a/src/main/java/com/raysmond/blog/config/Config.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.raysmond.blog.config; - -/** - * @author: Raysmond - * - * A constant class - */ -public final class Config { - - public static final String MESSAGE_SOURCE = "/i18n/messages"; - public static final String VIEWS = "/views/"; - public static final String RESOURCES_LOCATION = "/resources/"; - public static final String RESOURCES_HANDLER = RESOURCES_LOCATION + "**"; - -} diff --git a/src/main/java/com/raysmond/blog/config/JpaConfig.java b/src/main/java/com/raysmond/blog/config/JpaConfig.java deleted file mode 100644 index 1212ac4..0000000 --- a/src/main/java/com/raysmond/blog/config/JpaConfig.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.raysmond.blog.config; - -import java.util.Properties; - -import javax.sql.DataSource; - -import com.raysmond.blog.BlogApp; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.jpa.repository.config.EnableJpaRepositories; -import org.springframework.orm.jpa.JpaTransactionManager; -import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; -import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; -import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.annotation.EnableTransactionManagement; -import org.springframework.transaction.annotation.TransactionManagementConfigurer; - -@Configuration -@EnableTransactionManagement -@EnableJpaRepositories(basePackageClasses = BlogApp.class) -class JpaConfig implements TransactionManagementConfigurer { - - @Value("${dataSource.driverClassName}") - private String driver; - @Value("${dataSource.url}") - private String url; - @Value("${dataSource.username}") - private String username; - @Value("${dataSource.password}") - private String password; - @Value("${hibernate.dialect}") - private String dialect; - @Value("${hibernate.hbm2ddl.auto}") - private String hbm2ddlAuto; - - @Bean - public DataSource configureDataSource() { - HikariConfig config = new HikariConfig(); - config.setDriverClassName(driver); - config.setJdbcUrl(url); - config.setUsername(username); - config.setPassword(password); - - config.addDataSourceProperty("useUnicode", "true"); - config.addDataSourceProperty("characterEncoding", "utf8"); - config.addDataSourceProperty("cachePrepStmts", "true"); - config.addDataSourceProperty("prepStmtCacheSize", "250"); - config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); - config.addDataSourceProperty("useServerPrepStmts", "true"); - - return new HikariDataSource(config); - } - - @Bean - public LocalContainerEntityManagerFactoryBean entityManagerFactory() { - LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); - entityManagerFactoryBean.setDataSource(configureDataSource()); - entityManagerFactoryBean.setPackagesToScan("com.raysmond.blog"); - entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); - - Properties jpaProperties = new Properties(); - jpaProperties.put(org.hibernate.cfg.Environment.DIALECT, dialect); - jpaProperties.put(org.hibernate.cfg.Environment.HBM2DDL_AUTO, hbm2ddlAuto); - entityManagerFactoryBean.setJpaProperties(jpaProperties); - - return entityManagerFactoryBean; - } - - @Bean(name = "transactionManager") - public PlatformTransactionManager annotationDrivenTransactionManager() { - return new JpaTransactionManager(); - } -} diff --git a/src/main/java/com/raysmond/blog/config/WebAppInitializer.java b/src/main/java/com/raysmond/blog/config/WebAppInitializer.java deleted file mode 100644 index c5fdf78..0000000 --- a/src/main/java/com/raysmond/blog/config/WebAppInitializer.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.raysmond.blog.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.web.filter.CharacterEncodingFilter; -import org.springframework.web.filter.DelegatingFilterProxy; -import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; - -import javax.servlet.*; - -public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { - - @Override - protected String[] getServletMappings() { - return new String[]{"/"}; - } - - @Override - protected Class[] getRootConfigClasses() { - return new Class[] {ApplicationConfig.class, JpaConfig.class, SecurityConfig.class}; - } - - @Override - protected Class[] getServletConfigClasses() { - return new Class[] {WebMvcConfig.class}; - } - - @Override - protected Filter[] getServletFilters() { - CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter(); - characterEncodingFilter.setEncoding("UTF-8"); - characterEncodingFilter.setForceEncoding(true); - - DelegatingFilterProxy securityFilterChain = new DelegatingFilterProxy("springSecurityFilterChain"); - - return new Filter[] {characterEncodingFilter, securityFilterChain}; - } - - @Override - protected void customizeRegistration(ServletRegistration.Dynamic registration) { - registration.setInitParameter("defaultHtmlEscape", "true"); - registration.setInitParameter("spring.profiles.active", "default"); - } -} \ No newline at end of file diff --git a/src/main/java/com/raysmond/blog/config/WebMvcConfig.java b/src/main/java/com/raysmond/blog/config/WebMvcConfig.java deleted file mode 100644 index bf3728f..0000000 --- a/src/main/java/com/raysmond/blog/config/WebMvcConfig.java +++ /dev/null @@ -1,207 +0,0 @@ -package com.raysmond.blog.config; - -import static org.springframework.context.annotation.ComponentScan.Filter; - -import com.raysmond.blog.BlogApp; -import com.raysmond.blog.support.web.ViewHelper; -import de.neuland.jade4j.JadeConfiguration; -import de.neuland.jade4j.spring.template.SpringTemplateLoader; -import de.neuland.jade4j.spring.view.JadeViewResolver; -import com.raysmond.blog.config.interceptors.RequestProcessingTimeInterceptor; -import com.raysmond.blog.config.resolvers.JsonViewResolver; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.MessageSource; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.support.ReloadableResourceBundleMessageSource; -import org.springframework.http.MediaType; -import org.springframework.security.web.csrf.CsrfToken; -import org.springframework.stereotype.Controller; -import org.springframework.web.accept.ContentNegotiationManager; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.validation.Validator; -import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; -import org.springframework.web.servlet.HandlerInterceptor; -import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.ViewResolver; -import org.springframework.web.servlet.config.annotation.*; -import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; -import org.springframework.web.servlet.view.ContentNegotiatingViewResolver; -import org.thymeleaf.extras.springsecurity3.dialect.SpringSecurityDialect; -import org.thymeleaf.spring4.SpringTemplateEngine; -import org.thymeleaf.spring4.view.ThymeleafViewResolver; -import org.thymeleaf.templateresolver.ServletContextTemplateResolver; -import org.thymeleaf.templateresolver.TemplateResolver; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.util.ArrayList; -import java.util.List; - -@Configuration -@ComponentScan(basePackageClasses = BlogApp.class, includeFilters = @Filter(Controller.class), useDefaultFilters = false) -@EnableWebMvc -class WebMvcConfig extends WebMvcConfigurerAdapter { - - @Bean(name = "messageSource") - public MessageSource messageSource() { - ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); - messageSource.setBasename(Config.MESSAGE_SOURCE); - messageSource.setCacheSeconds(5); - return messageSource; - } - - @Override - public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { - configurer.ignoreAcceptHeader(true) - .defaultContentType(MediaType.TEXT_HTML); - } - - /* - * Configure ContentNegotiatingViewResolver - */ - @Bean - public ViewResolver contentNegotiatingViewResolver(ContentNegotiationManager manager) { - ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver(); - resolver.setContentNegotiationManager(manager); - - // Define all possible view resolvers - List resolvers = new ArrayList(); - - resolvers.add(jadeViewResolver()); - resolvers.add(htmlViewResolver()); - resolvers.add(jsonViewResolver()); - - resolver.setViewResolvers(resolvers); - return resolver; - } - - @Bean - public ViewResolver jsonViewResolver() { - return new JsonViewResolver(); - } - - @Bean - public TemplateResolver htmlTemplateResolver() { - TemplateResolver templateResolver = new ServletContextTemplateResolver(); - templateResolver.setPrefix(Config.VIEWS); - templateResolver.setSuffix(".html"); - templateResolver.setTemplateMode("HTML"); - templateResolver.setCacheable(false); - return templateResolver; - } - - @Bean - public SpringTemplateEngine htmlTemplateEngine() { - SpringTemplateEngine templateEngine = new SpringTemplateEngine(); - templateEngine.setTemplateResolver(htmlTemplateResolver()); - templateEngine.addDialect(new SpringSecurityDialect()); - return templateEngine; - } - - @Bean - public ThymeleafViewResolver htmlViewResolver() { - ThymeleafViewResolver thymeleafViewResolver = new ThymeleafViewResolver(); - thymeleafViewResolver.setTemplateEngine(htmlTemplateEngine()); - thymeleafViewResolver.setCharacterEncoding("UTF-8"); - return thymeleafViewResolver; - } - - @Bean - public SpringTemplateLoader jadeTemplateLoader() { - SpringTemplateLoader templateLoader = new SpringTemplateLoader(); - templateLoader.setBasePath(Config.VIEWS); - templateLoader.setEncoding("UTF-8"); - templateLoader.setSuffix(".jade"); - return templateLoader; - } - - - @Bean - public JadeConfiguration jadeConfiguration() { - JadeConfiguration configuration = new JadeConfiguration(); - configuration.setCaching(false); - configuration.setTemplateLoader(jadeTemplateLoader()); - return configuration; - } - - @Bean - public ViewResolver jadeViewResolver() { - JadeViewResolver resolver = new JadeViewResolver(); - resolver.setConfiguration(jadeConfiguration()); - return resolver; - } - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(viewObjectAddingInterceptor()); - registry.addInterceptor(requestProcessTimeInterceptor()); - } - - @Bean - public HandlerInterceptor requestProcessTimeInterceptor(){ - return new RequestProcessingTimeInterceptor(); - } - - @Autowired - public ViewHelper viewHelper; - - @Bean - public HandlerInterceptor viewObjectAddingInterceptor() { - return new HandlerInterceptorAdapter() { - @Override - public boolean preHandle( - HttpServletRequest request, - HttpServletResponse response, - Object handler) throws Exception { - - viewHelper.setStartTime(System.currentTimeMillis()); - return true; - } - @Override - public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView view) { - CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); - if (token != null) { - // add csrf token - view.addObject(token.getParameterName(), token); - - // add base path of application - view.addObject("basePath", request.getContextPath()); - - view.addObject("viewHelper", viewHelper); - } - } - }; - } - - @Override - public Validator getValidator() { - LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean(); - validator.setValidationMessageSource(messageSource()); - return validator; - } - - @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { - registry - .addResourceHandler(Config.RESOURCES_HANDLER) - .addResourceLocations(Config.RESOURCES_LOCATION); - } - - @Override - public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { - configurer.enable(); - } - - /** - * Handles favicon.ico requests assuring no 404 Not Found error is returned. - */ - @Controller - static class FaviconController { - @RequestMapping("favicon.ico") - String favicon() { - return "forward:/resources/images/favicon.ico"; - } - } -} diff --git a/src/main/java/com/raysmond/blog/config/interceptors/RequestProcessingTimeInterceptor.java b/src/main/java/com/raysmond/blog/config/interceptors/RequestProcessingTimeInterceptor.java deleted file mode 100644 index e36c0c5..0000000 --- a/src/main/java/com/raysmond/blog/config/interceptors/RequestProcessingTimeInterceptor.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.raysmond.blog.config.interceptors; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import com.raysmond.blog.support.web.ViewHelper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; - -import java.util.Date; - -/** - * Request processing time logging - * - * @author: Raysmond - */ -public class RequestProcessingTimeInterceptor extends HandlerInterceptorAdapter { - - private static final Logger logger = LoggerFactory - .getLogger(RequestProcessingTimeInterceptor.class); - - @Override - public boolean preHandle( - HttpServletRequest request, - HttpServletResponse response, - Object handler) throws Exception { - - long startTime = System.currentTimeMillis(); - String url = request.getRequestURL().toString(); - - logger.info("Start Request:" + url + ": Time=" + new Date()); - request.setAttribute("startTime", startTime); - - return true; - } - - @Override - public void postHandle( - HttpServletRequest request, - HttpServletResponse response, - Object handler, - ModelAndView modelAndView) throws Exception { - - } - - @Override - public void afterCompletion( - HttpServletRequest request, - HttpServletResponse response, - Object handler, Exception ex) throws Exception { - - long startTime = (Long) request.getAttribute("startTime"); - long time = System.currentTimeMillis() - startTime; - - String url = request.getRequestURL().toString(); - - logger.info("Complete Request:" + url + ": Time=" + new Date()); - logger.info("Time Taken=" + time+"ms"); - } - -} \ No newline at end of file diff --git a/src/main/java/com/raysmond/blog/config/resolvers/JsonViewResolver.java b/src/main/java/com/raysmond/blog/config/resolvers/JsonViewResolver.java deleted file mode 100644 index 217a7e5..0000000 --- a/src/main/java/com/raysmond/blog/config/resolvers/JsonViewResolver.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.raysmond.blog.config.resolvers; - -import java.util.Locale; - -import org.springframework.web.servlet.View; -import org.springframework.web.servlet.ViewResolver; -import org.springframework.web.servlet.view.json.MappingJackson2JsonView; - -/** - * JSON view resolver using Jackson library - * - * @author: Raysmond - */ -public class JsonViewResolver implements ViewResolver { - - @Override - public View resolveViewName(String viewName, Locale locale) throws Exception { - MappingJackson2JsonView view = new MappingJackson2JsonView(); - view.setPrettyPrint(true); - return view; - } - -} \ No newline at end of file diff --git a/src/main/java/com/raysmond/blog/config/views/JadeView.java b/src/main/java/com/raysmond/blog/config/views/JadeView.java deleted file mode 100644 index c4baac4..0000000 --- a/src/main/java/com/raysmond/blog/config/views/JadeView.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.raysmond.blog.config.views; - -import de.neuland.jade4j.JadeConfiguration; -import de.neuland.jade4j.exceptions.JadeCompilerException; -import de.neuland.jade4j.exceptions.JadeException; -import de.neuland.jade4j.template.JadeTemplate; -import org.springframework.web.servlet.view.AbstractTemplateView; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; -import java.io.Writer; -import java.util.*; - -/** - * Created by Raysmond on 9/6/15. - */ -public class JadeView extends AbstractTemplateView { - - private String encoding; - private JadeConfiguration configuration; - private boolean renderExceptions = false; - private String contentType; - - @Override - protected void renderMergedTemplateModel(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { - doRender(model, response); - } - - private void doRender(Map model, HttpServletResponse response) throws IOException { - logger.trace("Rendering Jade template [" + getUrl() + "] in JadeView '" + getBeanName() + "'"); - - if (contentType != null) { - response.setContentType(contentType); - } - - PrintWriter responseWriter = response.getWriter(); - - if (renderExceptions) { - Writer writer = new StringWriter(); - try { - configuration.renderTemplate(getTemplate(), model, writer); - responseWriter.write(writer.toString()); - } catch (JadeException e) { - String htmlString = e.toHtmlString(writer.toString()); - responseWriter.write(htmlString); - logger.error("failed to render template [" + getUrl() + "]", e); - } catch (IOException e) { - responseWriter.write("
could not find template: " + getUrl() + "\n");
-                e.printStackTrace(responseWriter);
-                responseWriter.write("
"); - logger.error("could not find template", e); - } - } else { - try { - configuration.renderTemplate(getTemplate(), model, responseWriter); - } catch (Throwable e) { - logger.error("failed to render template [" + getUrl() + "]\n", e); - } - } - } - - protected JadeTemplate getTemplate() throws IOException, JadeException { - return configuration.getTemplate(getUrl()); - } - - @Override - public boolean checkResource(Locale locale) throws Exception { - return configuration.templateExists(getUrl()); - } - - protected void processTemplate(JadeTemplate template, Map model, HttpServletResponse response) throws IOException { - try { - configuration.renderTemplate(template, model, response.getWriter()); - } catch (JadeCompilerException e) { - e.printStackTrace(); - } - } - - /* Configuration Handling */ - public JadeConfiguration getConfiguration() { - return configuration; - } - - public void setConfiguration(JadeConfiguration configuration) { - this.configuration = configuration; - } - - public String getEncoding() { - return encoding; - } - - public void setEncoding(String encoding) { - this.encoding = encoding; - } - - public void setRenderExceptions(boolean renderExceptions) { - this.renderExceptions = renderExceptions; - } - - public String getContentType() { - return contentType; - } - - public void setContentType(String contentType) { - this.contentType = contentType; - } - -} diff --git a/src/main/java/com/raysmond/blog/controllers/AccountController.java b/src/main/java/com/raysmond/blog/controllers/AccountController.java deleted file mode 100644 index ae2e274..0000000 --- a/src/main/java/com/raysmond/blog/controllers/AccountController.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.raysmond.blog.controllers; - -import java.security.Principal; - -import com.raysmond.blog.models.User; -import com.raysmond.blog.repositories.UserRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.security.access.annotation.Secured; -import org.springframework.stereotype.Controller; -import org.springframework.util.Assert; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.bind.annotation.ResponseStatus; - -@Controller -@Secured("ROLE_USER") -class AccountController { - - private UserRepository userRepository; - - @Autowired - public AccountController(UserRepository userRepository) { - this.userRepository = userRepository; - } - - @RequestMapping(value = "account/current", method = RequestMethod.GET) - @ResponseStatus(value = HttpStatus.OK) - @ResponseBody - public User accounts(Principal principal) { - Assert.notNull(principal); - return userRepository.findByEmail(principal.getName()); - } -} diff --git a/src/main/java/com/raysmond/blog/controllers/HomeController.java b/src/main/java/com/raysmond/blog/controllers/HomeController.java index 886ada3..eacc8a2 100644 --- a/src/main/java/com/raysmond/blog/controllers/HomeController.java +++ b/src/main/java/com/raysmond/blog/controllers/HomeController.java @@ -1,65 +1,32 @@ package com.raysmond.blog.controllers; -import java.security.Principal; - import com.raysmond.blog.models.Post; -import com.raysmond.blog.models.support.PostFormat; -import com.raysmond.blog.models.support.PostStatus; -import com.raysmond.blog.models.support.PostType; -import com.raysmond.blog.repositories.PostRepository; -import com.raysmond.blog.repositories.UserRepository; +import com.raysmond.blog.services.AppSetting; import com.raysmond.blog.services.PostService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; @Controller public class HomeController { - @Autowired - private PostRepository posts; - - @Autowired - private PostService postService; - - @Autowired - private UserRepository users; - - private static final int PAGE_SIZE = 5; - - @RequestMapping(value = "/") - public String index(@RequestParam(defaultValue = "1") int page, Model model) { - if (page<=0){ - page = 1; - } - - Page _posts = posts.findAllByPostType( PostType.POST, - new PageRequest(page - 1, PAGE_SIZE, Sort.Direction.DESC, "id")); - - model.addAttribute("totalPages", _posts.getTotalPages()); - model.addAttribute("posts", _posts); - model.addAttribute("page", page); - return "home/index"; - } + @Autowired + private PostService postService; - @RequestMapping(value = "/about") - public String about(Model model){ - Post post = posts.findByTitleAndPostType("About", PostType.PAGE); - if(post == null){ - post = postService.createPage( - users.findByEmail("admin@raysmond.com"), - "About", - "about me...", - PostFormat.MARKDOWN, - PostStatus.PUBLISHED); + @Autowired + private AppSetting appSetting; - } - model.addAttribute("about", post); - return "home/about"; - } + @GetMapping(value = "") + public String index(@RequestParam(defaultValue = "1") int page, Model model) { + page = page < 1 ? 0 : page - 1; + Page posts = postService.getAllPublishedPostsByPage(page, appSetting.getPageSize()); + model.addAttribute("totalPages", posts.getTotalPages()); + model.addAttribute("posts", posts); + model.addAttribute("page", page + 1); + return "home/home"; + } } diff --git a/src/main/java/com/raysmond/blog/controllers/PostController.java b/src/main/java/com/raysmond/blog/controllers/PostController.java index 2d19ae0..97d31c6 100644 --- a/src/main/java/com/raysmond/blog/controllers/PostController.java +++ b/src/main/java/com/raysmond/blog/controllers/PostController.java @@ -1,37 +1,78 @@ package com.raysmond.blog.controllers; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.raysmond.blog.error.NotFoundException; import com.raysmond.blog.models.Post; import com.raysmond.blog.models.support.PostType; -import com.raysmond.blog.repositories.PostRepository; +import com.raysmond.blog.services.PostService; + import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; +import static org.springframework.web.bind.annotation.RequestMethod.GET; + +import java.util.List; +import java.util.Map; + /** - * Created by Raysmond on 9/26/15. + * @author Raysmond */ @Controller -@RequestMapping("posts") public class PostController { @Autowired - private PostRepository posts; - - private static final int MAX_SIZE = 100; + private PostService postService; - @RequestMapping(value = "archive") - public String archive(Model model){ - model.addAttribute("posts", posts.findAllByPostType(PostType.POST, new PageRequest(0, MAX_SIZE, Sort.Direction.DESC, "id"))); + @GetMapping(value = "posts/archive") + public String archive(Model model) { + Map> posts = Maps.newHashMap(); + postService.getArchivePosts().forEach(post -> { + if (!posts.containsKey(post.getCreatedAt().getYear())) { + posts.put(post.getCreatedAt().getYear(), Lists.newArrayList()); + } + posts.get(post.getCreatedAt().getYear()).add(post); + }); + model.addAttribute("posts", posts); return "posts/archive"; } - @RequestMapping(value = "{postId:[0-9]+}") - public String show(@PathVariable Long postId, Model model){ - Post post = posts.findOne(postId); + @GetMapping(value = "posts/{permalink}") + public String show(@PathVariable String permalink, Model model) { + return showPost(permalink, model, PostType.POST); + } + + @GetMapping(value = "{permalink}") + public String page(@PathVariable String permalink, Model model) { + return showPost(permalink, model, PostType.PAGE); + } + + private String showPost(String permalink, Model model, PostType postType) { + Post post; + + try { + post = postService.getPublishedPostByPermalink(permalink); + } catch (NotFoundException ex) { + if (permalink.matches("\\d+") && postType.equals(PostType.POST)) { + post = postService.getPost(Long.valueOf(permalink)); + } else { + throw new NotFoundException(); + } + } + + if (!post.getPostType().equals(postType)) { + throw new NotFoundException(); + } + + postService.incrementViews(post.getId()); + + model.addAttribute("postType", postType.name()); model.addAttribute("post", post); - return "posts/show"; + model.addAttribute("tags", postService.getPostTags(post)); + return "posts/post"; } + } diff --git a/src/main/java/com/raysmond/blog/controllers/TagController.java b/src/main/java/com/raysmond/blog/controllers/TagController.java new file mode 100644 index 0000000..eb13eda --- /dev/null +++ b/src/main/java/com/raysmond/blog/controllers/TagController.java @@ -0,0 +1,58 @@ +package com.raysmond.blog.controllers; + +import com.raysmond.blog.error.NotFoundException; +import com.raysmond.blog.models.Post; +import com.raysmond.blog.models.Tag; +import com.raysmond.blog.services.AppSetting; +import com.raysmond.blog.services.PostService; +import com.raysmond.blog.services.TagService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import static org.springframework.web.bind.annotation.RequestMethod.GET; + +/** + * @author Raysmond + */ +@Controller +@RequestMapping("tags") +public class TagController { + @Autowired + private TagService tagService; + + @Autowired + private PostService postService; + + @Autowired + private AppSetting appSetting; + + @RequestMapping(value = "", method = GET) + public String index(Model model) { + model.addAttribute("tags", postService.countPostsByTags()); + return "tags/index"; + } + + @RequestMapping(value = "{tagName}", method = GET) + public String showTag(@PathVariable String tagName, @RequestParam(defaultValue = "1") int page, Model model) { + Tag tag = tagService.getTag(tagName); + + if (tag == null) { + throw new NotFoundException("Tag " + tagName + " is not found."); + } + + page = page < 1 ? 0 : page - 1; + Page posts = postService.findPostsByTag(tagName, page, appSetting.getPageSize()); + + model.addAttribute("tag", tag); + model.addAttribute("posts", posts); + model.addAttribute("page", page + 1); + model.addAttribute("totalPages", posts.getTotalPages()); + + return "tags/show"; + } +} diff --git a/src/main/java/com/raysmond/blog/controllers/UserController.java b/src/main/java/com/raysmond/blog/controllers/UserController.java index f9d4506..4675a45 100644 --- a/src/main/java/com/raysmond/blog/controllers/UserController.java +++ b/src/main/java/com/raysmond/blog/controllers/UserController.java @@ -2,15 +2,20 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import java.security.Principal; /** - * Created by Raysmond on 9/25/15. + * User pages + * + * @author Raysmond */ @Controller public class UserController { - @RequestMapping(value = "signin") - public String signin() { - return "users/signin"; + @RequestMapping("login") + public String signin(Principal principal, RedirectAttributes ra) { + return principal == null ? "users/login" : "redirect:/"; } } \ No newline at end of file diff --git a/src/main/java/com/raysmond/blog/error/CustomErrorController.java b/src/main/java/com/raysmond/blog/error/CustomErrorController.java deleted file mode 100644 index dac28a7..0000000 --- a/src/main/java/com/raysmond/blog/error/CustomErrorController.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.raysmond.blog.error; - -import java.text.MessageFormat; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.RequestMapping; - -import com.google.common.base.Throwables; - -@Controller -class CustomErrorController { - - /** - * Display an error page, as defined in web.xml custom-error element. - */ - @RequestMapping("generalError") - public String generalError(HttpServletRequest request, HttpServletResponse response, Model model) { - // retrieve some useful information from the request - Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code"); - Throwable throwable = (Throwable) request.getAttribute("javax.servlet.error.exception"); - // String servletName = (String) request.getAttribute("javax.servlet.error.servlet_name"); - String exceptionMessage = getExceptionMessage(throwable, statusCode); - - String requestUri = (String) request.getAttribute("javax.servlet.error.request_uri"); - if (requestUri == null) { - requestUri = "Unknown"; - } - - String message = MessageFormat.format("{0} returned for {1} with message {2}", - statusCode, requestUri, exceptionMessage - ); - - model.addAttribute("errorMessage", message); - return "error/general"; - } - - private String getExceptionMessage(Throwable throwable, Integer statusCode) { - if (throwable != null) { - return Throwables.getRootCause(throwable).getMessage(); - } - HttpStatus httpStatus = HttpStatus.valueOf(statusCode); - return httpStatus.getReasonPhrase(); - } -} diff --git a/src/main/java/com/raysmond/blog/error/ExceptionHandler.java b/src/main/java/com/raysmond/blog/error/ExceptionHandler.java deleted file mode 100644 index 9995bfe..0000000 --- a/src/main/java/com/raysmond/blog/error/ExceptionHandler.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.raysmond.blog.error; - -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.context.request.WebRequest; -import org.springframework.web.servlet.ModelAndView; - -import com.google.common.base.Throwables; - -/** - * General error handler for the application. - */ -@ControllerAdvice -class ExceptionHandler { - - /** - * Handle exceptions thrown by handlers. - */ - @org.springframework.web.bind.annotation.ExceptionHandler(value = Exception.class) - public ModelAndView exception(Exception exception, WebRequest request) { - ModelAndView modelAndView = new ModelAndView("error/general"); - modelAndView.addObject("errorMessage", Throwables.getRootCause(exception)); - return modelAndView; - } -} \ No newline at end of file diff --git a/src/main/java/com/raysmond/blog/error/ExceptionHandlerController.java b/src/main/java/com/raysmond/blog/error/ExceptionHandlerController.java new file mode 100644 index 0000000..da06617 --- /dev/null +++ b/src/main/java/com/raysmond/blog/error/ExceptionHandlerController.java @@ -0,0 +1,52 @@ +package com.raysmond.blog.error; + +import com.google.common.base.Throwables; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; + +/** + * General error handler for the application. + */ +@ControllerAdvice +@Slf4j +class ExceptionHandlerController { + @ResponseStatus(HttpStatus.NOT_FOUND) + @ExceptionHandler(NotFoundException.class) + public ModelAndView notFound(HttpServletRequest request, NotFoundException exception) { + String uri = request.getRequestURI(); + + log.error("Request page: {} raised NotFoundException {}", uri, exception); + + ModelAndView model = new ModelAndView("error/general"); + model.addObject("status", HttpStatus.NOT_FOUND.value()); + model.addObject("error", HttpStatus.NOT_FOUND.getReasonPhrase()); + model.addObject("path", uri); + model.addObject("customMessage", exception.getMessage()); + + return model; + } + + /** + * Handle all exceptions + */ +// @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE) + @ExceptionHandler(Exception.class) + public ModelAndView exception(HttpServletRequest request, Exception exception) { + String uri = request.getRequestURI(); + log.error("Request page: {} raised exception {}", uri, exception); + + ModelAndView model = new ModelAndView("error/general"); + model.addObject("error", Throwables.getRootCause(exception).getMessage()); + model.addObject("status", Throwables.getRootCause(exception).getCause()); + model.addObject("path", uri); + model.addObject("customMessage", exception.getMessage()); + + return model; + } +} \ No newline at end of file diff --git a/src/main/java/com/raysmond/blog/error/NotFoundException.java b/src/main/java/com/raysmond/blog/error/NotFoundException.java new file mode 100644 index 0000000..1531242 --- /dev/null +++ b/src/main/java/com/raysmond/blog/error/NotFoundException.java @@ -0,0 +1,25 @@ +package com.raysmond.blog.error; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +/** + * @author Raysmond + */ +@ResponseStatus(value = HttpStatus.NOT_FOUND) +public final class NotFoundException extends RuntimeException { + private String message; + + public NotFoundException() { + + } + + public NotFoundException(String message) { + this.message = message; + } + + @Override + public String getMessage() { + return message; + } +} \ No newline at end of file diff --git a/src/main/java/com/raysmond/blog/forms/PostForm.java b/src/main/java/com/raysmond/blog/forms/PostForm.java index 386a6c9..fca6ed0 100644 --- a/src/main/java/com/raysmond/blog/forms/PostForm.java +++ b/src/main/java/com/raysmond/blog/forms/PostForm.java @@ -1,13 +1,19 @@ package com.raysmond.blog.forms; -import com.raysmond.blog.models.Post; import com.raysmond.blog.models.support.PostFormat; +import com.raysmond.blog.models.support.PostStatus; +import com.raysmond.blog.models.support.PostType; + +import lombok.Data; + import org.hibernate.validator.constraints.NotEmpty; + import javax.validation.constraints.NotNull; /** - * Created by Raysmond on 9/25/15. + * @author Raysmond */ +@Data public class PostForm { @NotEmpty private String title; @@ -15,48 +21,21 @@ public class PostForm { @NotEmpty private String content; + private String summary; + @NotNull private PostFormat postFormat; - public PostForm(){ - - } - - public PostForm(Post post){ - title = post.getTitle(); - content = post.getContent(); - postFormat = post.getPostFormat(); - } - - public Post toPost(){ - Post post = new Post(); - post.setTitle(title); - post.setContent(content); - post.setPostFormat(postFormat); - return post; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } + @NotNull + private PostStatus postStatus; - public String getContent() { - return content; - } + @NotNull + private String permalink; - public void setContent(String content) { - this.content = content; - } + @NotNull + private String postTags; - public PostFormat getPostFormat() { - return postFormat; - } + @NotNull + private PostType postType; - public void setPostFormat(PostFormat postFormat) { - this.postFormat = postFormat; - } } diff --git a/src/main/java/com/raysmond/blog/forms/SettingsForm.java b/src/main/java/com/raysmond/blog/forms/SettingsForm.java new file mode 100644 index 0000000..67ce9f3 --- /dev/null +++ b/src/main/java/com/raysmond/blog/forms/SettingsForm.java @@ -0,0 +1,28 @@ +package com.raysmond.blog.forms; + +import lombok.Data; + +import org.hibernate.validator.constraints.NotEmpty; + +import javax.validation.constraints.NotNull; + +/** + * @author Raysmond + */ +@Data +public class SettingsForm { + @NotEmpty + @NotNull + private String siteName; + + @NotNull + private String siteSlogan; + + @NotNull + private Integer pageSize; + + private String intro; + + private String pictureUrl; + +} diff --git a/src/main/java/com/raysmond/blog/forms/UserForm.java b/src/main/java/com/raysmond/blog/forms/UserForm.java new file mode 100644 index 0000000..fe4ebc2 --- /dev/null +++ b/src/main/java/com/raysmond/blog/forms/UserForm.java @@ -0,0 +1,17 @@ +package com.raysmond.blog.forms; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * @author Raysmond + */ +@Data +public class UserForm { + @NotNull + private String password; + + @NotNull + private String newPassword; +} diff --git a/src/main/java/com/raysmond/blog/models/BaseModel.java b/src/main/java/com/raysmond/blog/models/BaseModel.java index 796169a..8f262d0 100644 --- a/src/main/java/com/raysmond/blog/models/BaseModel.java +++ b/src/main/java/com/raysmond/blog/models/BaseModel.java @@ -1,28 +1,40 @@ package com.raysmond.blog.models; -import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import javax.persistence.*; + import java.io.Serializable; +import java.time.ZonedDateTime; +import java.util.Date; /** - * Created by Raysmond on 9/3/15. + * An abstract base model class for entities + * + * @author Raysmond */ @MappedSuperclass public abstract class BaseModel implements Comparable, Serializable { @Id - @GeneratedValue(strategy = GenerationType.AUTO) + @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") private Long id; - public Long getId() { - return id; + @Column(nullable = false) + private ZonedDateTime createdAt; + + @Column(nullable = false) + private ZonedDateTime updatedAt; + + @PrePersist + public void prePersist() { + createdAt = updatedAt = ZonedDateTime.now(); } - public void setId(Long _id) { - id = _id; + @PreUpdate + public void preUpdate() { + updatedAt = ZonedDateTime.now(); } @Override @@ -31,18 +43,38 @@ public int compareTo(BaseModel o) { } public boolean equals(Object other) { - if (other == null || other.getClass() != this.getClass()) + if (other == null || other.getClass() != this.getClass()) { return false; - EqualsBuilder eb = new EqualsBuilder(); - eb.append(this.getId(), ((BaseModel) other).getId()); - return eb.isEquals(); + } + + return this.getId().equals(((BaseModel) other).getId()); } - /** - * use HashCodeBuilder to calculate a hashcode - */ public int hashCode() { return new HashCodeBuilder().append(getId()).toHashCode(); } + public Long getId() { + return id; + } + + public void setId(Long _id) { + id = _id; + } + + public ZonedDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(ZonedDateTime createdAt) { + this.createdAt = createdAt; + } + + public ZonedDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(ZonedDateTime updatedAt) { + this.updatedAt = updatedAt; + } } \ No newline at end of file diff --git a/src/main/java/com/raysmond/blog/models/Post.java b/src/main/java/com/raysmond/blog/models/Post.java index 58337a5..a3485d2 100644 --- a/src/main/java/com/raysmond/blog/models/Post.java +++ b/src/main/java/com/raysmond/blog/models/Post.java @@ -1,126 +1,89 @@ package com.raysmond.blog.models; -import com.raysmond.blog.forms.PostForm; import com.raysmond.blog.models.support.PostFormat; import com.raysmond.blog.models.support.PostStatus; import com.raysmond.blog.models.support.PostType; + +import lombok.Getter; +import lombok.Setter; + +import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.Type; +import org.springframework.util.StringUtils; import javax.persistence.*; -import java.util.Date; + +import java.text.SimpleDateFormat; +import java.util.HashSet; +import java.util.Set; /** - * Created by Raysmond on 9/25/15. + * @author Raysmond */ @Entity @Table(name = "posts") -public class Post extends BaseModel{ +@Getter +@Setter +@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "postCache") +public class Post extends BaseModel { + private static final SimpleDateFormat SLUG_DATE_FORMAT = new SimpleDateFormat("yyyy/MM/dd"); + @ManyToOne private User user; @Column(nullable = false) private String title; - @Type(type="text") + @Type(type = "text") private String content; @Type(type = "text") private String renderedContent; - @Column(nullable = false) - @Enumerated(EnumType.STRING) - private PostStatus postStatus; + @Type(type = "text") + private String summary; + + @Type(type = "text") + private String renderedSummary; @Column(nullable = false) @Enumerated(EnumType.STRING) - private PostFormat postFormat; + private PostStatus postStatus = PostStatus.PUBLISHED; @Column(nullable = false) @Enumerated(EnumType.STRING) - private PostType postType; + private PostFormat postFormat = PostFormat.MARKDOWN; @Column(nullable = false) - private Date createdAt = new Date(); - - @PrePersist - public void beforeCreate(){ - if (postStatus == null) - postStatus = PostStatus.DRAFT; - - if (postFormat == null) - postFormat = PostFormat.HTML; - - if (createdAt == null) - createdAt = new Date(); - - if (postType == null) - postType = PostType.POST; - } - - public User getUser() { - return user; - } - - public void setUser(User user) { - this.user = user; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getContent() { - return content; - } + @Enumerated(EnumType.STRING) + private PostType postType = PostType.POST; - public void setContent(String content) { - this.content = content; - } + @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @JoinTable(name = "posts_tags", + joinColumns = {@JoinColumn(name = "post_id", nullable = false, updatable = false)}, + inverseJoinColumns = {@JoinColumn(name = "tag_id", nullable = false, updatable = false)} + ) + @org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "tagCache") + private Set tags = new HashSet<>(); - public PostStatus getPostStatus() { - return postStatus; - } + private String permalink; - public void setPostStatus(PostStatus postStatus) { - this.postStatus = postStatus; - } + private Integer views = 0; - public PostFormat getPostFormat() { - return postFormat; - } - - public void setPostFormat(PostFormat postFormat) { - this.postFormat = postFormat; + public Integer getViews() { + return views == null ? 0 : views; } public String getRenderedContent() { - if (this.postFormat == PostFormat.MARKDOWN) + if (this.postFormat == PostFormat.MARKDOWN) { return renderedContent; + } return getContent(); } - public void setRenderedContent(String renderedContent) { - this.renderedContent = renderedContent; - } - - public Date getCreatedAt() { - return createdAt; - } - - public void setCreatedAt(Date createdAt) { - this.createdAt = createdAt; - } - - public PostType getPostType() { - return postType; - } - - public void setPostType(PostType postType) { - this.postType = postType; + public void setPermalink(String permalink) { + String token = permalink.toLowerCase().replace("\n", " ").replaceAll("[^a-z\\d\\s]", " "); + this.permalink = StringUtils.arrayToDelimitedString(StringUtils.tokenizeToStringArray(token, " "), "-"); } } diff --git a/src/main/java/com/raysmond/blog/models/Setting.java b/src/main/java/com/raysmond/blog/models/Setting.java new file mode 100644 index 0000000..4704a57 --- /dev/null +++ b/src/main/java/com/raysmond/blog/models/Setting.java @@ -0,0 +1,32 @@ +package com.raysmond.blog.models; + +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Lob; +import javax.persistence.Table; +import java.io.Serializable; + +import org.hibernate.annotations.CacheConcurrencyStrategy; + +/** + * A generic setting model + * + * @author Raysmond + */ +@Entity +@Table(name = "settings") +@Getter +@Setter +@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "settingCache") +public class Setting extends BaseModel { + @Column(name = "_key", unique = true, nullable = false) + private String key; + + @Lob + @Column(name = "_value") + private Serializable value; + +} diff --git a/src/main/java/com/raysmond/blog/models/Tag.java b/src/main/java/com/raysmond/blog/models/Tag.java new file mode 100644 index 0000000..67c2733 --- /dev/null +++ b/src/main/java/com/raysmond/blog/models/Tag.java @@ -0,0 +1,35 @@ +package com.raysmond.blog.models; + +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.annotations.CacheConcurrencyStrategy; + +/** + * @author Raysmond + */ +@Entity +@Table(name = "tags") +@Getter +@Setter +@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "tagCache") +public class Tag extends BaseModel { + + @Column(nullable = false, unique = true) + private String name; + + @ManyToMany(fetch = FetchType.LAZY, mappedBy = "tags") + private List posts = new ArrayList<>(); + + public Tag() { + + } + + public Tag(String name) { + this.setName(name); + } +} diff --git a/src/main/java/com/raysmond/blog/models/User.java b/src/main/java/com/raysmond/blog/models/User.java index b59e859..cf75b07 100644 --- a/src/main/java/com/raysmond/blog/models/User.java +++ b/src/main/java/com/raysmond/blog/models/User.java @@ -1,19 +1,28 @@ package com.raysmond.blog.models; -import javax.persistence.*; - import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Getter; +import lombok.Setter; + +import javax.persistence.*; + import java.util.ArrayList; import java.util.Collection; -@SuppressWarnings("serial") +import org.hibernate.annotations.CacheConcurrencyStrategy; + +/** + * @author Raysmond + */ @Entity @Table(name = "users") -@NamedQuery(name = User.FIND_BY_EMAIL, query = "select a from User a where a.email = :email") -public class User extends BaseModel implements java.io.Serializable { - - public static final String FIND_BY_EMAIL = "User.findByEmail"; +@Getter +@Setter +@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "userCache") +public class User extends BaseModel { + public static final String ROLE_ADMIN = "ROLE_ADMIN"; + public static final String ROLE_USER = "ROLE_USER"; @Column(unique = true) private String email; @@ -21,12 +30,12 @@ public class User extends BaseModel implements java.io.Serializable { @JsonIgnore private String password; - private String role = "ROLE_USER"; + private String role = ROLE_USER; @OneToMany(fetch = FetchType.LAZY, mappedBy = "user", cascade = CascadeType.REMOVE) private Collection posts = new ArrayList<>(); - protected User() { + public User() { } @@ -35,29 +44,4 @@ public User(String email, String password, String role) { this.password = password; this.role = role; } - - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } - - public String getRole() { - return role; - } - - public void setRole(String role) { - this.role = role; - } } diff --git a/src/main/java/com/raysmond/blog/models/support/PostFormat.java b/src/main/java/com/raysmond/blog/models/support/PostFormat.java index 4e0e27c..6b72ec9 100644 --- a/src/main/java/com/raysmond/blog/models/support/PostFormat.java +++ b/src/main/java/com/raysmond/blog/models/support/PostFormat.java @@ -1,7 +1,7 @@ package com.raysmond.blog.models.support; /** - * Created by Raysmond on 9/25/15. + * @author Raysmond */ public enum PostFormat { HTML("Html"), @@ -9,11 +9,11 @@ public enum PostFormat { private String displayName; - PostFormat(String displayName){ + PostFormat(String displayName) { this.displayName = displayName; } - public String getDisplayName(){ + public String getDisplayName() { return displayName; } diff --git a/src/main/java/com/raysmond/blog/models/support/PostStatus.java b/src/main/java/com/raysmond/blog/models/support/PostStatus.java index b29e2b5..bd78ecd 100644 --- a/src/main/java/com/raysmond/blog/models/support/PostStatus.java +++ b/src/main/java/com/raysmond/blog/models/support/PostStatus.java @@ -1,7 +1,7 @@ package com.raysmond.blog.models.support; /** - * Created by Raysmond on 9/26/15. + * @author Raysmond */ public enum PostStatus { DRAFT("Draft"), @@ -9,7 +9,7 @@ public enum PostStatus { private String name; - PostStatus(String name){ + PostStatus(String name) { this.name = name; } @@ -20,4 +20,13 @@ public String getName() { public void setName(String name) { this.name = name; } + + public String getId() { + return name(); + } + + @Override + public String toString() { + return getName(); + } } diff --git a/src/main/java/com/raysmond/blog/models/support/PostType.java b/src/main/java/com/raysmond/blog/models/support/PostType.java index cd7677f..83f9055 100644 --- a/src/main/java/com/raysmond/blog/models/support/PostType.java +++ b/src/main/java/com/raysmond/blog/models/support/PostType.java @@ -1,7 +1,7 @@ package com.raysmond.blog.models.support; /** - * Created by Raysmond on 9/26/15. + * @author Raysmond */ public enum PostType { PAGE("Page"), @@ -9,7 +9,7 @@ public enum PostType { private String name; - PostType(String name){ + PostType(String name) { this.name = name; } @@ -20,4 +20,13 @@ public String getName() { public void setName(String name) { this.name = name; } + + public String getId() { + return name(); + } + + @Override + public String toString() { + return getName(); + } } diff --git a/src/main/java/com/raysmond/blog/repositories/PostRepository.java b/src/main/java/com/raysmond/blog/repositories/PostRepository.java index 7f0c62b..5970541 100644 --- a/src/main/java/com/raysmond/blog/repositories/PostRepository.java +++ b/src/main/java/com/raysmond/blog/repositories/PostRepository.java @@ -1,27 +1,37 @@ package com.raysmond.blog.repositories; import com.raysmond.blog.models.Post; +import com.raysmond.blog.models.support.PostStatus; import com.raysmond.blog.models.support.PostType; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.repository.CrudRepository; -import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; -import javax.persistence.EntityManager; -import javax.persistence.PersistenceContext; +import java.util.List; /** - * Created by Raysmond on 9/25/15. + * @author Raysmond */ - @Repository -@Transactional -public interface PostRepository extends CrudRepository, PagingAndSortingRepository{ - Post findByTitle(String title); - Post findByTitleAndPostType(String title, PostType postType); +public interface PostRepository extends JpaRepository { + Post findByPermalinkAndPostStatus(String permalink, PostStatus postStatus); + Page findAllByPostType(PostType postType, Pageable pageRequest); + + Page findAllByPostTypeAndPostStatus(PostType postType, PostStatus postStatus, Pageable pageRequest); + + @Query("SELECT p FROM Post p INNER JOIN p.tags t WHERE t.name = :tag") + Page findByTag(@Param("tag") String tag, Pageable pageable); + + @Query("SELECT t.name, count(p) as tag_count from Post p " + + "INNER JOIN p.tags t " + + "WHERE p.postStatus = :status " + + "GROUP BY t.id " + + "ORDER BY tag_count DESC") + List countPostsByTags(@Param("status") PostStatus status); } diff --git a/src/main/java/com/raysmond/blog/repositories/SettingRepository.java b/src/main/java/com/raysmond/blog/repositories/SettingRepository.java new file mode 100644 index 0000000..5a48fba --- /dev/null +++ b/src/main/java/com/raysmond/blog/repositories/SettingRepository.java @@ -0,0 +1,14 @@ +package com.raysmond.blog.repositories; + +import com.raysmond.blog.models.Setting; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Raysmond + */ +@Repository +public interface SettingRepository extends JpaRepository { + Setting findByKey(String key); +} diff --git a/src/main/java/com/raysmond/blog/repositories/TagRepository.java b/src/main/java/com/raysmond/blog/repositories/TagRepository.java new file mode 100644 index 0000000..663869d --- /dev/null +++ b/src/main/java/com/raysmond/blog/repositories/TagRepository.java @@ -0,0 +1,11 @@ +package com.raysmond.blog.repositories; + +import com.raysmond.blog.models.Tag; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * @author Raysmond + */ +public interface TagRepository extends JpaRepository { + Tag findByName(String name); +} diff --git a/src/main/java/com/raysmond/blog/repositories/UserRepository.java b/src/main/java/com/raysmond/blog/repositories/UserRepository.java index d2b191b..8382b60 100644 --- a/src/main/java/com/raysmond/blog/repositories/UserRepository.java +++ b/src/main/java/com/raysmond/blog/repositories/UserRepository.java @@ -1,37 +1,13 @@ package com.raysmond.blog.repositories; -import javax.persistence.*; -import javax.inject.Inject; - import com.raysmond.blog.models.User; +import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.security.crypto.password.PasswordEncoder; +/** + * @author Raysmond + */ @Repository -@Transactional -public class UserRepository { - - @PersistenceContext - private EntityManager entityManager; - - @Inject - private PasswordEncoder passwordEncoder; - - @Transactional - public User save(User user) { - user.setPassword(passwordEncoder.encode(user.getPassword())); - entityManager.persist(user); - return user; - } - - public User findByEmail(String email) { - try { - return entityManager.createNamedQuery(User.FIND_BY_EMAIL, User.class) - .setParameter("email", email) - .getSingleResult(); - } catch (PersistenceException e) { - return null; - } - } +public interface UserRepository extends JpaRepository { + User findByEmail(String email); } diff --git a/src/main/java/com/raysmond/blog/services/AppSetting.java b/src/main/java/com/raysmond/blog/services/AppSetting.java new file mode 100644 index 0000000..1aaa8bc --- /dev/null +++ b/src/main/java/com/raysmond/blog/services/AppSetting.java @@ -0,0 +1,51 @@ +package com.raysmond.blog.services; + +import com.domingosuarez.boot.autoconfigure.jade4j.JadeHelper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * @author Raysmond + */ +@JadeHelper("App") +@Service +public class AppSetting { + + public static final String SITE_NAME = "site_name"; + public static final String SITE_SLOGAN = "site_slogan"; + public static final String PAGE_SIZE = "page_size"; + + @Autowired + private SettingService settingService; + + private String siteName = "SpringBlog"; + private String siteSlogan = "An interesting place to discover"; + private Integer pageSize = 5; + + public String getSiteName() { + return (String) settingService.get(SITE_NAME, siteName); + } + + public void setSiteName(String siteName) { + this.siteName = siteName; + settingService.put(SITE_NAME, siteName); + } + + public Integer getPageSize() { + return (Integer) settingService.get(PAGE_SIZE, pageSize); + } + + public void setPageSize(Integer pageSize) { + this.pageSize = pageSize; + settingService.put(PAGE_SIZE, pageSize); + } + + public String getSiteSlogan() { + return (String) settingService.get(SITE_SLOGAN, siteSlogan); + } + + public void setSiteSlogan(String siteSlogan) { + this.siteSlogan = siteSlogan; + settingService.put(SITE_SLOGAN, siteSlogan); + } +} diff --git a/src/main/java/com/raysmond/blog/services/CacheSettingService.java b/src/main/java/com/raysmond/blog/services/CacheSettingService.java new file mode 100644 index 0000000..cb2ec8b --- /dev/null +++ b/src/main/java/com/raysmond/blog/services/CacheSettingService.java @@ -0,0 +1,71 @@ +package com.raysmond.blog.services; + +import com.raysmond.blog.models.Setting; +import com.raysmond.blog.repositories.SettingRepository; + +import lombok.extern.slf4j.Slf4j; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; + +/** + * @author Raysmond + */ +@Service +@Slf4j +@Transactional +public class CacheSettingService implements SettingService { + private SettingRepository settingRepository; + + @Autowired + public CacheSettingService(SettingRepository settingRepository) { + this.settingRepository = settingRepository; + } + + @Override + @Cacheable(value = "settingCache", key = "#key") + public Serializable get(String key) { + Setting setting = settingRepository.findByKey(key); + Serializable value = null; + try { + value = setting == null ? null : setting.getValue(); + } catch (Exception ex) { + log.info("Cannot deserialize setting value with key = " + key); + } + + log.info("Get setting " + key + " from database. Value = " + value); + + return value; + } + + @Override + @Cacheable(value = "settingCache", key = "#key") + public Serializable get(String key, Serializable defaultValue) { + Serializable value = get(key); + return value == null ? defaultValue : value; + } + + @Override + @CacheEvict(value = "settingCache", key = "#key") + public void put(String key, Serializable value) { + log.info("Update setting " + key + " to database. Value = " + value); + + Setting setting = settingRepository.findByKey(key); + if (setting == null) { + setting = new Setting(); + setting.setKey(key); + } + try { + setting.setValue(value); + settingRepository.save(setting); + } catch (Exception ex) { + + log.info("Cannot save setting value with type: " + value.getClass() + ". key = " + key); + } + } +} diff --git a/src/main/java/com/raysmond/blog/services/MarkdownService.java b/src/main/java/com/raysmond/blog/services/MarkdownService.java deleted file mode 100644 index 0ea7e58..0000000 --- a/src/main/java/com/raysmond/blog/services/MarkdownService.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.raysmond.blog.services; - -/** - * Created by Raysmond on 9/25/15. - */ -public interface MarkdownService { - public String renderToHtml(String content); -} diff --git a/src/main/java/com/raysmond/blog/services/PostService.java b/src/main/java/com/raysmond/blog/services/PostService.java index 71f1095..550f5d5 100644 --- a/src/main/java/com/raysmond/blog/services/PostService.java +++ b/src/main/java/com/raysmond/blog/services/PostService.java @@ -1,56 +1,206 @@ package com.raysmond.blog.services; +import com.raysmond.blog.Constants; +import com.raysmond.blog.error.NotFoundException; import com.raysmond.blog.models.Post; -import com.raysmond.blog.models.User; +import com.raysmond.blog.models.Tag; import com.raysmond.blog.models.support.PostFormat; import com.raysmond.blog.models.support.PostStatus; import com.raysmond.blog.models.support.PostType; import com.raysmond.blog.repositories.PostRepository; +import com.raysmond.blog.support.web.MarkdownService; + +import lombok.extern.slf4j.Slf4j; + import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; /** - * Created by Raysmond on 9/26/15. + * @author Raysmond */ @Service +@Slf4j +@Transactional public class PostService { + + @Autowired + private PostRepository postRepository; + + @Autowired + private TagService tagService; + @Autowired - private PostRepository posts; + private UserService userService; @Autowired - private MarkdownService markdown; + @Qualifier("flexmark") + private MarkdownService markdownService; + + public Post getPost(Long postId) { + log.debug("Get post " + postId); + + Post post = postRepository.findOne(postId); + + if (post == null) { + throw new NotFoundException("Post with id " + postId + " is not found."); + } + + return post; + } + + public Post getPublishedPostByPermalink(String permalink) { + log.debug("Get post with permalink " + permalink); + + Post post = postRepository.findByPermalinkAndPostStatus(permalink, PostStatus.PUBLISHED); + + if (post == null) { + try { + post = postRepository.findOne(Long.valueOf(permalink)); + } catch (NumberFormatException e) { + post = null; + } + } + + if (post == null) { + throw new NotFoundException("Post with permalink '" + permalink + "' is not found."); + } + + return post; + } + + public Post createPost(Post post) { + if (post.getPostFormat() == PostFormat.MARKDOWN) { + post.setRenderedContent(markdownService.renderToHtml(post.getContent())); + post.setRenderedSummary(markdownService.renderToHtml(post.getSummary())); + } + + return postRepository.save(post); + } + + public Post updatePost(Post post) { + if (post.getPostFormat() == PostFormat.MARKDOWN) { + post.setRenderedContent(markdownService.renderToHtml(post.getContent())); + post.setRenderedSummary(markdownService.renderToHtml(post.getSummary())); + } + + return postRepository.save(post); + } + + public void deletePost(Post post) { + postRepository.delete(post); + } + + public List getArchivePosts() { + log.debug("Get all archive posts from database."); + + Pageable page = new PageRequest(0, Integer.MAX_VALUE, Sort.Direction.DESC, "createdAt"); + return postRepository.findAllByPostTypeAndPostStatus(PostType.POST, PostStatus.PUBLISHED, page) + .getContent() + .stream() + .map(this::extractPostMeta) + .collect(Collectors.toList()); + } + + public List getPostTags(Post post) { + log.debug("Get tags of post {}", post.getId()); - public Post createPage(User user, String title, String content, PostFormat format, PostStatus status){ - return _createPost(user,title,content,format, status, PostType.PAGE); + List tags = new ArrayList<>(); + + // Load the post first. If not, when the post is cached before while the tags not, + // then the LAZY loading of post tags will cause an initialization error because + // of not hibernate connection session + postRepository.findOne(post.getId()).getTags().forEach(tags::add); + return tags; } - public Post createPost(User user, String title, String content, PostFormat format, PostStatus status){ - return _createPost(user,title,content,format, status, PostType.POST); + private Post extractPostMeta(Post post) { + Post archivePost = new Post(); + archivePost.setId(post.getId()); + archivePost.setTitle(post.getTitle()); + archivePost.setPermalink(post.getPermalink()); + archivePost.setCreatedAt(post.getCreatedAt()); + + return archivePost; + } + + public Page getAllPublishedPostsByPage(int page, int pageSize) { + log.debug("Get posts by page " + page); + + return postRepository.findAllByPostTypeAndPostStatus( + PostType.POST, + PostStatus.PUBLISHED, + new PageRequest(page, pageSize, Sort.Direction.DESC, "createdAt")); } - public Post _createPost(User user, String title, String content, PostFormat format, PostStatus status, PostType type){ + public Post createAboutPage() { + log.debug("Create default about page"); + Post post = new Post(); - post.setUser(user); - post.setTitle(title); - post.setContent(content); - post.setPostFormat(format); - post.setPostStatus(status); - post.setPostType(type); + post.setTitle(Constants.ABOUT_PAGE_PERMALINK); + post.setContent(Constants.ABOUT_PAGE_PERMALINK.toLowerCase()); + post.setPermalink(Constants.ABOUT_PAGE_PERMALINK); + post.setUser(userService.getSuperUser()); + post.setPostFormat(PostFormat.MARKDOWN); return createPost(post); } - public Post createPost(Post post){ - if (post.getPostFormat() == PostFormat.MARKDOWN){ - post.setRenderedContent(markdown.renderToHtml(post.getContent())); + public Set parseTagNames(String tagNames) { + Set tags = new HashSet<>(); + + if (tagNames != null && !tagNames.isEmpty()) { + tagNames = tagNames.toLowerCase(); + String[] names = tagNames.split("\\s*,\\s*"); + for (String name : names) { + tags.add(tagService.findOrCreateByName(name)); + } } - return posts.save(post); + + return tags; + } + + public String getTagNames(Set tags) { + if (tags == null || tags.isEmpty()) { + return ""; + } + + StringBuilder names = new StringBuilder(); + tags.forEach(tag -> names.append(tag.getName()).append(",")); + names.deleteCharAt(names.length() - 1); + + return names.toString(); + } + + // cache or not? + public Page findPostsByTag(String tagName, int page, int pageSize) { + return postRepository.findByTag(tagName, new PageRequest(page, pageSize, Sort.Direction.DESC, "createdAt")); + } + + public List countPostsByTags() { + log.debug("Count posts group by tags."); + + return postRepository.countPostsByTags(PostStatus.PUBLISHED); } - public Post updatePost(Post post){ - if (post.getPostFormat() == PostFormat.MARKDOWN){ - post.setRenderedContent(markdown.renderToHtml(post.getContent())); + @Async + public void incrementViews(Long postId) { + synchronized(this) { + Post post = postRepository.findOne(postId); + post.setViews(post.getViews() + 1); + postRepository.save(post); } - return posts.save(post); } } diff --git a/src/main/java/com/raysmond/blog/services/SettingService.java b/src/main/java/com/raysmond/blog/services/SettingService.java new file mode 100644 index 0000000..3d0e988 --- /dev/null +++ b/src/main/java/com/raysmond/blog/services/SettingService.java @@ -0,0 +1,14 @@ +package com.raysmond.blog.services; + +import java.io.Serializable; + +/** + * @author Raysmond + */ +public interface SettingService { + Serializable get(String key); + + Serializable get(String key, Serializable defaultValue); + + void put(String key, Serializable value); +} diff --git a/src/main/java/com/raysmond/blog/services/TagService.java b/src/main/java/com/raysmond/blog/services/TagService.java new file mode 100644 index 0000000..da1d7ca --- /dev/null +++ b/src/main/java/com/raysmond/blog/services/TagService.java @@ -0,0 +1,43 @@ +package com.raysmond.blog.services; + +import com.raysmond.blog.models.Tag; +import com.raysmond.blog.repositories.TagRepository; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +/** + * @author Raysmond + */ +@Service +public class TagService { + private TagRepository tagRepository; + + @Autowired + public TagService(TagRepository tagRepository) { + this.tagRepository = tagRepository; + } + + public Tag findOrCreateByName(String name) { + Tag tag = tagRepository.findByName(name); + if (tag == null) { + tag = tagRepository.save(new Tag(name)); + } + return tag; + } + + public Tag getTag(String tagName) { + return tagRepository.findByName(tagName); + } + + public void deleteTag(Tag tag) { + tagRepository.delete(tag); + } + + public List getAllTags() { + return tagRepository.findAll(); + } + +} diff --git a/src/main/java/com/raysmond/blog/services/UserService.java b/src/main/java/com/raysmond/blog/services/UserService.java index 538f379..744b5f6 100644 --- a/src/main/java/com/raysmond/blog/services/UserService.java +++ b/src/main/java/com/raysmond/blog/services/UserService.java @@ -1,38 +1,91 @@ package com.raysmond.blog.services; -import java.util.Collections; - -import javax.annotation.PostConstruct; - +import com.raysmond.blog.Constants; import com.raysmond.blog.models.User; import com.raysmond.blog.repositories.UserRepository; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.core.*; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.*; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -public class UserService implements UserDetailsService { +import javax.annotation.PostConstruct; +import java.util.Collections; +@Transactional +@Service +@Slf4j +public class UserService implements UserDetailsService { + @Autowired + private UserRepository userRepository; @Autowired - private UserRepository users; + private PasswordEncoder passwordEncoder; @PostConstruct protected void initialize() { - if (users.findByEmail("user@raysmond.com") == null) { - users.save(new User("user@raysmond.com", "user", "ROLE_USER")); - users.save(new User("admin@raysmond.com", "admin", "ROLE_ADMIN")); + getSuperUser(); + } + + public User createUser(User user) { + user.setPassword(passwordEncoder.encode(user.getPassword())); + return userRepository.save(user); + } + + public User getSuperUser() { + User user = userRepository.findByEmail(Constants.DEFAULT_ADMIN_EMAIL); + + if (user == null) { + user = createUser(new User(Constants.DEFAULT_ADMIN_EMAIL, Constants.DEFAULT_ADMIN_PASSWORD, User.ROLE_ADMIN)); } + + return user; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { - User user = users.findByEmail(username); + User user = userRepository.findByEmail(username); if (user == null) { throw new UsernameNotFoundException("user not found"); } - return createUser(user); + return createSpringUser(user); + } + + public User currentUser() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + if (auth == null || auth instanceof AnonymousAuthenticationToken) { + return null; + } + + String email = ((org.springframework.security.core.userdetails.User) auth.getPrincipal()).getUsername(); + + return userRepository.findByEmail(email); + } + + public boolean changePassword(User user, String password, String newPassword) { + if (password == null || newPassword == null || password.isEmpty() || newPassword.isEmpty()) + return false; + + boolean match = passwordEncoder.matches(password, user.getPassword()); + if (!match) + return false; + + user.setPassword(passwordEncoder.encode(newPassword)); + userRepository.save(user); + + log.info("User @{} changed password.", user.getEmail()); + + return true; } public void signin(User user) { @@ -40,10 +93,10 @@ public void signin(User user) { } private Authentication authenticate(User user) { - return new UsernamePasswordAuthenticationToken(createUser(user), null, Collections.singleton(createAuthority(user))); + return new UsernamePasswordAuthenticationToken(createSpringUser(user), null, Collections.singleton(createAuthority(user))); } - private org.springframework.security.core.userdetails.User createUser(User user) { + private org.springframework.security.core.userdetails.User createSpringUser(User user) { return new org.springframework.security.core.userdetails.User( user.getEmail(), user.getPassword(), diff --git a/src/main/java/com/raysmond/blog/support/web/MarkdownService.java b/src/main/java/com/raysmond/blog/support/web/MarkdownService.java new file mode 100644 index 0000000..755e97b --- /dev/null +++ b/src/main/java/com/raysmond/blog/support/web/MarkdownService.java @@ -0,0 +1,8 @@ +package com.raysmond.blog.support.web; + +/** + * @author Raysmond + */ +public interface MarkdownService { + String renderToHtml(String content); +} diff --git a/src/main/java/com/raysmond/blog/support/web/Message.java b/src/main/java/com/raysmond/blog/support/web/Message.java index e05d046..ca9645f 100644 --- a/src/main/java/com/raysmond/blog/support/web/Message.java +++ b/src/main/java/com/raysmond/blog/support/web/Message.java @@ -7,40 +7,38 @@ public class Message implements java.io.Serializable { /** * Name of the flash attribute. */ - public static final String MESSAGE_ATTRIBUTE = "message"; + public static final String MESSAGE_ATTRIBUTE = "message"; + private final String message; + private final Type type; + private final Object[] args; + public Message(String message, Type type) { + this.message = message; + this.type = type; + this.args = null; + } - /** - * The type of the message to be displayed. The type is used to show message in a different style. - */ - public static enum Type { - DANGER, WARNING, INFO, SUCCESS; - } - - private final String message; - private final Type type; - private final Object[] args; + public Message(String message, Type type, Object... args) { + this.message = message; + this.type = type; + this.args = args; + } - public Message(String message, Type type) { - this.message = message; - this.type = type; - this.args = null; - } - - public Message(String message, Type type, Object... args) { - this.message = message; - this.type = type; - this.args = args; - } + public String getMessage() { + return message; + } - public String getMessage() { - return message; - } + public Type getType() { + return type; + } - public Type getType() { - return type; - } + public Object[] getArgs() { + return args; + } - public Object[] getArgs() { - return args; - } + /** + * The type of the message to be displayed. The type is used to show message in a different style. + */ + public static enum Type { + DANGER, WARNING, INFO, SUCCESS; + } } diff --git a/src/main/java/com/raysmond/blog/support/web/MessageHelper.java b/src/main/java/com/raysmond/blog/support/web/MessageHelper.java index f85bcda..02b7557 100644 --- a/src/main/java/com/raysmond/blog/support/web/MessageHelper.java +++ b/src/main/java/com/raysmond/blog/support/web/MessageHelper.java @@ -1,10 +1,10 @@ package com.raysmond.blog.support.web; -import static com.raysmond.blog.support.web.Message.MESSAGE_ATTRIBUTE; - import org.springframework.ui.Model; import org.springframework.web.servlet.mvc.support.RedirectAttributes; +import static com.raysmond.blog.support.web.Message.MESSAGE_ATTRIBUTE; + public final class MessageHelper { private MessageHelper() { diff --git a/src/main/java/com/raysmond/blog/support/web/PygmentsService.java b/src/main/java/com/raysmond/blog/support/web/PygmentsService.java index 57abbb1..0f47c3a 100644 --- a/src/main/java/com/raysmond/blog/support/web/PygmentsService.java +++ b/src/main/java/com/raysmond/blog/support/web/PygmentsService.java @@ -1,10 +1,10 @@ package com.raysmond.blog.support.web; -import org.springframework.stereotype.Service; import org.python.util.PythonInterpreter; +import org.springframework.stereotype.Service; /** - * Created by Raysmond on 9/26/15. + * @author Raysmond */ @Service public class PygmentsService implements SyntaxHighlightService { @@ -18,9 +18,9 @@ public String highlight(String content) { // Simple use Pygments as you would in Python interpreter.exec("from pygments import highlight\n" - + "from pygments.lexers import PythonLexer\n" - + "from pygments.formatters import HtmlFormatter\n" - + "\nresult = highlight(code, PythonLexer(), HtmlFormatter())"); + + "from pygments.lexers import PythonLexer\n" + + "from pygments.formatters import HtmlFormatter\n" + + "\nresult = highlight(code, PythonLexer(), HtmlFormatter())"); return interpreter.get("result", String.class); } diff --git a/src/main/java/com/raysmond/blog/support/web/PygmentsVerbatimSerializer.java b/src/main/java/com/raysmond/blog/support/web/PygmentsVerbatimSerializer.java index 3d34b6a..332b703 100644 --- a/src/main/java/com/raysmond/blog/support/web/PygmentsVerbatimSerializer.java +++ b/src/main/java/com/raysmond/blog/support/web/PygmentsVerbatimSerializer.java @@ -3,10 +3,9 @@ import org.pegdown.Printer; import org.pegdown.VerbatimSerializer; import org.pegdown.ast.VerbatimNode; -import org.springframework.beans.factory.annotation.Autowired; /** - * Created by Raysmond on 9/26/15. + * @author Raysmond */ public class PygmentsVerbatimSerializer implements VerbatimSerializer { public static final PygmentsVerbatimSerializer INSTANCE = new PygmentsVerbatimSerializer(); @@ -16,26 +15,6 @@ public class PygmentsVerbatimSerializer implements VerbatimSerializer { @Override public void serialize(final VerbatimNode node, final Printer printer) { printer.print(syntaxHighlightService.highlight(node.getText())); - - // String type = node.getType(); - // String text = node.getText(); -// -// printer.println().print("
");
-//        String text = node.getText();
-//        // print HTML breaks for all initial newlines
-//        while (text.charAt(0) == '\n') {
-//            printer.print("
"); -// text = text.substring(1); -// } -// printer.printEncoded(text); -// printer.print("
"); - } } \ No newline at end of file diff --git a/src/main/java/com/raysmond/blog/support/web/SyntaxHighlightService.java b/src/main/java/com/raysmond/blog/support/web/SyntaxHighlightService.java index f7ab9a8..cb4c4b3 100644 --- a/src/main/java/com/raysmond/blog/support/web/SyntaxHighlightService.java +++ b/src/main/java/com/raysmond/blog/support/web/SyntaxHighlightService.java @@ -1,8 +1,8 @@ package com.raysmond.blog.support.web; /** - * Created by Raysmond on 9/26/15. + * @author Raysmond */ public interface SyntaxHighlightService { - public String highlight(String content); + String highlight(String content); } diff --git a/src/main/java/com/raysmond/blog/support/web/ViewHelper.java b/src/main/java/com/raysmond/blog/support/web/ViewHelper.java index 57bfe6d..e2df79f 100644 --- a/src/main/java/com/raysmond/blog/support/web/ViewHelper.java +++ b/src/main/java/com/raysmond/blog/support/web/ViewHelper.java @@ -1,24 +1,52 @@ package com.raysmond.blog.support.web; +import com.domingosuarez.boot.autoconfigure.jade4j.JadeHelper; +import com.raysmond.blog.services.AppSetting; -import org.springframework.stereotype.Service; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; import java.text.SimpleDateFormat; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.Date; /** - * Created by Raysmond on 9/26/15. + * @author Raysmond */ @Service +@JadeHelper("viewHelper") public class ViewHelper { private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MMMM dd, yyyy"); private static final SimpleDateFormat DATE_FORMAT_MONTH_DAY = new SimpleDateFormat("MMM dd"); + private AppSetting appSetting; + + private String applicationEnv; private long startTime; - public long getResponseTime(){ + /** + * Check if current user is authenticated + * + * @return true/false + */ + public boolean isLogin() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return null != authentication + && authentication.isAuthenticated() + && !(authentication instanceof AnonymousAuthenticationToken); + } + + @Autowired + public ViewHelper(AppSetting appSetting) { + this.appSetting = appSetting; + } + + public long getResponseTime() { return System.currentTimeMillis() - startTime; } @@ -30,12 +58,27 @@ public void setStartTime(long startTime) { this.startTime = startTime; } - public String getFormattedDate(Date date){ + public String getFormattedDate(Date date) { return date == null ? "" : DATE_FORMAT.format(date); } - public String getMonthAndDay(Date date){ + public String getFormattedDate(ZonedDateTime date) { + return date == null ? "" : date.format(DateTimeFormatter.ISO_LOCAL_DATE); + } + + public String getMonthAndDay(Date date) { return date == null ? "" : DATE_FORMAT_MONTH_DAY.format(date); } + public String metaTitle(String title) { + return title + " · " + appSetting.getSiteName(); + } + + public String getApplicationEnv() { + return applicationEnv; + } + + public void setApplicationEnv(String applicationEnv) { + this.applicationEnv = applicationEnv; + } } diff --git a/src/main/java/com/raysmond/blog/support/web/impl/FlexmarkMarkdownService.java b/src/main/java/com/raysmond/blog/support/web/impl/FlexmarkMarkdownService.java new file mode 100644 index 0000000..db19899 --- /dev/null +++ b/src/main/java/com/raysmond/blog/support/web/impl/FlexmarkMarkdownService.java @@ -0,0 +1,46 @@ +package com.raysmond.blog.support.web.impl; + +import java.util.Arrays; + +import org.springframework.stereotype.Service; + +import com.raysmond.blog.support.web.MarkdownService; +import com.vladsch.flexmark.ast.Node; +import com.vladsch.flexmark.ext.autolink.AutolinkExtension; +import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension; +import com.vladsch.flexmark.ext.tables.TablesExtension; +import com.vladsch.flexmark.html.HtmlRenderer; +import com.vladsch.flexmark.parser.Parser; +import com.vladsch.flexmark.util.options.MutableDataSet; + +import lombok.extern.slf4j.Slf4j; + +/** + * 使用flexmark-java解析markdown + * 参考:https://github.com/vsch/flexmark-java + * + * @author Raysmond + */ +@Service("flexmark") +@Slf4j +public class FlexmarkMarkdownService implements MarkdownService { + @Override + public String renderToHtml(String content) { + MutableDataSet options = new MutableDataSet(); + + options.set(Parser.EXTENSIONS, + Arrays.asList(TablesExtension.create(), + AutolinkExtension.create(), + StrikethroughExtension.create())); + + // uncomment to convert soft-breaks to hard breaks + //options.set(HtmlRenderer.SOFT_BREAK, "
\n"); + + Parser parser = Parser.builder(options).build(); + HtmlRenderer renderer = HtmlRenderer.builder(options).build(); + + // You can re-use parser and renderer instances + Node document = parser.parse(content); + return renderer.render(document); + } +} diff --git a/src/main/java/com/raysmond/blog/services/PegDownMarkdownService.java b/src/main/java/com/raysmond/blog/support/web/impl/PegDownMarkdownService.java similarity index 61% rename from src/main/java/com/raysmond/blog/services/PegDownMarkdownService.java rename to src/main/java/com/raysmond/blog/support/web/impl/PegDownMarkdownService.java index 9e57d9f..313d298 100644 --- a/src/main/java/com/raysmond/blog/services/PegDownMarkdownService.java +++ b/src/main/java/com/raysmond/blog/support/web/impl/PegDownMarkdownService.java @@ -1,15 +1,17 @@ -package com.raysmond.blog.services; +package com.raysmond.blog.support.web.impl; -import com.raysmond.blog.support.web.PygmentsVerbatimSerializer; import org.pegdown.*; import org.pegdown.ast.RootNode; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; import java.util.Collections; +import com.raysmond.blog.support.web.MarkdownService; + /** - * Created by Raysmond on 9/25/15. + * @author Raysmond */ @Service @Qualifier("pegdown") @@ -18,16 +20,19 @@ public class PegDownMarkdownService implements MarkdownService { private final PegDownProcessor pegdown; public PegDownMarkdownService() { - pegdown = new PegDownProcessor(Extensions.ALL); + pegdown = new PegDownProcessor(Extensions.ALL ^ Extensions.EXTANCHORLINKS); } @Override public String renderToHtml(String markdownSource) { + if (StringUtils.isEmpty(markdownSource)) { + return null; + } // synchronizing on pegdown instance since neither the processor nor the underlying parser is thread-safe. - synchronized (pegdown) { + synchronized(pegdown) { RootNode astRoot = pegdown.parseMarkdown(markdownSource.toCharArray()); - ToHtmlSerializer serializer = new ToHtmlSerializer(new LinkRenderer(), - Collections.singletonMap(VerbatimSerializer.DEFAULT, PygmentsVerbatimSerializer.INSTANCE)); + ToHtmlSerializer serializer = new ToHtmlSerializer(new LinkRenderer()); + // Collections.singletonMap(VerbatimSerializer.DEFAULT, PygmentsVerbatimSerializer.INSTANCE)); return serializer.toHtml(astRoot); } } diff --git a/src/main/java/com/raysmond/blog/utils/DTOUtil.java b/src/main/java/com/raysmond/blog/utils/DTOUtil.java new file mode 100644 index 0000000..9ccb014 --- /dev/null +++ b/src/main/java/com/raysmond/blog/utils/DTOUtil.java @@ -0,0 +1,40 @@ +package com.raysmond.blog.utils; + +import org.modelmapper.ModelMapper; +import org.modelmapper.convention.MatchingStrategies; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Raysmond + */ +public class DTOUtil { + + private static ModelMapper MAPPER = null; + + private static ModelMapper getMapper() { + if (MAPPER == null) { + MAPPER = new ModelMapper(); + MAPPER.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT); + } + + return MAPPER; + } + + public static T map(S source, Class targetClass) { + return getMapper().map(source, targetClass); + } + + public static void mapTo(S source, T dist) { + getMapper().map(source, dist); + } + + public static List mapList(List source, Class targetClass) { + List list = new ArrayList<>(); + for (S s : source) { + list.add(getMapper().map(s, targetClass)); + } + return list; + } +} diff --git a/src/main/java/com/raysmond/blog/utils/Markdown.java b/src/main/java/com/raysmond/blog/utils/Markdown.java new file mode 100644 index 0000000..9adc711 --- /dev/null +++ b/src/main/java/com/raysmond/blog/utils/Markdown.java @@ -0,0 +1,18 @@ +package com.raysmond.blog.utils; + +import com.raysmond.blog.support.web.MarkdownService; +import com.raysmond.blog.support.web.impl.PegDownMarkdownService; + +/** + * A Markdown processing util class + * + * @author Raysmond + */ +public class Markdown { + + private static final MarkdownService MARKDOWN_SERVICE = new PegDownMarkdownService(); + + public static String markdownToHtml(String content) { + return MARKDOWN_SERVICE.renderToHtml(content); + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..5e9705d --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,39 @@ +spring: + profiles: + active: dev + + thymeleaf: + cache: false + + devtools: + restart: + enabled: true + exclude: static/**,templates/** + + jade4j: + caching: false + + jpa: + show-sql: true + + datasource: + type: com.zaxxer.hikari.HikariDataSource + url: jdbc:mysql://127.0.0.1/spring_blog_dev?useUnicode=true&characterEncoding=utf8&useSSL=false + username: root + password: + + hikari: + data-source-properties: + cachePrepStmts: true + prepStmtCacheSize: 250 + prepStmtCacheSqlLimit: 2048 + useServerPrepStmts: true + redis: + host: localhost + port: 6379 + default_expire_time: 86400 + +logging: + level: + org.springframework.web: ERROR + com.raysmond.blog: DEBUG diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 0000000..646bb66 --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,38 @@ +spring: + profiles: + active: prod + devtools: + restart: + enabled: false + + thymeleaf: + cache: true + + jade4j: + caching: true + + jpa: + show-sql: true + + datasource: + type: com.zaxxer.hikari.HikariDataSource + url: jdbc:mysql://127.0.0.1/spring_blog_prod?useUnicode=true&characterEncoding=utf8&useSSL=false + username: root + password: + + hikari: + data-source-properties: + cachePrepStmts: true + prepStmtCacheSize: 250 + prepStmtCacheSqlLimit: 2048 + useServerPrepStmts: true + + redis: + host: redis + port: 6379 + default_expire_time: 86400 + +logging: + level: + org.springframework.web: ERROR + com.raysmond.blog: DEBUG \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..27b8350 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,40 @@ +server: + port: 8080 + +spring: + jpa: + database-platform: org.hibernate.dialect.MySQL5InnoDBDialect + database: MYSQL + show-sql: true + generate-ddl: true + hibernate: + ddl-auto: update + dialect: org.hibernate.dialect.MySQL5Dialect + properties: + hibernate: + generate_statistics: true + cache: + use_second_level_cache: true + use_query_cache: true + region: + factory_class: org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory + + thymeleaf: + mode: LEGACYHTML5 + + cache: + ehcache: + config: classpath:ehcache.xml + +logging: + level: + org.springframework.web: ERROR + com.raysmond.blog: DEBUG + +# Redisson configuration file +redisson-config: conf/redisson.yaml + +management: + security: + enabled: true + roles: ROLE_ADMIN \ No newline at end of file diff --git a/src/main/resources/ehcache.xml b/src/main/resources/ehcache.xml new file mode 100644 index 0000000..e21c829 --- /dev/null +++ b/src/main/resources/ehcache.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/webapp/i18n/messages.properties b/src/main/resources/i18n/messages.properties similarity index 83% rename from src/main/webapp/i18n/messages.properties rename to src/main/resources/i18n/messages.properties index 1b5d755..d51250f 100644 --- a/src/main/webapp/i18n/messages.properties +++ b/src/main/resources/i18n/messages.properties @@ -1,5 +1,6 @@ view.index.title=Home page signup.success=Congratulations {0}! You have successfully signed up. +signin.already=You have already signed in. # Validation messages notBlank.message = The value may not be empty! diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml deleted file mode 100644 index 41b38cb..0000000 --- a/src/main/resources/logback.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - SpringBlog - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{32} - %msg%n - - - - - - - - - - - diff --git a/src/main/resources/persistence.properties b/src/main/resources/persistence.properties deleted file mode 100644 index d4ee907..0000000 --- a/src/main/resources/persistence.properties +++ /dev/null @@ -1,8 +0,0 @@ -dataSource.driverClassName=com.mysql.jdbc.Driver -dataSource.url=jdbc:mysql://127.0.0.1/spring_blog -dataSource.username=root -dataSource.password= - -hibernate.dialect=org.hibernate.dialect.MySQLDialect -hibernate.hbm2ddl.auto=update -hibernate.show_sql=true \ No newline at end of file diff --git a/src/main/resources/static/css/admin/admin.css b/src/main/resources/static/css/admin/admin.css new file mode 100644 index 0000000..43134c3 --- /dev/null +++ b/src/main/resources/static/css/admin/admin.css @@ -0,0 +1,142 @@ +body { + color: #434343; + font-size: 14px; +} + +.main-container { + padding-top: 60px; +} + +form .item-row { + margin-top: 10px; +} + +td.operations, th.operations { + text-align: center; + width: 120px; +} + +td.operations a { + margin-left: 5px; +} + +.post-form textarea { + height: 350px; +} + +.post-form #content-editor { + height: 430px; + width: 100%; +} +.post-form #summary-editor { + height: 230px; + width: 100%; +} + +.settings-form tbody td.setting-label { + width: 250px; + text-align: right; + padding-right: 20px; + vertical-align: middle; + font-weight: bold; +} + +.profile .row .col-sm-2 { + font-weight: bold; + text-align: right; + padding-right: 30px; +} + +.feather { + width: 16px; + height: 16px; + vertical-align: text-bottom; +} + +/* + * Sidebar + */ + +.sidebar { + position: fixed; + top: 0; + bottom: 0; + left: 0; + z-index: 100; /* Behind the navbar */ + padding: 0; + box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1); +} + +.sidebar-sticky { + position: -webkit-sticky; + position: sticky; + top: 48px; /* Height of navbar */ + height: calc(100vh - 48px); + padding-top: .5rem; + overflow-x: hidden; + overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ +} + +.sidebar .nav-link { + font-weight: 500; + color: #333; +} + +.sidebar .nav-link .feather { + margin-right: 4px; + color: #999; +} + +.sidebar .nav-link.active { + color: #007bff; +} + +.sidebar .nav-link:hover .feather, +.sidebar .nav-link.active .feather { + color: inherit; +} + +.sidebar-heading { + font-size: .75rem; + text-transform: uppercase; +} + +/* + * Navbar + */ +.navbar-brand { + padding-top: .75rem; + padding-bottom: .75rem; + font-size: 1rem; + background-color: rgba(0, 0, 0, .25); + box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25); +} + +.navbar .form-control { + padding: .75rem 1rem; + border-width: 0; + border-radius: 0; +} + +.form-control-dark { + color: #fff; + background-color: rgba(255, 255, 255, .1); + border-color: rgba(255, 255, 255, .1); +} + +.form-control-dark:focus { + border-color: transparent; + box-shadow: 0 0 0 3px rgba(255, 255, 255, .25); +} + +/* + * Utilities + */ + +.border-top { + border-top: 1px solid #e5e5e5; +} + +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} \ No newline at end of file diff --git a/src/main/resources/static/css/bootstrap4-theme.css b/src/main/resources/static/css/bootstrap4-theme.css new file mode 100644 index 0000000..22b5c83 --- /dev/null +++ b/src/main/resources/static/css/bootstrap4-theme.css @@ -0,0 +1,124 @@ +html { + font-size: 14px; + color: #444; +} + +body { + color: #444; +} + +@media (min-width: 768px) { + html { + font-size: 14px; + } +} + +.container { + max-width: 900px; +} + +.main { + background: #FBFBFB; +} + +.logo a { + color: #444; +} + +.pricing-header { + max-width: 700px; +} + +.card-deck .card { + min-width: 220px; +} + +.border-top { + border-top: 1px solid #e5e5e5; +} + +.border-bottom { + border-bottom: 1px solid #e5e5e5; +} + +.box-shadow { + box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); +} + +a { + color: #409eff; +} + +a:hover { + text-decoration: none; +} + +/* topbar */ +nav.topbar > a { + color: #444; +} + +nav.topbar > a:focus, nav.topbar > a:hover, nav.topbar > a:active { + color: #409eff; +} + +/* post */ +.blog-post a.post-title > h2 { + color: #444; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: 300; +} + +blockquote { + padding: 20px; + margin: 20px 0; + border: 1px solid #afafaf; + border-left-width: 5px; + border-radius: 3px; +} + +h1.blog-post-title { + font-weight: 500; +} + +.post-content h1 > a, +.post-content h2 > a, +.post-content h3 > a, +.post-content h4 > a, +.post-content h5 > a, +.post-content h6 > a { + color: #444; +} + +.post-content h2 { + border-bottom: 1px #e5e5e5 solid; + padding-bottom: 10px; + margin-bottom: 15px; +} + +.post-content { + line-height: 1.7; +} + +.post-content img { + max-width: 100%; +} + +code { + color: #292e31; + border-radius: 3px; + border: 1px solid #e5e5e5; + background: #fafafa; + padding: 2px 3px; +} + +#post-contents ol { + padding-left: 20px; +} + +#post-contents ol li a { + color: #444; + font-size: 13px; +} \ No newline at end of file diff --git a/src/main/resources/static/css/theme-light.css b/src/main/resources/static/css/theme-light.css new file mode 100644 index 0000000..6cb93c2 --- /dev/null +++ b/src/main/resources/static/css/theme-light.css @@ -0,0 +1,277 @@ +html { + font-size: 100%; +} + +body { + background: #fff; + /*font: 300 1em/1.8 "Helvetica Neue", Helvetica, Microsoft Yahei, Hiragino Sans GB, Microsoft Sans Serif, WenQuanYi Micro Hei, sans;*/ + font: 300 1em/1.8 -apple-system,SF UI Text,Arial,PingFang SC,Hiragino Sans GB,Microsoft YaHei,WenQuanYi Micro Hei,sans-serif;; + -webkit-font-smoothing: antialiased; + color: #515862; +} + +a { + -webkit-transition-property: opacity; + -moz-transition-property: opacity; + -o-transition-property: opacity; + transition-property: opacity; + -webkit-transition: all 0.3s ease; + -moz-transition: all 0.3s ease; + -o-transition: all 0.3s ease; + transition: all 0.3s ease; +} + +/* override typo css */ +h1, h2, h3, h4, h5, h6 { + color: #47596D; + -webkit-font-smoothing: normal; +} + +.typo a { + color: #337ab7; +} + +.typo a:hover { + color: #666; +} + +hr { + border-color: #eee; +} + +/* end */ + +pre { + background: #F9F9F9; + border-radius: 3px; + border-color: #eee; + font-size: 0.8em; + line-height: 1.55; + overflow: auto; + word-break: keep-all; + word-wrap: normal; +} + +a.btn-default { + color: #888; +} + +a.btn-default:hover { + background: #fff; + color: #333; + border-color: #333; +} + +.navbar { + z-index: 100; +} + +.navbar a.navbar-brand { + color: #9caab0; + font-weight: bold; +} + +.navbar .container { + border-bottom: 1px #eee solid; + padding-top: 10px; +} + +.navbar li a { + color: #9caab0; + text-transform: uppercase; + font-weight: bold; + font-size: 0.9em; +} + +.nav > li > a:focus, .nav > li > a:hover { + background: none; + color: #888; +} + +@media screen and (max-width: 480px) { + #navbar { + text-align: center; + } + + .nav > li { + display: inline-block; + } +} + +.container { + padding-top: 20px; + max-width: 840px; +} + +.main-container { + padding-top: 0; +} + +form input, form button { + margin-top: 10px; +} + +.header-title { + padding: 0 100px; + width: 100%; + position: relative; +} + +.header-title h1 { + margin-top: 20px; +} + +.header-title p { + font-size: 0.9em; + +} + +.header-title p.meta { + color: #999; + font-size: 0.9em; +} + +.header-title .avatar img { + margin-top: 20px; + border-radius: 50%; + box-shadow: 0 0 0 3px #fff, 0 0 0 4px #ccc; + width: 80px; + height: 80px; +} + +.header-title-wrapper { + display: table-cell; + vertical-align: middle; + margin: 0 auto; + text-align: center; + width: 100%; + display: block; +} + +.post { + padding: 50px 80px; + margin-bottom: 30px; +} + +@media screen and (max-width: 480px) { + .post { + padding: 30px 10px; + } + + .post h2.title, .post .meta { + text-align: center; + } +} + +.post .meta, .typo .post .meta { + color: rgba(187, 187, 187, 0.8); + font-size: 0.9em; +} + +.post h1 > a, .post h2 > a, .post h3 > a, .post h4 > a { + color: #75889E; + text-decoration: none; +} + +.post h2.title { + margin-bottom: 20px; + margin-top: 15px; +} + +.post h2.title a:hover, .post h2.title a:focus { + color: #888; +} + +.post .info { + margin-top: 30px; + text-align: center; + color: #888; + font-size: 0.9em; +} + +.post .comments { + margin-top: 30px; + padding-top: 30px; + border-top: 1px #eee solid; +} + +.post ul.tags { + margin-left: 0; + padding-left: 0; +} + +.post ul.tags li { + display: inline-block; + list-style: none; + margin-left: 10px; +} + +.latest-posts { + width: 80%; + margin: 0 auto; + margin-top: 30px; + margin-bottom: 50px; +} + +.loadmore { + text-align: center; + margin-top: 40px; +} + +.post-item { + text-align: center; + width: 80%; + margin: 10px auto; + padding: 10px 20px; +} + +.post-item:hover { + background: #F2F8FB; +} + +.post-item a > h2 { + font-size: 1.1em; + margin-top: 0; + color: #515862; + font-weight: 400; +} + +.post-item .meta { + text-align: center; + color: #888; + font-size: 0.9em; +} + +ul.archive-list li span.month { + width: 60px; + display: inline-block; +} + +ul.tags li { + margin: 5px 10px; +} + +.footer { + text-align: center; + margin-bottom: 50px; + font-size: 0.9em; + color: #888; +} + +.footer a { + color: #888; +} + +.footer p, .typo .footer p { + margin-bottom: 0.4em; +} + +.footer-wrapper { + display: block; + max-width: 500px; + margin: 0 auto; +} + +.footer-wrapper hr { + border-color: #ddd; +} diff --git a/src/main/webapp/resources/css/theme.css b/src/main/resources/static/css/theme.css similarity index 52% rename from src/main/webapp/resources/css/theme.css rename to src/main/resources/static/css/theme.css index 146a2d3..2a59b18 100644 --- a/src/main/webapp/resources/css/theme.css +++ b/src/main/resources/static/css/theme.css @@ -1,15 +1,29 @@ +html{ + font-size: 100%; +} + body { - -webkit-font-smoothing: antialiased; - color: #434343; background: #efefef; - line-height: 1.6; + font: 300 1em/1.8 "Helvetica Neue",Helvetica, Microsoft Yahei, Hiragino Sans GB, Microsoft Sans Serif, WenQuanYi Micro Hei, sans; + /*-webkit-font-smoothing: antialiased; */ } +/* override typo css */ +.typo a{ + color: #337ab7; +} + +hr{ + border-color: #eee; +} + +/* end */ + pre{ background: #F9F9F9; - border-radius: 1px; - border: none; - font-size: 12px; + border-radius: 3px; + border-color: #eee; + font-size: 0.8em; line-height: 1.55; overflow: auto; word-break: keep-all; @@ -22,6 +36,7 @@ pre{ .navbar li a{ color: #eee; + font-size: 0.8em; } .nav>li>a:focus, .nav>li>a:hover{ @@ -29,6 +44,15 @@ pre{ color: #fff; } +@media screen and (max-width: 480px) { + #navbar{ + text-align: center; + } + .nav>li{ + display: inline-block; + } +} + .main-container{ margin-top: 330px; padding-top: 60px; @@ -49,7 +73,15 @@ form input, form button{ display: table; background-color: #159957; background-image: linear-gradient(120deg, #155799, #159957); +} + +.header-title h1 { + color: #fff; +} +.header-title p{ + font-size: 0.9em; + color: #eee; } .header-title-wrapper{ @@ -71,9 +103,19 @@ form input, form button{ border-radius: 3px; } +@media screen and (max-width: 480px) { + .post{ + padding: 30px 10px; + } + .post h2.title, .post .meta{ + text-align: center; + } +} + -.post .meta{ +.post .meta, .typo .post .meta{ color: rgba(187,187,187,0.8); + font-size: 0.9em; } .post h1 > a, .post h2 > a, .post h3 > a, .post h4 > a{ @@ -83,24 +125,55 @@ form input, form button{ .post h2.title{ margin-bottom: 20px; + margin-top: 15px; } -.post h2.title a{ + +.post h2.title a:hover, .post h2.title a:focus{ color: #555; - text-decoration: none; } -.post h2.title a:hover, .post h2.title a:focus{ - color: #333; +.post .info{ + margin-top: 30px; + text-align: center; + color: #888; + font-size: 0.9em; } .post .comments{ - margin-top: 80px; + margin-top: 30px; + padding-top: 30px; + border-top: 1px #eee solid; +} + +.post ul.tags{ + margin-left: 0; + padding-left: 0; +} + +.post ul.tags li{ + display: inline-block; + list-style: none; + margin-left: 10px; +} + +ul.archive-list li span.month{ + width: 60px; + display: inline-block; +} + +ul.tags li{ + margin: 5px 10px; } .footer{ text-align: center; margin-bottom: 50px; + font-size: 0.9em; +} + +.footer p, .typo .footer p { + margin-bottom: 0.4em; } .footer-wrapper{ diff --git a/src/main/resources/static/images/user.jpg b/src/main/resources/static/images/user.jpg new file mode 100644 index 0000000..21f0e9e Binary files /dev/null and b/src/main/resources/static/images/user.jpg differ diff --git a/src/main/webapp/resources/vendors/bootstrap-paginator-1.0.2/LICENSE b/src/main/resources/static/vendors/bootstrap-paginator-1.0.2/LICENSE similarity index 100% rename from src/main/webapp/resources/vendors/bootstrap-paginator-1.0.2/LICENSE rename to src/main/resources/static/vendors/bootstrap-paginator-1.0.2/LICENSE diff --git a/src/main/webapp/resources/vendors/bootstrap-paginator-1.0.2/README.md b/src/main/resources/static/vendors/bootstrap-paginator-1.0.2/README.md similarity index 100% rename from src/main/webapp/resources/vendors/bootstrap-paginator-1.0.2/README.md rename to src/main/resources/static/vendors/bootstrap-paginator-1.0.2/README.md diff --git a/src/main/resources/static/vendors/bootstrap-paginator-1.0.2/js/bootstrap-paginator.min.js b/src/main/resources/static/vendors/bootstrap-paginator-1.0.2/js/bootstrap-paginator.min.js new file mode 100644 index 0000000..440df04 --- /dev/null +++ b/src/main/resources/static/vendors/bootstrap-paginator-1.0.2/js/bootstrap-paginator.min.js @@ -0,0 +1 @@ +!function($){"use strict";var BootstrapPaginator=function(element,options){this.init(element,options)},old=null;BootstrapPaginator.prototype={init:function(element,options){this.$element=$(element);var version=options&&options.bootstrapMajorVersion?options.bootstrapMajorVersion:$.fn.bootstrapPaginator.defaults.bootstrapMajorVersion,id=this.$element.attr("id");if(2===version&&!this.$element.is("div"))throw"in Bootstrap version 2 the pagination must be a div element. Or if you are using Bootstrap pagination 3. Please specify it in bootstrapMajorVersion in the option";if(version>2&&!this.$element.is("ul"))throw"in Bootstrap version 3 the pagination root item must be an ul element.";this.currentPage=1,this.lastPage=1,this.setOptions(options),this.initialized=!0},setOptions:function(options){this.options=$.extend({},this.options||$.fn.bootstrapPaginator.defaults,options),this.totalPages=parseInt(this.options.totalPages,10),this.numberOfPages=parseInt(this.options.numberOfPages,10),options&&"undefined"!=typeof options.currentPage&&this.setCurrentPage(options.currentPage),this.listen(),this.render(),this.initialized||this.lastPage===this.currentPage||this.$element.trigger("page-changed",[this.lastPage,this.currentPage])},listen:function(){this.$element.off("page-clicked"),this.$element.off("page-changed"),"function"==typeof this.options.onPageClicked&&this.$element.bind("page-clicked",this.options.onPageClicked),"function"==typeof this.options.onPageChanged&&this.$element.on("page-changed",this.options.onPageChanged),this.$element.bind("page-clicked",this.onPageClicked)},destroy:function(){this.$element.off("page-clicked"),this.$element.off("page-changed"),this.$element.removeData("bootstrapPaginator"),this.$element.empty()},show:function(page){this.setCurrentPage(page),this.render(),this.lastPage!==this.currentPage&&this.$element.trigger("page-changed",[this.lastPage,this.currentPage])},showNext:function(){var pages=this.getPages();pages.next&&this.show(pages.next)},showPrevious:function(){var pages=this.getPages();pages.prev&&this.show(pages.prev)},showFirst:function(){var pages=this.getPages();pages.first&&this.show(pages.first)},showLast:function(){var pages=this.getPages();pages.last&&this.show(pages.last)},onPageItemClicked:function(event){var type=event.data.type,page=event.data.page;this.$element.trigger("page-clicked",[event,type,page])},onPageClicked:function(event,originalEvent,type,page){var currentTarget=$(event.currentTarget);switch(type){case"first":currentTarget.bootstrapPaginator("showFirst");break;case"prev":currentTarget.bootstrapPaginator("showPrevious");break;case"next":currentTarget.bootstrapPaginator("showNext");break;case"last":currentTarget.bootstrapPaginator("showLast");break;case"page":currentTarget.bootstrapPaginator("show",page)}},render:function(){var containerClass=this.getValueFromOption(this.options.containerClass,this.$element),size=this.options.size||"normal",alignment=this.options.alignment||"left",pages=this.getPages(),listContainer=2===this.options.bootstrapMajorVersion?$("
    "):this.$element,listContainerClass=2===this.options.bootstrapMajorVersion?this.getValueFromOption(this.options.listContainerClass,listContainer):null,first=null,prev=null,next=null,last=null,p=null,i=0;switch(this.$element.prop("class",""),this.$element.addClass("pagination"),size.toLowerCase()){case"large":case"small":case"mini":this.$element.addClass($.fn.bootstrapPaginator.sizeArray[this.options.bootstrapMajorVersion][size.toLowerCase()])}if(2===this.options.bootstrapMajorVersion)switch(alignment.toLowerCase()){case"center":this.$element.addClass("pagination-centered");break;case"right":this.$element.addClass("pagination-right")}for(this.$element.addClass(containerClass),this.$element.empty(),2===this.options.bootstrapMajorVersion&&(this.$element.append(listContainer),listContainer.addClass(listContainerClass)),this.pageRef=[],pages.first&&(first=this.buildPageItem("first",pages.first),first&&listContainer.append(first)),pages.prev&&(prev=this.buildPageItem("prev",pages.prev),prev&&listContainer.append(prev)),i=0;i"),itemContent=$(""),text="",title="",itemContainerClass=this.options.itemContainerClass(type,page,this.currentPage),itemContentClass=this.getValueFromOption(this.options.itemContentClass,type,page,this.currentPage),tooltipOpts=null;switch(type){case"first":if(!this.getValueFromOption(this.options.shouldShowPage,type,page,this.currentPage))return;text=this.options.itemTexts(type,page,this.currentPage),title=this.options.tooltipTitles(type,page,this.currentPage);break;case"last":if(!this.getValueFromOption(this.options.shouldShowPage,type,page,this.currentPage))return;text=this.options.itemTexts(type,page,this.currentPage),title=this.options.tooltipTitles(type,page,this.currentPage);break;case"prev":if(!this.getValueFromOption(this.options.shouldShowPage,type,page,this.currentPage))return;text=this.options.itemTexts(type,page,this.currentPage),title=this.options.tooltipTitles(type,page,this.currentPage);break;case"next":if(!this.getValueFromOption(this.options.shouldShowPage,type,page,this.currentPage))return;text=this.options.itemTexts(type,page,this.currentPage),title=this.options.tooltipTitles(type,page,this.currentPage);break;case"page":if(!this.getValueFromOption(this.options.shouldShowPage,type,page,this.currentPage))return;text=this.options.itemTexts(type,page,this.currentPage),title=this.options.tooltipTitles(type,page,this.currentPage)}return itemContainer.addClass(itemContainerClass).append(itemContent),itemContent.addClass(itemContentClass).html(text).on("click",null,{type:type,page:page},$.proxy(this.onPageItemClicked,this)),this.options.pageUrl&&itemContent.attr("href",this.getValueFromOption(this.options.pageUrl,type,page,this.currentPage)),this.options.useBootstrapTooltip?(tooltipOpts=$.extend({},this.options.bootstrapTooltipOptions,{title:title}),itemContent.tooltip(tooltipOpts)):itemContent.attr("title",title),itemContainer},setCurrentPage:function(page){if(page>this.totalPages||1>page)throw"Page out of range";this.lastPage=this.currentPage,this.currentPage=parseInt(page,10)},getPages:function(){var totalPages=this.totalPages,pageStart=0===this.currentPage%this.numberOfPages?(parseInt(this.currentPage/this.numberOfPages,10)-1)*this.numberOfPages+1:parseInt(this.currentPage/this.numberOfPages,10)*this.numberOfPages+1,output=[],i=0,counter=0;for(pageStart=1>pageStart?1:pageStart,i=pageStart,counter=0;counter=i;i+=1,counter+=1)output.push(i);return output.first=1,output.prev=this.currentPage>1?this.currentPage-1:1,output.next=this.currentPage ul` 这样结构的样式。 + +3、增加类: + +主要是一些需要中文日常排版需要的元素和语文对应样式的增强,目前包括: + +(1) 专名号:使用标签 `` 或者 `.typo-u`
    +(2) 着重号:使用 class `.typo-em`
    +(3) 清理浮动:与一般 reset.css 保持一致 `.clearfix`
    +(4) 强制换行:添加 `.textwrap` 到文本所在的容器,如果是 `table` 测还需要 `.textwrap-table`
    +(5) 衬线字体:添加 `.serif`
    +(6) 创建 border-box:在 html 中添加 `.borderbox` [#why](http://www.paulirish.com/2012/box-sizing-border-box-ftw/) + + +### 三、开源许可 +基于 [MIT License](http://zh.wikipedia.org/wiki/MIT_License) 开源,使用代码只需说明来源,或者引用 [license.txt](https://github.com/sofish/typo.css/blob/master/license.txt) 即可。 diff --git a/src/main/resources/static/vendors/typo.css-2.1.2/license.txt b/src/main/resources/static/vendors/typo.css-2.1.2/license.txt new file mode 100755 index 0000000..ed98731 --- /dev/null +++ b/src/main/resources/static/vendors/typo.css-2.1.2/license.txt @@ -0,0 +1,22 @@ +TYPO.CSS - a better way to manage the typography of your Chinese-lang-base site. + +Copyright (C) 2012 Sofish Lin http://sofish.de + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/src/main/resources/static/vendors/typo.css-2.1.2/typo.css b/src/main/resources/static/vendors/typo.css-2.1.2/typo.css new file mode 100755 index 0000000..a3b90ba --- /dev/null +++ b/src/main/resources/static/vendors/typo.css-2.1.2/typo.css @@ -0,0 +1,345 @@ +@charset "utf-8"; + +/* 防止用户自定义背景颜色对网页的影响,添加让用户可以自定义字体 */ +html { + color: #333; + background: #fff; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + text-rendering: optimizelegibility; +} + +/* 如果你的项目仅支持 IE9+ | Chrome | Firefox 等,推荐在 中添加 .borderbox 这个 class */ +html.borderbox *, html.borderbox *:before, html.borderbox *:after { + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +/* 内外边距通常让各个浏览器样式的表现位置不同 */ +body, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, code, form, fieldset, legend, input, textarea, p, blockquote, th, td, hr, button, article, aside, details, figcaption, figure, footer, header, menu, nav, section { + margin: 0; + padding: 0; +} + +/* 重设 HTML5 标签, IE 需要在 js 中 createElement(TAG) */ +article, aside, details, figcaption, figure, footer, header, menu, nav, section { + display: block; +} + +/* HTML5 媒体文件跟 img 保持一致 */ +audio, canvas, video { + display: inline-block; +} + +/* 要注意表单元素并不继承父级 font 的问题 */ +body, button, input, select, textarea { + font: 300 1em/1.8 PingFang SC, Lantinghei SC, Microsoft Yahei, Hiragino Sans GB, Microsoft Sans Serif, WenQuanYi Micro Hei, sans; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} + +/* 去掉各Table cell 的边距并让其边重合 */ +table { + border-collapse: collapse; + border-spacing: 0; +} + +/* 去除默认边框 */ +fieldset, img { + border: 0; +} + +/* 块/段落引用 */ +blockquote { + position: relative; + color: #999; + font-weight: 400; + border-left: 1px solid #1abc9c; + padding-left: 1em; + margin: 1em 3em 1em 2em; +} + +@media only screen and ( max-width: 640px ) { + blockquote { + margin: 1em 0; + } +} + +/* Firefox 以外,元素没有下划线,需添加 */ +acronym, abbr { + border-bottom: 1px dotted; + font-variant: normal; +} + +/* 添加鼠标问号,进一步确保应用的语义是正确的(要知道,交互他们也有洁癖,如果你不去掉,那得多花点口舌) */ +abbr { + cursor: help; +} + +/* 一致的 del 样式 */ +del { + text-decoration: line-through; +} + +address, caption, cite, code, dfn, em, th, var { + font-style: normal; + font-weight: 400; +} + +/* 去掉列表前的标识, li 会继承,大部分网站通常用列表来很多内容,所以应该当去 */ +ul, ol { + list-style: none; +} + +/* 对齐是排版最重要的因素, 别让什么都居中 */ +caption, th { + text-align: left; +} + +q:before, q:after { + content: ''; +} + +/* 统一上标和下标 */ +sub, sup { + font-size: 75%; + line-height: 0; + position: relative; +} + +:root sub, :root sup { + vertical-align: baseline; /* for ie9 and other modern browsers */ +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* 让链接在 hover 状态下显示下划线 */ +a { + color: #1abc9c; +} + +a:hover { + text-decoration: underline; +} + +.typo a { + border-bottom: 1px solid #1abc9c; +} + +.typo a:hover { + border-bottom-color: #555; + color: #555; + text-decoration: none; +} + +/* 默认不显示下划线,保持页面简洁 */ +ins, a { + text-decoration: none; +} + +/* 专名号:虽然 u 已经重回 html5 Draft,但在所有浏览器中都是可以使用的, + * 要做到更好,向后兼容的话,添加 class="typo-u" 来显示专名号 + * 关于 标签:http://www.whatwg.org/specs/web-apps/current-work/multipage/text-level-semantics.html#the-u-element + * 被放弃的是 4,之前一直搞错 http://www.w3.org/TR/html401/appendix/changes.html#idx-deprecated + * 一篇关于 标签的很好文章:http://html5doctor.com/u-element/ + */ +u, .typo-u { + text-decoration: underline; +} + +/* 标记,类似于手写的荧光笔的作用 */ +mark { + background: #fffdd1; + border-bottom: 1px solid #ffedce; + padding: 2px; + margin: 0 5px; +} + +/* 代码片断 */ +pre, code, pre tt { + font-family: Courier, 'Courier New', monospace; +} + +pre { + background: #f8f8f8; + border: 1px solid #ddd; + padding: 1em 1.5em; + display: block; + -webkit-overflow-scrolling: touch; +} + +/* 一致化 horizontal rule */ +hr { + border: none; + border-bottom: 1px solid #cfcfcf; + margin-bottom: 0.8em; + height: 10px; +} + +/* 底部印刷体、版本等标记 */ +small, .typo-small, + /* 图片说明 */ +figcaption { + font-size: 0.9em; + color: #888; +} + +strong, b { + font-weight: bold; + color: #000; +} + +/* 可拖动文件添加拖动手势 */ +[draggable] { + cursor: move; +} + +.clearfix:before, .clearfix:after { + content: ""; + display: table; +} + +.clearfix:after { + clear: both; +} + +.clearfix { + zoom: 1; +} + +/* 强制文本换行 */ +.textwrap, .textwrap td, .textwrap th { + word-wrap: break-word; + word-break: break-all; +} + +.textwrap-table { + table-layout: fixed; +} + +/* 提供 serif 版本的字体设置: iOS 下中文自动 fallback 到 sans-serif */ +.serif { + font-family: Palatino, Optima, Georgia, serif; +} + +/* 保证块/段落之间的空白隔行 */ +.typo p, .typo pre, .typo ul, .typo ol, .typo dl, .typo form, .typo hr, .typo table, +.typo-p, .typo-pre, .typo-ul, .typo-ol, .typo-dl, .typo-form, .typo-hr, .typo-table, blockquote { + margin-bottom: 1.2em +} + +h1, h2, h3, h4, h5, h6 { + font-family: PingFang SC, Verdana, Helvetica Neue, Microsoft Yahei, Hiragino Sans GB, Microsoft Sans Serif, WenQuanYi Micro Hei, sans-serif; + font-weight: 100; + color: #000; + line-height: 1.35; +} + +/* 标题应该更贴紧内容,并与其他块区分,margin 值要相应做优化 */ +.typo h1, .typo h2, .typo h3, .typo h4, .typo h5, .typo h6, +.typo-h1, .typo-h2, .typo-h3, .typo-h4, .typo-h5, .typo-h6 { + margin-top: 1.2em; + margin-bottom: 0.6em; + line-height: 1.35; +} + +.typo h1, .typo-h1 { + font-size: 2em; +} + +.typo h2, .typo-h2 { + font-size: 1.8em; +} + +.typo h3, .typo-h3 { + font-size: 1.6em; +} + +.typo h4, .typo-h4 { + font-size: 1.4em; +} + +.typo h5, .typo h6, .typo-h5, .typo-h6 { + font-size: 1.2em; +} + +/* 在文章中,应该还原 ul 和 ol 的样式 */ +.typo ul, .typo-ul { + margin-left: 1.3em; + list-style: disc; +} + +.typo ol, .typo-ol { + list-style: decimal; + margin-left: 1.9em; +} + +.typo li ul, .typo li ol, .typo-ul ul, .typo-ul ol, .typo-ol ul, .typo-ol ol { + margin-bottom: 0.8em; + margin-left: 2em; +} + +.typo li ul, .typo-ul ul, .typo-ol ul { + list-style: circle; +} + +/* 同 ul/ol,在文章中应用 table 基本格式 */ +.typo table th, .typo table td, .typo-table th, .typo-table td, .typo table caption { + border: 1px solid #ddd; + padding: 0.5em 1em; + color: #666; +} + +.typo table th, .typo-table th { + background: #fbfbfb; +} + +.typo table thead th, .typo-table thead th { + background: #f1f1f1; +} + +.typo table caption { + border-bottom: none; +} + +/* 去除 webkit 中 input 和 textarea 的默认样式 */ +.typo-input, .typo-textarea { + -webkit-appearance: none; + border-radius: 0; +} + +.typo-em, .typo em, legend, caption { + color: #000; + font-weight: inherit; +} + +/* 着重号,只能在少量(少于100个字符)且全是全角字符的情况下使用 */ +.typo-em { + position: relative; +} + +.typo-em:after { + position: absolute; + top: 0.65em; + left: 0; + width: 100%; + overflow: hidden; + white-space: nowrap; + content: "・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・"; +} + +/* Responsive images */ +.typo img { + max-width: 100%; +} diff --git a/src/main/resources/static/vendors/typo.css-2.1.2/typo.html b/src/main/resources/static/vendors/typo.css-2.1.2/typo.html new file mode 100755 index 0000000..54fc2a0 --- /dev/null +++ b/src/main/resources/static/vendors/typo.css-2.1.2/typo.html @@ -0,0 +1,430 @@ + + + + + + + + Typo.css - 中文网页重设与排版 + + + + +
    + + +
    + +
    + +

    中文网页重设与排版:Typo.css


    + +

    一致化浏览器排版效果,构建最适合中文阅读的网页排版

    + +
      +
    1. 关于 Typo.css
    2. +
    3. 排版实例 + +
    4. +
    5. 附录 + +
    6. +
    + +

    一、关于 Typo.css

    + +

    Typo.css 的目的是,在一致化浏览器排版效果的同时,构建最适合中文阅读的网页排版。

    +

    现状和如何去做:

    + +

    排版是一个麻烦的问题 # 附录一,需要精心设计,而这个设计却是常被视觉设计师所忽略的。前端工程师更常看到这样的问题,但不便变更。因为在多个 OS 中的不同浏览器渲染不同,改动需要多的时间做回归测试,所以改变变得更困难。而像我们一般使用的 + Yahoo、Eric Meyer 和 Alice base.css 中采用的 Reset 都没有很好地考虑中文排版。Typo.css 要做的就是解决中文排版的问题。

    + +

    Typo.css 测试于如下平台:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    OS/浏览器FirefoxChromeSafariOperaIE9IE8IE7IE6
    OS X----
    Win 7-
    Win XP-
    Ubuntu-----
    + +

    中文排版的重点和难点

    + +

    在中文排版中,HTML4 的很多标准在语义在都有照顾到。但从视觉效果上,却很难利用单独的 CSS 来实现,像着重号(例:这里强调一下)。在 HTML4 中,专名号标签(<u>)已经被放弃,而 + HTML5 被重新提起Typo.css 也根据实际情况提供相应的方案。我们重要要注意的两点是:

    +
      +
    1. 语义:语义对应的用法和样式是否与中文排版一致
    2. +
    3. 表现:在各浏览器中的字体、大小和缩放是否如排版预期
    4. +
    +

    对于这些,Typo.css 排版项目的中文偏重注意点,都添加在附录中,详见:

    +
    + 附录一Typo.css 排版偏重点 +
    + +

    目前已有像百姓网等全面使用 Typo.css 的项目,测试平台的覆盖,特别是在移动端上还没有覆盖完主流平台,希望有能力的同学能加入测试行列,或者加入到 Typo.css + 的开发。加入方法:参与 Typo.css 开发。如有批评、建议和意见,也随时欢迎给在 Github 直接提 issues,或给邮件

    + + +

    二、排版实例:

    + +

    提供2个排版实例,第一个中文实例来自于来自于张燕婴的《论语》,由于古文排版涉及到的元素比较多,所以采用《论语》中《学而》的第一篇作为排版实例介绍;第2个来自到经典的 + Lorem Ipsum,并加入了一些代码和列表等比较具有代表性的排版元素。

    + +

    例1:论语学而篇第一

    + +

    + 作者:孔子( + + ) + +

    + +

    本篇引语

    + +

    + 《学而》是《论语》第一篇的篇名。《论语》中各篇一般都是以第一章的前二三个字作为该篇的篇名。《学而》一篇包括16章,内容涉及诸多方面。其中重点是「吾日三省吾身」;「节用而爱人,使民以时」;「礼之用,和为贵」以及仁、孝、信等道德范畴。

    + +

    原文

    + +

    子曰:「学而时习之,不亦说乎?有朋自远方来,不亦乐乎?人不知,而不愠,不亦君子乎?」

    + +

    译文

    + +

    孔子说:「学了又时常温习和练习,不是很愉快吗?有志同道合的人从远方来,不是很令人高兴的吗?人家不了解我,我也不怨恨、恼怒,不也是一个有德的君子吗?」

    + +

    评析

    + +

    宋代著名学者朱熹对此章评价极高,说它是「入道之门,积德之基」。本章这三句话是人们非常熟悉的。历来的解释都是:学了以后,又时常温习和练习,不也高兴吗等等。三句话,一句一个意思,前后句子也没有什么连贯性。但也有人认为这样解释不符合原义,指出这里的「学」不是指学习,而是指学说或主张;「时」不能解为时常,而是时代或社会的意思,「习」不是温习,而是使用,引申为采用。而且,这三句话不是孤立的,而是前后相互连贯的。这三句的意思是:自己的学说,要是被社会采用了,那就太高兴了;退一步说,要是没有被社会所采用,可是很多朋友赞同的学说,纷纷到我这里来讨论问题,我也感到快乐;再退一步说,即使社会不采用,人们也不理解我,我也不怨恨,这样做,不也就是君子吗?(见《齐鲁学刊》1986年第6期文)这种解释可以自圆其说,而且也有一定的道理,供读者在理解本章内容时参考。 +

    + +

    此外,在对「人不知,而不愠」一句的解释中,也有人认为,「人不知」的后面没有宾语,人家不知道什么呢?当时因为孔子有说话的特定环境,他不需要说出知道什么,别人就可以理解了,却给后人留下一个谜。有人说,这一句是接上一句说的,从远方来的朋友向我求教,我告诉他,他还不懂,我却不怨恨。这样,「人不知」就是「人家不知道我所讲述的」了。这样的解释似乎有些牵强。

    + +

    总之,本章提出以学习为乐事,做到人不知而不愠,反映出孔子学而不厌、诲人不倦、注重修养、严格要求自己的主张。这些思想主张在《论语》书中多处可见,有助于对第一章内容的深入了解。

    + +

    例2:英文排版

    + +

    Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's + standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a + type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining + essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum + passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem + Ipsum.

    +
    + Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +
    + +

    The standard Lorem Ipsum passage, used since the 1500s

    + +

    "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna + aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint + occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."

    + +

    Section 1.10.32 of "de Finibus Bonorum et Malorum", written by Cicero in 45 BC

    + +

    "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, + eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam + voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione + voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci + velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim + ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi + consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, + vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"

    + +

    List style in action

    +
      +
    • If you wish to succeed, you should use persistence as your good friend, experience as your reference, prudence as + your brother and hope as your sentry. +

      如果你希望成功,当以恒心为良友,以经验为参谋,以谨慎为兄弟,以希望为哨兵。

      +
    • +
    • Sometimes one pays most for the things one gets for nothing. +

      有时候一个人为不花钱得到的东西付出的代价最高。

      +
    • +
    • Only those who have the patience to do simple things perfectly ever acquire the skill to do difficult things + easily. +

      只有有耐心圆满完成简单工作的人,才能够轻而易举的完成困难的事。

      +
    • +
    + +

    You may want to create a perfect <hr /> line, despite the fact that there will never have one +

    +
    +

    La Racheforcauld said: + "Few things are impossible in themselves; and it is often for want of will, rather than of means, that man fails + to succeed". + + You just need to follow the browser's behavior, and set a right margin to it。it will works nice as the + demo you're watching now. The following code is the best way to render typo in Chinese: +

    +
    +/* 标题应该更贴紧内容,并与其他块区分,margin 值要相应做优化 */
    +h1,h2,h3,h4,h5,h6 {
    +    line-height:1;font-family:Arial,sans-serif;margin:1.4em 0 0.8em;
    +}
    +h1{font-size:1.8em;}
    +h2{font-size:1.6em;}
    +h3{font-size:1.4em;}
    +h4{font-size:1.2em;}
    +h5,h6{font-size:1em;}
    +
    +/* 现代排版:保证块/段落之间的空白隔行 */
    +.typo p, .typo pre, .typo ul, .typo ol, .typo dl, .typo form, .typo hr {
    +    margin:1em 0 0.6em;
    +}
    +
    + +

    三、附录

    + +
    1、Typo.css 排版偏重点
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    类型语义标签注意点
    基础标签标题h1h6全局不强制大小,.typo 中标题与其对应的内容应紧贴,并且有相应的大小设置
    上、下标sup/sub保持与 MicroSoft Office Word 等程序的日常排版一致
    引用blockquote显示/嵌套样式
    缩写abbr是否都有下划线,鼠标 hover 是否为帮助手势
    分割线hr显示的 paddingmargin正确
    列表ul/ol/dl在全局没有 list-style,在 .typo 中对齐正确
    定义列表dl全局 paddingmargin为0, .typo 中对齐正确
    选项input[type=radio[, checkbox]]与其他 form 元素排版时是否居中
    斜体i只设置一种斜体,让 emcite 显示为正体
    强调em在全局显示正体,在 .typo 中显示与 bstrong 的样式一致,为粗体
    加强strong/b显示为粗体
    标记mark类似荧光笔
    印刷small保持为正确字体的 80% 大小,颜色设置为浅灰色
    表格table全局不显示线条,在 table 中显示表格外框,并且表头有浅灰背景
    代码pre/code字体使用 courier 系字体,保持与 serif 有比较一致的显示效果
    特殊符号着重号在文字下加点在支持 :after:before 的浏览器可以做渐进增强实现
    专名号林建锋专名号,有下划线,使用 u 或都 .typo-u
    破折号——保持一划,而非两划
    人民币¥使用两平等线的符号,或者 HTML 实体符号 &yen;
    删除符 + 已删除(deleted) + 一致化各浏览器显示,中英混排正确
    加强类专名号.typo-u由于 u 被 HTML4 放弃,在向后兼容上推荐使用 .typo-u
    着重符.typo-em利用 :after:before 实现着重符
    清除浮动.clearfix与一般 CSS Reset 保持一对致 API
    注意点(1)中英文混排行高/行距
    (2)上下标在 IE 中显示效果
    (3)块/段落分割空白是否符合设计原则
    (4)input 多余空间问题
    (5)默认字体色彩,目前采用 #333 在各种浏览显示比较好
    + +
    2、开源许可
    + +

    Typo.css 基于 MIT License 开源,使用代码只需说明来源,或者引用 license.txt 即可。

    +
    + + + Fork me on GitHub + + + + diff --git a/src/main/resources/static/vendors/typo.css-2.1.2/typo.min.css b/src/main/resources/static/vendors/typo.css-2.1.2/typo.min.css new file mode 100644 index 0000000..9a83587 --- /dev/null +++ b/src/main/resources/static/vendors/typo.css-2.1.2/typo.min.css @@ -0,0 +1 @@ +@charset "utf-8";html{color: #333;background: #fff;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;text-rendering: optimizelegibility}html.borderbox *, html.borderbox *:before, html.borderbox *:after{-moz-box-sizing: border-box;-webkit-box-sizing: border-box;box-sizing: border-box}body, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, code, form, fieldset, legend, input, textarea, p, blockquote, th, td, hr, button, article, aside, details, figcaption, figure, footer, header, menu, nav, section{margin: 0;padding: 0}article, aside, details, figcaption, figure, footer, header, menu, nav, section{display: block}audio, canvas, video{display: inline-block}body, button, input, select, textarea{font: 300 1em/1.8 PingFang SC, Lantinghei SC, Microsoft Yahei, Hiragino Sans GB, Microsoft Sans Serif, WenQuanYi Micro Hei, sans}button::-moz-focus-inner,input::-moz-focus-inner{padding: 0;border: 0}table{border-collapse: collapse;border-spacing: 0}fieldset, img{border: 0}blockquote{position: relative;color: #999;font-weight: 400;border-left: 1px solid #1abc9c;padding-left: 1em;margin: 1em 3em 1em 2em}@media only screen and ( max-width: 640px ){blockquote{margin: 1em 0}}acronym, abbr{border-bottom: 1px dotted;font-variant: normal}abbr{cursor: help}del{text-decoration: line-through}address, caption, cite, code, dfn, em, th, var{font-style: normal;font-weight: 400}ul, ol{list-style: none}caption, th{text-align: left}q:before, q:after{content: ''}sub, sup{font-size: 75%;line-height: 0;position: relative}:root sub, :root sup{vertical-align: baseline}sup{top: -0.5em}sub{bottom: -0.25em}a{color: #1abc9c}a:hover{text-decoration: underline}.typo a:hover{color: #555;text-decoration: none}ins, a{text-decoration: none}u, .typo-u{text-decoration: underline}mark{background: #fffdd1;border-bottom: 1px solid #ffedce;padding: 2px;margin: 0 5px}pre, code, pre tt{font-family: Courier, 'Courier New', monospace}pre{background: #f8f8f8;border: 1px solid #ddd;padding: 1em 1.5em;display: block;-webkit-overflow-scrolling: touch}hr{border: none;border-bottom: 1px solid #cfcfcf;margin-bottom: 0.8em;height: 10px}small, .typo-small,figcaption{font-size: 0.9em;color: #888}strong, b{font-weight: bold;color: #000}[draggable]{cursor: move}.clearfix:before, .clearfix:after{content: "";display: table}.clearfix:after{clear: both}.clearfix{zoom: 1}.textwrap, .textwrap td, .textwrap th{word-wrap: break-word;word-break: break-all}.textwrap-table{table-layout: fixed}.serif{font-family: Palatino, Optima, Georgia, serif}.typo p, .typo pre, .typo ul, .typo ol, .typo dl, .typo form, .typo hr, .typo table,.typo-p, .typo-pre, .typo-ul, .typo-ol, .typo-dl, .typo-form, .typo-hr, .typo-table, blockquote{margin-bottom: 1.2em}h1, h2, h3, h4, h5, h6{font-family: PingFang SC, Verdana, Helvetica Neue, Microsoft Yahei, Hiragino Sans GB, Microsoft Sans Serif, WenQuanYi Micro Hei, sans-serif;font-weight: 100;color: #000;line-height: 1.35}.typo h1, .typo h2, .typo h3, .typo h4, .typo h5, .typo h6,.typo-h1, .typo-h2, .typo-h3, .typo-h4, .typo-h5, .typo-h6{margin-top: 1.2em;margin-bottom: 0.6em;line-height: 1.35}.typo h1, .typo-h1{font-size: 2em}.typo h2, .typo-h2{font-size: 1.8em}.typo h3, .typo-h3{font-size: 1.6em}.typo h4, .typo-h4{font-size: 1.4em}.typo h5, .typo h6, .typo-h5, .typo-h6{font-size: 1.2em}.typo ul, .typo-ul{margin-left: 1.3em;list-style: disc}.typo ol, .typo-ol{list-style: decimal;margin-left: 1.9em}.typo li ul, .typo li ol, .typo-ul ul, .typo-ul ol, .typo-ol ul, .typo-ol ol{margin-bottom: 0.8em;margin-left: 2em}.typo li ul, .typo-ul ul, .typo-ol ul{list-style: circle}.typo table th, .typo table td, .typo-table th, .typo-table td, .typo table caption{border: 1px solid #ddd;padding: 0.5em 1em;color: #666}.typo table th, .typo-table th{background: #fbfbfb}.typo table thead th, .typo-table thead th{background: #f1f1f1}.typo table caption{border-bottom: none}.typo-input, .typo-textarea{-webkit-appearance: none;border-radius: 0}.typo-em, .typo em, legend, caption{color: #000;font-weight: inherit}.typo-em{position: relative}.typo-em:after{position: absolute;top: 0.65em;left: 0;width: 100%;overflow: hidden;white-space: nowrap;content: "・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・"}.typo img{max-width: 100%} diff --git a/src/main/webapp/views/admin/index.jade b/src/main/resources/templates/admin/home/index.jade similarity index 62% rename from src/main/webapp/views/admin/index.jade rename to src/main/resources/templates/admin/home/index.jade index 6e03588..a37df68 100755 --- a/src/main/webapp/views/admin/index.jade +++ b/src/main/resources/templates/admin/home/index.jade @@ -1,7 +1,5 @@ -extends ./layout/admin +extends ../layout/admin block content h1 Admin Dashboard hr - - diff --git a/src/main/resources/templates/admin/home/settings.jade b/src/main/resources/templates/admin/home/settings.jade new file mode 100644 index 0000000..5dabed7 --- /dev/null +++ b/src/main/resources/templates/admin/home/settings.jade @@ -0,0 +1,39 @@ +extends ../layout/admin + +block content + h1 Settings + hr + + form.settings-form(method="post",action="/admin/settings") + input(type="hidden", name='_csrf', value='#{_csrf.token}') + table.table.table-hover.table-bordered.settings + tbody + tr + td.setting-label Site Name + td.input + input.form-control(type="text", name="siteName", value="#{settings.getSiteName()}") + + tr + td.setting-label Site Slogan + td.input + input.form-control(type="text", name="siteSlogan", value="#{settings.getSiteSlogan()}") + tr + td.setting-label Page Size + td.input + input.form-control(type="text", name="pageSize", value="#{settings.getPageSize()}") + + tr + td.setting-label Intro + td.input + input.form-control(type="text", name="intro", value="#{settings.getIntro()}") + + tr + td.setting-label Picture Url + td.input + input.form-control(type="text", name="pictureUrl", value="#{settings.getPictureUrl()}") + + tr + td.setting-label + td.input + input.btn.btn-primary(type="submit", value="Save") + diff --git a/src/main/resources/templates/admin/layout/admin.jade b/src/main/resources/templates/admin/layout/admin.jade new file mode 100644 index 0000000..35eceb4 --- /dev/null +++ b/src/main/resources/templates/admin/layout/admin.jade @@ -0,0 +1,23 @@ +!!! 5 +html(lang="en") + head + include head + block head + + body + include navbar + .container-fluid + .row + nav.col-md-2.d-none.d-md-block.bg-light.sidebar + .sidebar-sticky + ul.nav.flex-column + li.nav-item + a.nav-link.active(href="/admin/posts") Posts + li.nav-item + a.nav-link(href="/admin/settings") Settings + li.nav-item + a.nav-link(href="/admin/users/profile") Profile + main.col-md-9.ml-sm-auto.col-lg-10.pt-3.px-4(role="main") + include message + block content + diff --git a/src/main/webapp/views/admin/layout/footer.jade b/src/main/resources/templates/admin/layout/footer.jade similarity index 100% rename from src/main/webapp/views/admin/layout/footer.jade rename to src/main/resources/templates/admin/layout/footer.jade diff --git a/src/main/resources/templates/admin/layout/head.jade b/src/main/resources/templates/admin/layout/head.jade new file mode 100644 index 0000000..04a4543 --- /dev/null +++ b/src/main/resources/templates/admin/layout/head.jade @@ -0,0 +1,18 @@ +meta(charset="utf-8") +meta(http-equiv="X-UA-Compatible", content="IE=edge") +meta(name="viewport", content="width=device-width, initial-scale=1") + +meta(name="description", content="A simple blog demo with Spring MVC.") +meta(name="author", content="Raysmond") +title #{App.getSiteName()} + +link(rel='stylesheet', type='text/css', href="/bower_components/bootstrap/dist/css/bootstrap.min.css") +link(rel='stylesheet', type='text/css', href="/bower_components/fontawesome/css/font-awesome.min.css") +link(rel="stylesheet", type='text/css', href="/css/admin/admin.css") + +script(src="/bower_components/jquery/dist/jquery.min.js") +script(src="/bower_components/bootstrap/dist/js/bootstrap.min.js") +script(src="/bower_components/marked/marked.min.js") +script(src="/bower_components/to-markdown/dist/to-markdown.js") +script(src="/vendors/bootstrap-paginator-1.0.2/js/bootstrap-paginator.min.js") + diff --git a/src/main/resources/templates/admin/layout/message.jade b/src/main/resources/templates/admin/layout/message.jade new file mode 100644 index 0000000..22d1574 --- /dev/null +++ b/src/main/resources/templates/admin/layout/message.jade @@ -0,0 +1,4 @@ +if message != null + + .alert.alert-info + = message.getMessage() diff --git a/src/main/resources/templates/admin/layout/navbar.jade b/src/main/resources/templates/admin/layout/navbar.jade new file mode 100644 index 0000000..2f9582f --- /dev/null +++ b/src/main/resources/templates/admin/layout/navbar.jade @@ -0,0 +1,8 @@ +nav.navbar.navbar-dark.sticky-top.bg-dark.flex-md-nowrap.p-0 + a.navbar-brand.col-sm-3.col-md-2.mr-0(href="/") SpringBlog + input.form-control.form-control-dark.w-100(placeholder="Search") + ul.navbar-nav.px-3 + li.nav-item.text-nowrap + a.nav-link(href="javascript:$('#form').submit();") Logout + form#form(style="visibility: hidden", method="post", action="/logout") + input(type="hidden", name='_csrf', value='#{_csrf.token}') diff --git a/src/main/resources/templates/admin/posts/edit.jade b/src/main/resources/templates/admin/posts/edit.jade new file mode 100644 index 0000000..de5376a --- /dev/null +++ b/src/main/resources/templates/admin/posts/edit.jade @@ -0,0 +1,96 @@ +extends ../layout/admin + +block head + script(src="/bower_components/ace-builds/src-min/ace.js") + script(src="/bower_components/ace-builds/src-min/theme-github.js") + script(src="/bower_components/ace-builds/src-min/mode-markdown.js") + +block content + h1 Edit Post + hr + + form.post-form(method="post",action="/admin/posts/#{post.getId()}") + .item-row + input(type="hidden", name='_csrf', value='#{_csrf.token}') + .form-group + label 标题 + input.form-control(type="text", name="title", value='#{postForm.getTitle()}') + + .form-group + label 正文 + textarea.form-control#content(name="content", style="display:none;") + = postForm.getContent() + div#content-editor + #{postForm.getContent()} + + .form-group + label 摘要 + textarea.form-control#summary(name="summary", style="display:none;") + = postForm.getSummary() + div#summary-editor + #{postForm.getSummary()} + + .item-row + hr + .row + .col-sm-2 + .form-group + label Type + select.form-control(name="postType") + for item in postTypes + if item != postForm.getPostType() + option(value="#{item.getId()}") #{item.getName()} + else + option(value="#{item.getId()}", selected="selected") #{item.getName()} + .col-sm-2 + .form-group + label Format + select.form-control(name="postFormat") + for format in postFormats + if format != postForm.getPostFormat() + option(value="#{format.getId()}") #{format.getDisplayName()} + else + option(value="#{format.getId()}", selected="selected") #{format.getDisplayName()} + .col-sm-2 + .form-group + label Status + select.form-control(name="postStatus") + for status in postStatus + if status != postForm.getPostStatus() + option(value="#{status.getId()}") #{status.getName()} + else + option(value="#{status.getId()}", selected="selected") #{status.getName()} + + .col-sm-2 + .form-group + label Permalink + input.form-control(name="permalink", value="#{postForm.getPermalink()}") + + .col-sm-3 + .form-group + label Tags + input.form-control(name="postTags", value="#{postForm.getPostTags()}") + .item-row + hr + button.btn.btn-primary.btn-lg(type="submit") Save + hr + + script + var editor = ace.edit("content-editor"); + var summaryEditor = ace.edit("summary-editor"); + + editor.setTheme("ace/theme/github"); + summaryEditor.setTheme("ace/theme/github"); + + var MarkdownMode = ace.require("ace/mode/markdown").Mode; + editor.getSession().setMode(new MarkdownMode()); + editor.getSession().setUseWrapMode(true); + + summaryEditor.getSession().setMode(new MarkdownMode()); + summaryEditor.getSession().setUseWrapMode(true); + + $("form").submit(function(){ + $("#content").val(editor.getValue()); + $("#summary").val(summaryEditor.getValue()); + return true; + }); diff --git a/src/main/resources/templates/admin/posts/index.jade b/src/main/resources/templates/admin/posts/index.jade new file mode 100644 index 0000000..82571f8 --- /dev/null +++ b/src/main/resources/templates/admin/posts/index.jade @@ -0,0 +1,67 @@ +extends ../layout/admin + +block content + h1 Posts + hr + + a.btn.btn-primary(href="/admin/posts/new") Create + + .row(style="margin-top: 10px;") + .col-sm-12 + table.table.table-striped + thead + tr + th # + th Title + th Format + th Status + th Permalink + th Created At + th Updated At + th Operations + + tbody + for post in posts + tr + td #{post.getId()} + td #{post.getTitle()} + td #{post.getPostFormat()} + td #{post.getPostStatus()} + td #{post.getPermalink()} + td #{viewHelper.getFormattedDate(post.getCreatedAt())} + td #{viewHelper.getFormattedDate(post.getUpdatedAt())} + td + a.btn.btn-sm.btn-info.mr-1(href="/posts/#{post.getId()}", target:'_blank') + i.fa.fa-eye + a.btn.btn-sm.btn-primary.mr-1(href="/admin/posts/#{post.getId()}/edit") + i.fa.fa-edit + a.btn.btn-sm.btn-danger.btn-delete(href="javascript:deletePost(#{post.id})", postId="#{post.id}") + i.fa.fa-trash-o + + form(id="form-#{post.getId()}",style="visibility: hidden", method="post", action="/admin/posts/#{post.getId()}/delete") + input(type="hidden", name='_csrf', value='#{_csrf.token}') + .col-sm-12 + div#pagination + script + function deletePost(postId){ + if (confirm("Are you sure to delete post @"+postId)) { + $('#form-'+postId).submit(); + } + } + + $(function(){ + var page = #{page}; + + // Use bootstrap paginator + // Ref. http://lyonlai.github.io/bootstrap-paginator/ + if(page>1){ + var options = { + currentPage: #{page} + 1, + totalPages: #{totalPages}, + pageUrl: function(type, page, current){ + return document.location.pathname+'?page='+page; + } + } + $('#pagination').bootstrapPaginator(options); + } + }); diff --git a/src/main/resources/templates/admin/posts/new.jade b/src/main/resources/templates/admin/posts/new.jade new file mode 100644 index 0000000..a99c2d8 --- /dev/null +++ b/src/main/resources/templates/admin/posts/new.jade @@ -0,0 +1,95 @@ +extends ../layout/admin + +block head + script(src="/bower_components/ace-builds/src-min/ace.js") + script(src="/bower_components/ace-builds/src-min/theme-github.js") + script(src="/bower_components/ace-builds/src-min/mode-markdown.js") + +block content + h1 New Post + hr + + form.post-form(method="post",action="/admin/posts") + .item-row + input(type="hidden", name='_csrf', value='#{_csrf.token}') + .form-group + label 标题 + input.form-control(type="text", name="title", value='#{postForm.getTitle()}') + + .form-group + label 正文 + textarea.form-control#content(name="content", style="display:none;") + = postForm.getContent() + div#content-editor + #{postForm.getContent()} + + .form-group + label 摘要 + textarea.form-control#summary(name="summary", style="display:none;") + = postForm.getSummary() + div#summary-editor + #{postForm.getSummary()} + .item-row + hr + .row + .col-sm-2 + .form-group + label Type + select.form-control(name="postType") + for item in postTypes + if item != postForm.getPostType() + option(value="#{item.getId()}") #{item.getName()} + else + option(value="#{item.getId()}", selected="selected") #{item.getName()} + .col-sm-2 + .form-group + label Format + select.form-control(name="postFormat") + for format in postFormats + if format != postForm.getPostFormat() + option(value="#{format.getId()}") #{format.getDisplayName()} + else + option(value="#{format.getId()}", selected="selected") #{format.getDisplayName()} + .col-sm-2 + .form-group + label Status + select.form-control(name="postStatus") + for status in postStatus + if status != postForm.getPostStatus() + option(value="#{status.getId()}") #{status.getName()} + else + option(value="#{status.getId()}", selected="selected") #{status.getName()} + + .col-sm-2 + .form-group + label Permalink + input.form-control(name="permalink", value="#{postForm.getPermalink()}") + + .col-sm-3 + .form-group + label Tags + input.form-control(name="postTags", value="#{postForm.getPostTags()}") + .item-row + hr + button.btn.btn-primary.btn-block(type="submit") Create new post + + script + var editor = ace.edit("content-editor"); + var summaryEditor = ace.edit("summary-editor"); + + editor.setTheme("ace/theme/github"); + summaryEditor.setTheme("ace/theme/github"); + + var MarkdownMode = ace.require("ace/mode/markdown").Mode; + editor.getSession().setMode(new MarkdownMode()); + editor.getSession().setUseWrapMode(true); + + summaryEditor.getSession().setMode(new MarkdownMode()); + summaryEditor.getSession().setUseWrapMode(true); + + $("form").submit(function(){ + $("#content").val(editor.getValue()); + $("#summary").val(summaryEditor.getValue()); + return true; + }); + diff --git a/src/main/resources/templates/admin/users/profile.jade b/src/main/resources/templates/admin/users/profile.jade new file mode 100644 index 0000000..985008f --- /dev/null +++ b/src/main/resources/templates/admin/users/profile.jade @@ -0,0 +1,43 @@ +extends ../layout/admin + +block content + h1 Profile + hr + + .profile + form.post-form(method="post", action="/admin/users/#{user.getId()}") + input(type="hidden", name='_csrf', value='#{_csrf.token}') + + .row + .col-sm-2 ID + .col-sm-3 #{user.getId()} + + hr + .row + .col-sm-2 Email + .col-sm-3 #{user.getEmail()} + + hr + .row + .col-sm-2 Created Time + .col-sm-3 #{user.getCreatedAt()} + + hr + .row + .col-sm-2 Password + .col-sm-3 + input.form-control(type="password", name="password") + + hr + .row + .col-sm-2 New Password + .col-sm-3 + input.form-control(type="password", name="newPassword") + + hr + .row + .col-sm-2 + .col-sm-3 + input.btn.btn-primary(type="submit", value="Save profile") + + hr diff --git a/src/main/resources/templates/error.html b/src/main/resources/templates/error.html new file mode 100644 index 0000000..fc8b92a --- /dev/null +++ b/src/main/resources/templates/error.html @@ -0,0 +1,23 @@ + + + + + +
    +

    + + +

    + +
    + Path: +
    + +
    +
    + +
    + + diff --git a/src/main/resources/templates/error/general.html b/src/main/resources/templates/error/general.html new file mode 100644 index 0000000..fc8b92a --- /dev/null +++ b/src/main/resources/templates/error/general.html @@ -0,0 +1,23 @@ + + + + + +
    +

    + + +

    + +
    + Path: +
    + +
    +
    + +
    + + diff --git a/src/main/resources/templates/fragments/disqus.html b/src/main/resources/templates/fragments/disqus.html new file mode 100644 index 0000000..134ce45 --- /dev/null +++ b/src/main/resources/templates/fragments/disqus.html @@ -0,0 +1,30 @@ + + + + + Title + + +
    +
    + + + +
    + + diff --git a/src/main/resources/templates/home/home.html b/src/main/resources/templates/home/home.html new file mode 100644 index 0000000..bb7efd4 --- /dev/null +++ b/src/main/resources/templates/home/home.html @@ -0,0 +1,51 @@ + + + + + +
    + +
    + +

    + Welcome to this site. My name is Raysmond and I'm a backend development engineer in + Shanghai, China. Here, I want to share some topics around development, architecture, DevOps and more.

    +
    +

    最新文章

    +
    +
    + +
    +
    + +

    +
    + +
    +
    + +
    +
    + + +
    + +
    + + diff --git a/src/main/resources/templates/layout/bootstrap4.html b/src/main/resources/templates/layout/bootstrap4.html new file mode 100644 index 0000000..2115263 --- /dev/null +++ b/src/main/resources/templates/layout/bootstrap4.html @@ -0,0 +1,71 @@ + + + + + + + + + + + + +
    + +
    +
    +
    +
    +
    + +
    +
    +

    + Back to top +

    +

    All rights reserved © Raysmond, 2015-2018.

    +

    Lovingly made by SpringBlog, powered by Spring Boot. +

    +
    +
    + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/layout/header.html b/src/main/resources/templates/layout/header.html new file mode 100644 index 0000000..11e4b2f --- /dev/null +++ b/src/main/resources/templates/layout/header.html @@ -0,0 +1,29 @@ + + + + + Title + + +
    +
    +
    +
    + + + 后台管理 +
    +
    +
    +
    + + diff --git a/src/main/resources/templates/posts/archive.html b/src/main/resources/templates/posts/archive.html new file mode 100644 index 0000000..610649a --- /dev/null +++ b/src/main/resources/templates/posts/archive.html @@ -0,0 +1,24 @@ + + + + + + +
    +

    文章归档

    + +
    +
    +
    +
      +
    • + +
    • +
    +
    +
    +
    + + diff --git a/src/main/resources/templates/posts/post.html b/src/main/resources/templates/posts/post.html new file mode 100644 index 0000000..50f9991 --- /dev/null +++ b/src/main/resources/templates/posts/post.html @@ -0,0 +1,52 @@ + + + + + + +
    +
    +
    +
    +

    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    + + diff --git a/src/main/resources/templates/tags/index.html b/src/main/resources/templates/tags/index.html new file mode 100644 index 0000000..43883a8 --- /dev/null +++ b/src/main/resources/templates/tags/index.html @@ -0,0 +1,20 @@ + + + + + + +
    +

    标签

    + +
    + +
    +
    + + diff --git a/src/main/resources/templates/users/login.html b/src/main/resources/templates/users/login.html new file mode 100644 index 0000000..76167cc --- /dev/null +++ b/src/main/resources/templates/users/login.html @@ -0,0 +1,30 @@ + + + + + +
    +

    登录

    + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    + +
    +
    +
    + + diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index 5945b69..0000000 --- a/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - /generalError - - \ No newline at end of file diff --git a/src/main/webapp/resources/css/admin/admin.css b/src/main/webapp/resources/css/admin/admin.css deleted file mode 100644 index 1555d91..0000000 --- a/src/main/webapp/resources/css/admin/admin.css +++ /dev/null @@ -1,30 +0,0 @@ -body { - -webkit-font-smoothing: antialiased; - color: #434343; -} - -.main-container{ - padding-top: 60px; -} - -form .item-row{ - margin-top: 10px; -} - -td.operations, th.operations{ - text-align: center; - width: 120px; -} -td.operations a{ - margin-left: 5px; -} - -.post-form textarea{ - height: 350px; -} - - -.post-form #content-editor{ - height: 430px; - width: 100%; -} diff --git a/src/main/webapp/resources/fonts/glyphicons-halflings-regular.eot b/src/main/webapp/resources/fonts/glyphicons-halflings-regular.eot deleted file mode 100644 index b93a495..0000000 Binary files a/src/main/webapp/resources/fonts/glyphicons-halflings-regular.eot and /dev/null differ diff --git a/src/main/webapp/resources/fonts/glyphicons-halflings-regular.svg b/src/main/webapp/resources/fonts/glyphicons-halflings-regular.svg deleted file mode 100644 index 94fb549..0000000 --- a/src/main/webapp/resources/fonts/glyphicons-halflings-regular.svg +++ /dev/null @@ -1,288 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/webapp/resources/fonts/glyphicons-halflings-regular.ttf b/src/main/webapp/resources/fonts/glyphicons-halflings-regular.ttf deleted file mode 100644 index 1413fc6..0000000 Binary files a/src/main/webapp/resources/fonts/glyphicons-halflings-regular.ttf and /dev/null differ diff --git a/src/main/webapp/resources/fonts/glyphicons-halflings-regular.woff b/src/main/webapp/resources/fonts/glyphicons-halflings-regular.woff deleted file mode 100644 index 9e61285..0000000 Binary files a/src/main/webapp/resources/fonts/glyphicons-halflings-regular.woff and /dev/null differ diff --git a/src/main/webapp/resources/fonts/glyphicons-halflings-regular.woff2 b/src/main/webapp/resources/fonts/glyphicons-halflings-regular.woff2 deleted file mode 100644 index 64539b5..0000000 Binary files a/src/main/webapp/resources/fonts/glyphicons-halflings-regular.woff2 and /dev/null differ diff --git a/src/main/webapp/resources/images/user.jpg b/src/main/webapp/resources/images/user.jpg deleted file mode 100644 index c73889c..0000000 Binary files a/src/main/webapp/resources/images/user.jpg and /dev/null differ diff --git a/src/main/webapp/resources/vendors/bootstrap-paginator-1.0.2/.gitignore b/src/main/webapp/resources/vendors/bootstrap-paginator-1.0.2/.gitignore deleted file mode 100644 index 49a77ec..0000000 --- a/src/main/webapp/resources/vendors/bootstrap-paginator-1.0.2/.gitignore +++ /dev/null @@ -1,11 +0,0 @@ -.DS_Store -.idea -css -documentation -img -fonts -lib -test -*.json -*.sh -CHANGELOG diff --git a/src/main/webapp/resources/vendors/bootstrap-paginator-1.0.2/src/bootstrap-paginator.js b/src/main/webapp/resources/vendors/bootstrap-paginator-1.0.2/src/bootstrap-paginator.js deleted file mode 100644 index 0513a8f..0000000 --- a/src/main/webapp/resources/vendors/bootstrap-paginator-1.0.2/src/bootstrap-paginator.js +++ /dev/null @@ -1,657 +0,0 @@ -/** - * bootstrap-paginator.js v0.5 - * -- - * Copyright 2013 Yun Lai - * -- - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -(function ($) { - - "use strict"; // jshint ;_; - - - /* Paginator PUBLIC CLASS DEFINITION - * ================================= */ - - /** - * Boostrap Paginator Constructor - * - * @param element element of the paginator - * @param options the options to config the paginator - * - * */ - var BootstrapPaginator = function (element, options) { - this.init(element, options); - }, - old = null; - - BootstrapPaginator.prototype = { - - /** - * Initialization function of the paginator, accepting an element and the options as parameters - * - * @param element element of the paginator - * @param options the options to config the paginator - * - * */ - init: function (element, options) { - - this.$element = $(element); - - var version = (options && options.bootstrapMajorVersion) ? options.bootstrapMajorVersion : $.fn.bootstrapPaginator.defaults.bootstrapMajorVersion, - id = this.$element.attr("id"); - - if (version === 2 && !this.$element.is("div")) { - - throw "in Bootstrap version 2 the pagination must be a div element. Or if you are using Bootstrap pagination 3. Please specify it in bootstrapMajorVersion in the option"; - } else if (version > 2 && !this.$element.is("ul")) { - throw "in Bootstrap version 3 the pagination root item must be an ul element." - } - - - - this.currentPage = 1; - - this.lastPage = 1; - - this.setOptions(options); - - this.initialized = true; - }, - - /** - * Update the properties of the paginator element - * - * @param options options to config the paginator - * */ - setOptions: function (options) { - - this.options = $.extend({}, (this.options || $.fn.bootstrapPaginator.defaults), options); - - this.totalPages = parseInt(this.options.totalPages, 10); //setup the total pages property. - this.numberOfPages = parseInt(this.options.numberOfPages, 10); //setup the numberOfPages to be shown - - //move the set current page after the setting of total pages. otherwise it will cause out of page exception. - if (options && typeof (options.currentPage) !== 'undefined') { - - this.setCurrentPage(options.currentPage); - } - - this.listen(); - - //render the paginator - this.render(); - - if (!this.initialized && this.lastPage !== this.currentPage) { - this.$element.trigger("page-changed", [this.lastPage, this.currentPage]); - } - - }, - - /** - * Sets up the events listeners. Currently the pageclicked and pagechanged events are linked if available. - * - * */ - listen: function () { - - this.$element.off("page-clicked"); - - this.$element.off("page-changed");// unload the events for the element - - if (typeof (this.options.onPageClicked) === "function") { - this.$element.bind("page-clicked", this.options.onPageClicked); - } - - if (typeof (this.options.onPageChanged) === "function") { - this.$element.on("page-changed", this.options.onPageChanged); - } - - this.$element.bind("page-clicked", this.onPageClicked); - }, - - - /** - * - * Destroys the paginator element, it unload the event first, then empty the content inside. - * - * */ - destroy: function () { - - this.$element.off("page-clicked"); - - this.$element.off("page-changed"); - - this.$element.removeData('bootstrapPaginator'); - - this.$element.empty(); - - }, - - /** - * Shows the page - * - * */ - show: function (page) { - - this.setCurrentPage(page); - - this.render(); - - if (this.lastPage !== this.currentPage) { - this.$element.trigger("page-changed", [this.lastPage, this.currentPage]); - } - }, - - /** - * Shows the next page - * - * */ - showNext: function () { - var pages = this.getPages(); - - if (pages.next) { - this.show(pages.next); - } - - }, - - /** - * Shows the previous page - * - * */ - showPrevious: function () { - var pages = this.getPages(); - - if (pages.prev) { - this.show(pages.prev); - } - - }, - - /** - * Shows the first page - * - * */ - showFirst: function () { - var pages = this.getPages(); - - if (pages.first) { - this.show(pages.first); - } - - }, - - /** - * Shows the last page - * - * */ - showLast: function () { - var pages = this.getPages(); - - if (pages.last) { - this.show(pages.last); - } - - }, - - /** - * Internal on page item click handler, when the page item is clicked, change the current page to the corresponding page and - * trigger the pageclick event for the listeners. - * - * - * */ - onPageItemClicked: function (event) { - - var type = event.data.type, - page = event.data.page; - - this.$element.trigger("page-clicked", [event, type, page]); - - }, - - onPageClicked: function (event, originalEvent, type, page) { - - //show the corresponding page and retrieve the newly built item related to the page clicked before for the event return - - var currentTarget = $(event.currentTarget); - - switch (type) { - case "first": - currentTarget.bootstrapPaginator("showFirst"); - break; - case "prev": - currentTarget.bootstrapPaginator("showPrevious"); - break; - case "next": - currentTarget.bootstrapPaginator("showNext"); - break; - case "last": - currentTarget.bootstrapPaginator("showLast"); - break; - case "page": - currentTarget.bootstrapPaginator("show", page); - break; - } - - }, - - /** - * Renders the paginator according to the internal properties and the settings. - * - * - * */ - render: function () { - - //fetch the container class and add them to the container - var containerClass = this.getValueFromOption(this.options.containerClass, this.$element), - size = this.options.size || "normal", - alignment = this.options.alignment || "left", - pages = this.getPages(), - listContainer = this.options.bootstrapMajorVersion === 2 ? $("
      ") : this.$element, - listContainerClass = this.options.bootstrapMajorVersion === 2 ? this.getValueFromOption(this.options.listContainerClass, listContainer) : null, - first = null, - prev = null, - next = null, - last = null, - p = null, - i = 0; - - - this.$element.prop("class", ""); - - this.$element.addClass("pagination"); - - switch (size.toLowerCase()) { - case "large": - case "small": - case "mini": - this.$element.addClass($.fn.bootstrapPaginator.sizeArray[this.options.bootstrapMajorVersion][size.toLowerCase()]); - break; - default: - break; - } - - if (this.options.bootstrapMajorVersion === 2) { - switch (alignment.toLowerCase()) { - case "center": - this.$element.addClass("pagination-centered"); - break; - case "right": - this.$element.addClass("pagination-right"); - break; - default: - break; - } - } - - - this.$element.addClass(containerClass); - - //empty the outter most container then add the listContainer inside. - this.$element.empty(); - - if (this.options.bootstrapMajorVersion === 2) { - this.$element.append(listContainer); - - listContainer.addClass(listContainerClass); - } - - //update the page element reference - this.pageRef = []; - - if (pages.first) {//if the there is first page element - first = this.buildPageItem("first", pages.first); - - if (first) { - listContainer.append(first); - } - - } - - if (pages.prev) {//if the there is previous page element - - prev = this.buildPageItem("prev", pages.prev); - - if (prev) { - listContainer.append(prev); - } - - } - - - for (i = 0; i < pages.length; i = i + 1) {//fill the numeric pages. - - p = this.buildPageItem("page", pages[i]); - - if (p) { - listContainer.append(p); - } - } - - if (pages.next) {//if there is next page - - next = this.buildPageItem("next", pages.next); - - if (next) { - listContainer.append(next); - } - } - - if (pages.last) {//if there is last page - - last = this.buildPageItem("last", pages.last); - - if (last) { - listContainer.append(last); - } - } - }, - - /** - * - * Creates a page item base on the type and page number given. - * - * @param page page number - * @param type type of the page, whether it is the first, prev, page, next, last - * - * @return Object the constructed page element - * */ - buildPageItem: function (type, page) { - - var itemContainer = $("
    • "),//creates the item container - itemContent = $(""),//creates the item content - text = "", - title = "", - itemContainerClass = this.options.itemContainerClass(type, page, this.currentPage), - itemContentClass = this.getValueFromOption(this.options.itemContentClass, type, page, this.currentPage), - tooltipOpts = null; - - - switch (type) { - - case "first": - if (!this.getValueFromOption(this.options.shouldShowPage, type, page, this.currentPage)) { return; } - text = this.options.itemTexts(type, page, this.currentPage); - title = this.options.tooltipTitles(type, page, this.currentPage); - break; - case "last": - if (!this.getValueFromOption(this.options.shouldShowPage, type, page, this.currentPage)) { return; } - text = this.options.itemTexts(type, page, this.currentPage); - title = this.options.tooltipTitles(type, page, this.currentPage); - break; - case "prev": - if (!this.getValueFromOption(this.options.shouldShowPage, type, page, this.currentPage)) { return; } - text = this.options.itemTexts(type, page, this.currentPage); - title = this.options.tooltipTitles(type, page, this.currentPage); - break; - case "next": - if (!this.getValueFromOption(this.options.shouldShowPage, type, page, this.currentPage)) { return; } - text = this.options.itemTexts(type, page, this.currentPage); - title = this.options.tooltipTitles(type, page, this.currentPage); - break; - case "page": - if (!this.getValueFromOption(this.options.shouldShowPage, type, page, this.currentPage)) { return; } - text = this.options.itemTexts(type, page, this.currentPage); - title = this.options.tooltipTitles(type, page, this.currentPage); - break; - } - - itemContainer.addClass(itemContainerClass).append(itemContent); - - itemContent.addClass(itemContentClass).html(text).on("click", null, {type: type, page: page}, $.proxy(this.onPageItemClicked, this)); - - if (this.options.pageUrl) { - itemContent.attr("href", this.getValueFromOption(this.options.pageUrl, type, page, this.currentPage)); - } - - if (this.options.useBootstrapTooltip) { - tooltipOpts = $.extend({}, this.options.bootstrapTooltipOptions, {title: title}); - - itemContent.tooltip(tooltipOpts); - } else { - itemContent.attr("title", title); - } - - return itemContainer; - - }, - - setCurrentPage: function (page) { - if (page > this.totalPages || page < 1) {// if the current page is out of range, throw exception. - - throw "Page out of range"; - - } - - this.lastPage = this.currentPage; - - this.currentPage = parseInt(page, 10); - - }, - - /** - * Gets an array that represents the current status of the page object. Numeric pages can be access via array mode. length attributes describes how many numeric pages are there. First, previous, next and last page can be accessed via attributes first, prev, next and last. Current attribute marks the current page within the pages. - * - * @return object output objects that has first, prev, next, last and also the number of pages in between. - * */ - getPages: function () { - - var totalPages = this.totalPages,// get or calculate the total pages via the total records - pageStart = (this.currentPage % this.numberOfPages === 0) ? (parseInt(this.currentPage / this.numberOfPages, 10) - 1) * this.numberOfPages + 1 : parseInt(this.currentPage / this.numberOfPages, 10) * this.numberOfPages + 1,//calculates the start page. - output = [], - i = 0, - counter = 0; - - pageStart = pageStart < 1 ? 1 : pageStart;//check the range of the page start to see if its less than 1. - - for (i = pageStart, counter = 0; counter < this.numberOfPages && i <= totalPages; i = i + 1, counter = counter + 1) {//fill the pages - output.push(i); - } - - output.first = 1;//add the first when the current page leaves the 1st page. - - if (this.currentPage > 1) {// add the previous when the current page leaves the 1st page - output.prev = this.currentPage - 1; - } else { - output.prev = 1; - } - - if (this.currentPage < totalPages) {// add the next page when the current page doesn't reach the last page - output.next = this.currentPage + 1; - } else { - output.next = totalPages; - } - - output.last = totalPages;// add the last page when the current page doesn't reach the last page - - output.current = this.currentPage;//mark the current page. - - output.total = totalPages; - - output.numberOfPages = this.options.numberOfPages; - - return output; - - }, - - /** - * Gets the value from the options, this is made to handle the situation where value is the return value of a function. - * - * @return mixed value that depends on the type of parameters, if the given parameter is a function, then the evaluated result is returned. Otherwise the parameter itself will get returned. - * */ - getValueFromOption: function (value) { - - var output = null, - args = Array.prototype.slice.call(arguments, 1); - - if (typeof value === 'function') { - output = value.apply(this, args); - } else { - output = value; - } - - return output; - - } - - }; - - - /* TYPEAHEAD PLUGIN DEFINITION - * =========================== */ - - old = $.fn.bootstrapPaginator; - - $.fn.bootstrapPaginator = function (option) { - - var args = arguments, - result = null; - - $(this).each(function (index, item) { - var $this = $(item), - data = $this.data('bootstrapPaginator'), - options = (typeof option !== 'object') ? null : option; - - if (!data) { - data = new BootstrapPaginator(this, options); - - $this = $(data.$element); - - $this.data('bootstrapPaginator', data); - - return; - } - - if (typeof option === 'string') { - - if (data[option]) { - result = data[option].apply(data, Array.prototype.slice.call(args, 1)); - } else { - throw "Method " + option + " does not exist"; - } - - } else { - result = data.setOptions(option); - } - }); - - return result; - - }; - - $.fn.bootstrapPaginator.sizeArray = { - - "2": { - "large": "pagination-large", - "small": "pagination-small", - "mini": "pagination-mini" - }, - "3": { - "large": "pagination-lg", - "small": "pagination-sm", - "mini": "" - } - - }; - - $.fn.bootstrapPaginator.defaults = { - containerClass: "", - size: "normal", - alignment: "left", - bootstrapMajorVersion: 2, - listContainerClass: "", - itemContainerClass: function (type, page, current) { - return (page === current) ? "active" : ""; - }, - itemContentClass: function (type, page, current) { - return ""; - }, - currentPage: 1, - numberOfPages: 5, - totalPages: 1, - pageUrl: function (type, page, current) { - return null; - }, - onPageClicked: null, - onPageChanged: null, - useBootstrapTooltip: false, - shouldShowPage: function (type, page, current) { - - var result = true; - - switch (type) { - case "first": - result = (current !== 1); - break; - case "prev": - result = (current !== 1); - break; - case "next": - result = (current !== this.totalPages); - break; - case "last": - result = (current !== this.totalPages); - break; - case "page": - result = true; - break; - } - - return result; - - }, - itemTexts: function (type, page, current) { - switch (type) { - case "first": - return "<<"; - case "prev": - return "<"; - case "next": - return ">"; - case "last": - return ">>"; - case "page": - return page; - } - }, - tooltipTitles: function (type, page, current) { - - switch (type) { - case "first": - return "Go to first page"; - case "prev": - return "Go to previous page"; - case "next": - return "Go to next page"; - case "last": - return "Go to last page"; - case "page": - return (page === current) ? "Current page is " + page : "Go to page " + page; - } - }, - bootstrapTooltipOptions: { - animation: true, - html: true, - placement: 'top', - selector: false, - title: "", - container: false - } - }; - - $.fn.bootstrapPaginator.Constructor = BootstrapPaginator; - - - -}(window.jQuery)); diff --git a/src/main/webapp/views/admin/layout/admin.jade b/src/main/webapp/views/admin/layout/admin.jade deleted file mode 100644 index 0c1a46c..0000000 --- a/src/main/webapp/views/admin/layout/admin.jade +++ /dev/null @@ -1,11 +0,0 @@ -!!! 5 -html(lang="zh-CN") - head - include head - block head - - body - include navbar - - .container.main-container - block content diff --git a/src/main/webapp/views/admin/layout/head.jade b/src/main/webapp/views/admin/layout/head.jade deleted file mode 100644 index b7cad57..0000000 --- a/src/main/webapp/views/admin/layout/head.jade +++ /dev/null @@ -1,14 +0,0 @@ -meta(charset="utf-8") -meta(http-equiv="X-UA-Compatible", content="IE=edge") -meta(name="viewport", content="width=device-width, initial-scale=1") - -meta(name="description", content="A simple blog demo with Spring MVC.") -meta(name="author", content="Raysmond") -title SringBlog - -link(rel='stylesheet', type='text/css', href='#{basePath}/webjars/bootstrap/3.3.5/css/bootstrap.min.css') -link(rel='stylesheet', type='text/css', href='#{basePath}/webjars/font-awesome/4.3.0-3/css/font-awesome.min.css') -link(rel="stylesheet", type='text/css', href="#{basePath}/resources/css/admin/admin.css") - -script(src="#{basePath}/webjars/jquery/2.1.4/jquery.min.js") -script(src="#{basePath}/webjars/bootstrap/3.3.5/js/bootstrap.min.js") diff --git a/src/main/webapp/views/admin/layout/navbar.jade b/src/main/webapp/views/admin/layout/navbar.jade deleted file mode 100644 index 03b9843..0000000 --- a/src/main/webapp/views/admin/layout/navbar.jade +++ /dev/null @@ -1,29 +0,0 @@ -nav.navbar.navbar-fixed-top.navbar-bright - .container - .navbar-header - button.navbar-toggle.collapsed(type="button", data-toggle="collapse", data-target="#navbar", aria-expanded="false", aria-controls="navbar") - span.sr-only Toggle navigation - span.icon-bar - span.icon-bar - span.icon-bar - a.navbar-brand(href="#{basePath}/admin") - span Sring Blog - .collapse.navbar-collapse#navbar - ul.nav.navbar-nav - li.active - a(href="#{basePath}/admin/posts") Posts - li - a(href="#{basePath}/admin/settings") Setting - ul.nav.navbar-nav.navbar-right.navbar-user - li.dropdown - a.dropdown-toggle(data-toggle="dropdown") - img(src="#{basePath}/resources/images/user.jpg", style="width:32px;height:32px;") - span   User - ul.dropdown-menu - li - a(href="javascript:$('#form').submit();") - span.glyphicon.glyphicon-log-out(aria-hidden="true")  Logout - - form#form(style="visibility: hidden", method="post", action="#{basePath}/logout") - input(type="hidden", name='_csrf', value='#{_csrf.token}') - diff --git a/src/main/webapp/views/admin/posts_edit.jade b/src/main/webapp/views/admin/posts_edit.jade deleted file mode 100644 index 4b25d2a..0000000 --- a/src/main/webapp/views/admin/posts_edit.jade +++ /dev/null @@ -1,44 +0,0 @@ -extends ./layout/admin - -block head - script(src="#{basePath}/webjars/ace/1.2.0/src-min/ace.js") - script(src="#{basePath}/webjars/ace/1.2.0/src-min/theme-github.js") - script(src="#{basePath}/webjars/ace/1.2.0/src-min/mode-markdown.js") - -block content - h1 Edit Post - hr - - form.post-form(method="post",action="#{basePath}/admin/posts/#{post.getId()}") - .item-row - input(type="hidden", name='_csrf', value='#{_csrf.token}') - .item-row - input.form-control(type="text", name="title", value='#{postForm.getTitle()}') - .item-row - textarea.form-control#content(name="content", style="display:none;") - = postForm.getContent() - div#content-editor - #{postForm.getContent()} - .item-row - select.form-control(style="width:120px;", name="postFormat") - for format in postFormats - if format != post.getPostFormat() - option(value="#{format.getId()}") #{format.getDisplayName()} - else - option(value="#{format.getId()}", selected="selected") #{format.getDisplayName()} - .item-row - button.btn.btn-primary.btn-block(type="submit") Save - - script - var editor = ace.edit("content-editor"); - editor.setTheme("ace/theme/github"); - - var MarkdownMode = ace.require("ace/mode/markdown").Mode; - editor.getSession().setMode(new MarkdownMode()); - - editor.getSession().setUseWrapMode(true); - - $("form").submit(function(){ - $("#content").val(editor.getValue()); - return true; - }); diff --git a/src/main/webapp/views/admin/posts_index.jade b/src/main/webapp/views/admin/posts_index.jade deleted file mode 100644 index 378876d..0000000 --- a/src/main/webapp/views/admin/posts_index.jade +++ /dev/null @@ -1,39 +0,0 @@ -extends ./layout/admin - -block content - h1 Posts - hr - - a.btn.btn-primary(href="#{basePath}/admin/posts/new") Create - - .row(style="margin-top: 10px;") - .col-sm-12 - table.table.table-bordered.table-hover.table-stripped - thead - tr - th ID - th Title - th Format - th Status - th Created At - th.operations Operations - - tbody - for post in posts - tr - td #{post.getId()} - td #{post.getTitle()} - td #{post.getPostFormat()} - td #{post.getPostStatus()} - td #{post.getCreatedAt()} - td.operations - a.btn.btn-xs.btn-info(href="#{basePath}/posts/#{post.getId()}", target:'_blank') - i.fa.fa-eye - a.btn.btn-xs.btn-primary(href="#{basePath}/admin/posts/#{post.getId()}/edit") - i.fa.fa-edit - a.btn.btn-xs.btn-danger.btn-delete(href="javascript:$('#form-#{post.getId()}').submit();", postId="#{post.id}") - i.fa.fa-trash-o - - form(id="form-#{post.getId()}",style="visibility: hidden", method="post", action="#{basePath}/admin/posts/#{post.getId()}/delete") - input(type="hidden", name='_csrf', value='#{_csrf.token}') - diff --git a/src/main/webapp/views/admin/posts_new.jade b/src/main/webapp/views/admin/posts_new.jade deleted file mode 100644 index 8ee97e2..0000000 --- a/src/main/webapp/views/admin/posts_new.jade +++ /dev/null @@ -1,41 +0,0 @@ -extends ./layout/admin - -block head - script(src="#{basePath}/webjars/ace/1.2.0/src-min/ace.js") - script(src="#{basePath}/webjars/ace/1.2.0/src-min/theme-github.js") - script(src="#{basePath}/webjars/ace/1.2.0/src-min/mode-markdown.js") - -block content - h1 New Post - hr - - form.post-form(method="post",action="#{basePath}/admin/posts") - .item-row - input(type="hidden", name='_csrf', value='#{_csrf.token}') - .item-row - input.form-control(type="text", name="title", value='#{postForm.getTitle()}') - .item-row - textarea.form-control#content(name="content", style="display:none;") - = postForm.getContent() - div#content-editor - #{postForm.getContent()} - .item-row - select.form-control(style="width:120px;", name="postFormat") - for format in postFormats - option(value="#{format.getId()}") #{format.getDisplayName()} - .item-row - button.btn.btn-primary.btn-block(type="submit") Create new post - - script - var editor = ace.edit("content-editor"); - editor.setTheme("ace/theme/github"); - - var MarkdownMode = ace.require("ace/mode/markdown").Mode; - editor.getSession().setMode(new MarkdownMode()); - - editor.getSession().setUseWrapMode(true); - - $("form").submit(function(){ - $("#content").val(editor.getValue()); - return true; - }); diff --git a/src/main/webapp/views/admin/settings.jade b/src/main/webapp/views/admin/settings.jade deleted file mode 100644 index cd10c11..0000000 --- a/src/main/webapp/views/admin/settings.jade +++ /dev/null @@ -1,5 +0,0 @@ -extends ./layout/admin - -block content - h1 Settings - hr diff --git a/src/main/webapp/views/error/general.html b/src/main/webapp/views/error/general.html deleted file mode 100644 index 9a0dd33..0000000 --- a/src/main/webapp/views/error/general.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - Error page - - - - - -
      -
      Error: Error
      -
      - - diff --git a/src/main/webapp/views/fragments/alert.html b/src/main/webapp/views/fragments/alert.html deleted file mode 100644 index 7c04c11..0000000 --- a/src/main/webapp/views/fragments/alert.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - -
      - - Test -
      - - diff --git a/src/main/webapp/views/fragments/disqus.jade b/src/main/webapp/views/fragments/disqus.jade deleted file mode 100644 index bfd563c..0000000 --- a/src/main/webapp/views/fragments/disqus.jade +++ /dev/null @@ -1,16 +0,0 @@ -div#disqus_thread - -script - /* * * CONFIGURATION VARIABLES * * */ - var disqus_shortname = 'raysmond'; - - /* * * DON'T EDIT BELOW THIS LINE * * */ - (function() { - var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true; - dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js'; - (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq); - })(); - -noscript - |Please enable JavaScript to view the - a(href="https://disqus.com/?ref_noscript", rel="nofollow") comments powered by Disqus diff --git a/src/main/webapp/views/home/about.jade b/src/main/webapp/views/home/about.jade deleted file mode 100644 index e1ff022..0000000 --- a/src/main/webapp/views/home/about.jade +++ /dev/null @@ -1,13 +0,0 @@ -extends ../layout/app - -block page_title - h1 Raysmond - p Something about me. - -block title - |About Raysmond - -block content - .post - .content - !{about.getRenderedContent()} diff --git a/src/main/webapp/views/home/index.jade b/src/main/webapp/views/home/index.jade deleted file mode 100644 index 9fae4ee..0000000 --- a/src/main/webapp/views/home/index.jade +++ /dev/null @@ -1,27 +0,0 @@ -extends ../layout/app - -block page_title - h1 Raysmond - p Do it now or never - -block title - |Raysmond - -block content - include ../posts/index - - .load-more - nav - ul.pager - if totalPages > page - li.previous - a.btn(href="#{basePath}/?page=#{page+1}") - span(aria-hidden="true) ← - | Older posts - - if page > 1 - li.next - a.btn(href="#{basePath}/?page=#{page-1}") - |Newer posts - span(aria-hidden="true) → - diff --git a/src/main/webapp/views/layout/app.jade b/src/main/webapp/views/layout/app.jade deleted file mode 100644 index 51194d7..0000000 --- a/src/main/webapp/views/layout/app.jade +++ /dev/null @@ -1,15 +0,0 @@ -!!! 5 -html(lang="zh-CN") - head - include head - block head - - body - include navbar - - include top - - .container.main-container - block content - - include footer diff --git a/src/main/webapp/views/layout/footer.jade b/src/main/webapp/views/layout/footer.jade deleted file mode 100644 index fd88c72..0000000 --- a/src/main/webapp/views/layout/footer.jade +++ /dev/null @@ -1,7 +0,0 @@ -.container - .footer - .footer-wrapper - hr - p Copyright © · 2015 Raysmond · All Right Reserved. - p Page: #{viewHelper.getResponseTime()}ms - diff --git a/src/main/webapp/views/layout/head.jade b/src/main/webapp/views/layout/head.jade deleted file mode 100644 index 6bd5e95..0000000 --- a/src/main/webapp/views/layout/head.jade +++ /dev/null @@ -1,18 +0,0 @@ -meta(charset="utf-8") -meta(http-equiv="X-UA-Compatible", content="IE=edge") -meta(name="viewport", content="width=device-width, initial-scale=1") - -meta(name="description", content="A simple blog demo with Spring MVC.") -meta(name="author", content="Raysmond") - -title - block title - |Raysmond - -link(rel='stylesheet', type='text/css', href='#{basePath}/webjars/bootstrap/3.3.5/css/bootstrap.min.css') -link(rel='stylesheet', type='text/css', href='#{basePath}/webjars/font-awesome/4.3.0-3/css/font-awesome.min.css') -link(rel="stylesheet", type='text/css', href="#{basePath}/resources/vendors/pygments/github.css") -link(rel="stylesheet", type='text/css', href="#{basePath}/resources/css/theme.css") - -script(src="#{basePath}/webjars/jquery/2.1.4/jquery.min.js") -// script(src="#{basePath}/webjars/bootstrap/3.3.5/js/bootstrap.min.js") diff --git a/src/main/webapp/views/layout/navbar.jade b/src/main/webapp/views/layout/navbar.jade deleted file mode 100644 index b596d04..0000000 --- a/src/main/webapp/views/layout/navbar.jade +++ /dev/null @@ -1,15 +0,0 @@ -nav.navbar.navbar-bright - .container - .navbar-header - button.navbar-toggle.collapsed(type="button", data-toggle="collapse", data-target="#navbar", aria-expanded="false", aria-controls="navbar") - span.sr-only Toggle navigation - span.icon-bar - span.icon-bar - span.icon-bar - .collapse.navbar-collapse#navbar - ul.nav.navbar-nav - li.active - a(href="#{basePath}/") Home - li: a(href="#{basePath}/posts/archive") Archive - li: a(href="#{basePath}/about") About - diff --git a/src/main/webapp/views/layout/top.jade b/src/main/webapp/views/layout/top.jade deleted file mode 100644 index 855203e..0000000 --- a/src/main/webapp/views/layout/top.jade +++ /dev/null @@ -1,4 +0,0 @@ -.container - .header-title - .header-title-wrapper - block page_title diff --git a/src/main/webapp/views/posts/archive.jade b/src/main/webapp/views/posts/archive.jade deleted file mode 100644 index 4804786..0000000 --- a/src/main/webapp/views/posts/archive.jade +++ /dev/null @@ -1,23 +0,0 @@ -extends ../layout/app - -block page_title - h1 Posts Archive - p Look back and find something interesting. - -block title - | Posts Archive · Raysmond - -block content - .post - .content - .year - h2 2015 - ul - for post in posts - if post.getCreatedAt().getYear() == (2015 - 1900) - li - span.month - = viewHelper.getMonthAndDay(post.getCreatedAt()) - span   ·  - a(href="#{basePath}/posts/#{post.getId()}") #{post.getTitle()} - diff --git a/src/main/webapp/views/posts/index.jade b/src/main/webapp/views/posts/index.jade deleted file mode 100644 index ef26e16..0000000 --- a/src/main/webapp/views/posts/index.jade +++ /dev/null @@ -1,14 +0,0 @@ -.posts - for post in posts - .row.post - .col-sm-12 - .meta - .time #{viewHelper.getFormattedDate(post.getCreatedAt())} - h2.title - a(href="#{basePath}/posts/#{post.getId()}") - = post.getTitle() - - .content - !{post.getRenderedContent()} - - hr diff --git a/src/main/webapp/views/posts/show.jade b/src/main/webapp/views/posts/show.jade deleted file mode 100644 index 78d957b..0000000 --- a/src/main/webapp/views/posts/show.jade +++ /dev/null @@ -1,17 +0,0 @@ -extends ../layout/app - -block title - = post.getTitle() - | · Raysmond - -block page_title - h1 #{post.getTitle()} - p #{viewHelper.getFormattedDate(post.getCreatedAt())} - -block content - .post - .content - !{post.getRenderedContent()} - - .comments - include ../fragments/disqus diff --git a/src/main/webapp/views/users/signin.jade b/src/main/webapp/views/users/signin.jade deleted file mode 100644 index 21abcf5..0000000 --- a/src/main/webapp/views/users/signin.jade +++ /dev/null @@ -1,15 +0,0 @@ -extends ../layout/app - -block page_title - h1 Admin Signin - -block content - .row - .col-md-4.col-md-offset-4.login-container - .login-panel - form.signin-form(method="post",action="#{basePath}/authenticate") - input(type="hidden", name='_csrf', value='#{_csrf.token}') - input.form-control(type="text",name="username") - input.form-control(type="password",name="password") - button.btn.btn-primary.btn-block(type="submit") Login - diff --git a/src/test/java/com/raysmond/blog/config/EmbeddedDataSourceConfig.java b/src/test/java/com/raysmond/blog/config/EmbeddedDataSourceConfig.java deleted file mode 100644 index 81fa0c9..0000000 --- a/src/test/java/com/raysmond/blog/config/EmbeddedDataSourceConfig.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.raysmond.blog.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Profile; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; -import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; - -import javax.annotation.PostConstruct; -import javax.sql.DataSource; - -/** - * The data source config that can be used in integration tests. - */ -@Configuration -@Profile("test") -public class EmbeddedDataSourceConfig { - - @Bean - public DataSource dataSource() { - return new EmbeddedDatabaseBuilder() - .setType(EmbeddedDatabaseType.HSQL) - .build(); - } -} diff --git a/src/test/java/com/raysmond/blog/config/NoCsrfSecurityConfig.java b/src/test/java/com/raysmond/blog/config/NoCsrfSecurityConfig.java deleted file mode 100644 index b1268e9..0000000 --- a/src/test/java/com/raysmond/blog/config/NoCsrfSecurityConfig.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.raysmond.blog.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; - -@Configuration -public class NoCsrfSecurityConfig extends SecurityConfig { - - @Override - protected void configure(HttpSecurity http) throws Exception { - super.configure(http); - http.csrf().disable(); - } - -} diff --git a/src/test/java/com/raysmond/blog/config/WebAppConfigurationAware.java b/src/test/java/com/raysmond/blog/config/WebAppConfigurationAware.java deleted file mode 100644 index eb9130f..0000000 --- a/src/test/java/com/raysmond/blog/config/WebAppConfigurationAware.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.raysmond.blog.config; - -import org.junit.Before; -import org.junit.runner.RunWith; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.web.context.WebApplicationContext; - -import javax.inject.Inject; - -import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; - -@RunWith(SpringJUnit4ClassRunner.class) -@ActiveProfiles("test") -@WebAppConfiguration -@ContextConfiguration(classes = { - ApplicationConfig.class, - EmbeddedDataSourceConfig.class, - JpaConfig.class, - NoCsrfSecurityConfig.class, - WebMvcConfig.class -}) -public abstract class WebAppConfigurationAware { - - @Inject - protected WebApplicationContext wac; - protected MockMvc mockMvc; - - @Before - public void before() { - this.mockMvc = webAppContextSetup(this.wac).build(); - } - -} diff --git a/src/test/java/com/raysmond/blog/config/WebSecurityConfigurationAware.java b/src/test/java/com/raysmond/blog/config/WebSecurityConfigurationAware.java deleted file mode 100644 index 8caa230..0000000 --- a/src/test/java/com/raysmond/blog/config/WebSecurityConfigurationAware.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.raysmond.blog.config; - -import org.junit.Before; -import org.springframework.security.web.FilterChainProxy; - -import javax.inject.Inject; - -import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; - -public abstract class WebSecurityConfigurationAware extends WebAppConfigurationAware { - - @Inject - private FilterChainProxy springSecurityFilterChain; - - @Before - public void before() { - this.mockMvc = webAppContextSetup(this.wac) - .addFilters(this.springSecurityFilterChain).build(); - } -} diff --git a/src/test/java/com/raysmond/blog/services/ModelsTest.java b/src/test/java/com/raysmond/blog/services/ModelsTest.java deleted file mode 100644 index 6fe4e0a..0000000 --- a/src/test/java/com/raysmond/blog/services/ModelsTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.raysmond.blog.services; - -import static org.assertj.core.api.Assertions.assertThat; - -import com.raysmond.blog.repositories.UserRepository; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.ExpectedException; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; - -/** - * - * @author: Raysmond - */ -@RunWith(MockitoJUnitRunner.class) -public class ModelsTest { - @Mock - private UserRepository users; - - @Rule - public ExpectedException thrown = ExpectedException.none(); - - @Test - public void modelShouldnotBeNull() { - - assertThat(users != null); - } - -} -