├── .gitignore ├── src ├── main │ ├── webapp │ │ ├── resources │ │ │ ├── img │ │ │ │ ├── en.png │ │ │ │ └── ru.png │ │ │ ├── fonts │ │ │ │ ├── FontAwesome.otf │ │ │ │ ├── fontawesome-webfont.eot │ │ │ │ ├── fontawesome-webfont.ttf │ │ │ │ ├── fontawesome-webfont.woff │ │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ │ └── glyphicons-halflings-regular.woff │ │ │ ├── js │ │ │ │ ├── app │ │ │ │ │ ├── inputs.js │ │ │ │ │ └── article-edit.js │ │ │ │ ├── bootstrap-maxlength.min.js │ │ │ │ ├── summernote-ru-RU.js │ │ │ │ └── validator.min.js │ │ │ ├── css │ │ │ │ ├── app │ │ │ │ │ ├── footer.css │ │ │ │ │ ├── signin-signup.css │ │ │ │ │ ├── header.css │ │ │ │ │ └── common.css │ │ │ │ ├── tokenfield-typeahead.min.css │ │ │ │ ├── bootstrap-tokenfield.min.css │ │ │ │ └── summernote.css │ │ │ └── msg │ │ │ │ ├── messages.properties │ │ │ │ └── messages_en.properties │ │ └── WEB-INF │ │ │ ├── views │ │ │ ├── common │ │ │ │ ├── footer.jsp │ │ │ │ ├── sidebar.jsp │ │ │ │ └── header.jsp │ │ │ ├── about.jsp │ │ │ ├── contacts.jsp │ │ │ ├── home.jsp │ │ │ ├── search.jsp │ │ │ ├── success.jsp │ │ │ ├── error.jsp │ │ │ ├── profile.jsp │ │ │ ├── signin.jsp │ │ │ ├── article-edit.jsp │ │ │ ├── signup.jsp │ │ │ └── article-view.jsp │ │ │ ├── tags │ │ │ ├── category-tags.tag │ │ │ ├── article-list.tag │ │ │ ├── pagination.tag │ │ │ └── template.tag │ │ │ ├── web.xml │ │ │ └── spring │ │ │ └── webmvc-config.xml │ ├── resources │ │ ├── META-INF │ │ │ ├── persistence.xml │ │ │ ├── database.properties │ │ │ └── spring │ │ │ │ ├── applicationContext-core.xml │ │ │ │ ├── applicationContext-security.xml │ │ │ │ └── applicationContext-persistence.xml │ │ ├── ehcache.xml │ │ └── log4j.xml │ └── java │ │ └── net │ │ └── filippov │ │ └── newsportal │ │ ├── service │ │ ├── CommentService.java │ │ ├── CategoryService.java │ │ ├── UserService.java │ │ ├── impl │ │ │ ├── CommentServiceImpl.java │ │ │ ├── CategoryServiceImpl.java │ │ │ ├── TagServiceImpl.java │ │ │ ├── UserServiceImpl.java │ │ │ └── AbstractServiceImpl.java │ │ ├── TagService.java │ │ ├── util │ │ │ └── QueryParameters.java │ │ ├── AbstractService.java │ │ └── ArticleService.java │ │ ├── exception │ │ ├── UnacceptableFileFormatException.java │ │ ├── ServiceException.java │ │ ├── NotFoundException.java │ │ └── NotUniqueUserFieldException.java │ │ ├── web │ │ ├── constants │ │ │ ├── Common.java │ │ │ ├── View.java │ │ │ └── URL.java │ │ ├── ErrorController.java │ │ ├── CustomExceptionHandler.java │ │ ├── FileUploadController.java │ │ ├── MainController.java │ │ ├── UserController.java │ │ └── SearchController.java │ │ ├── domain │ │ ├── BaseEntity.java │ │ ├── UserRole.java │ │ ├── Category.java │ │ ├── Tag.java │ │ ├── Comment.java │ │ ├── User.java │ │ └── Article.java │ │ ├── security │ │ ├── CustomAuthenticationSuccessHandler.java │ │ └── CustomUserDetailsService.java │ │ └── repository │ │ ├── GenericRepository.java │ │ └── GenericRepositoryJpaImpl.java └── test │ ├── java │ └── net │ │ └── filippov │ │ └── newsportal │ │ └── domain │ │ └── NewsTest.java │ └── resources │ └── log4j.xml ├── README.md ├── .springBeans └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | .settings 4 | /target 5 | /target 6 | /target 7 | -------------------------------------------------------------------------------- /src/main/webapp/resources/img/en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-filippov/newsportal/HEAD/src/main/webapp/resources/img/en.png -------------------------------------------------------------------------------- /src/main/webapp/resources/img/ru.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-filippov/newsportal/HEAD/src/main/webapp/resources/img/ru.png -------------------------------------------------------------------------------- /src/main/webapp/resources/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-filippov/newsportal/HEAD/src/main/webapp/resources/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /src/main/webapp/resources/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-filippov/newsportal/HEAD/src/main/webapp/resources/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /src/main/webapp/resources/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-filippov/newsportal/HEAD/src/main/webapp/resources/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /src/main/webapp/resources/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-filippov/newsportal/HEAD/src/main/webapp/resources/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /src/main/webapp/resources/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-filippov/newsportal/HEAD/src/main/webapp/resources/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /src/main/webapp/resources/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-filippov/newsportal/HEAD/src/main/webapp/resources/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /src/main/webapp/resources/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oleg-filippov/newsportal/HEAD/src/main/webapp/resources/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/main/webapp/resources/js/app/inputs.js: -------------------------------------------------------------------------------- 1 | // Inputs maxlength control 2 | $('input[maxlength]').maxlength(); 3 | 4 | $('input#news-title[maxlength], textarea[maxlength]').maxlength({threshold:20}); 5 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/persistence.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | NONE 7 | 8 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/common/footer.jsp: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/service/CommentService.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.service; 2 | 3 | import net.filippov.newsportal.domain.Comment; 4 | 5 | /** 6 | * Provides comment-related operations 7 | * 8 | * @author Oleg Filippov 9 | */ 10 | public interface CommentService extends AbstractService { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/webapp/resources/css/app/footer.css: -------------------------------------------------------------------------------- 1 | html,body { 2 | height: 100%; 3 | } 4 | 5 | /* Wrapper for page content to push down footer */ 6 | #wrap { 7 | min-height: 100%; 8 | height: auto; 9 | /* Negative indent footer by its height */ 10 | margin: 0 auto -30px; 11 | /* Pad bottom by footer height */ 12 | padding: 0 0 30px; 13 | } 14 | 15 | /* Set the fixed height of the footer */ 16 | #footer { 17 | height: 30px; 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/exception/UnacceptableFileFormatException.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.exception; 2 | 3 | /** 4 | * Throws if file has unacceptable format 5 | * 6 | * @author Oleg Filippov 7 | */ 8 | public class UnacceptableFileFormatException extends RuntimeException { 9 | 10 | private static final long serialVersionUID = -8680431385054555808L; 11 | 12 | public UnacceptableFileFormatException() { 13 | super(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/web/constants/Common.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.web.constants; 2 | 3 | /** 4 | * Common constants 5 | * 6 | * @author Oleg Filippov 7 | */ 8 | public class Common { 9 | 10 | public static final int ARTICLES_PER_PAGE = 5; 11 | public static final int COMMENTS_PER_PAGE = 20; // not working yet 12 | public static final int TAG_MAX_COUNT = 50; 13 | public static final String DEFAULT_CATEGORY_NAME = "Games"; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/service/CategoryService.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.service; 2 | 3 | import net.filippov.newsportal.domain.Category; 4 | 5 | /** 6 | * Provides category-related operations 7 | * 8 | * @author Oleg Filippov 9 | */ 10 | public interface CategoryService extends AbstractService { 11 | 12 | /** 13 | * Get category from repository by it's name 14 | * 15 | * @param name category name 16 | * @return category 17 | */ 18 | Category getByName(String name); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/about.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" 2 | pageEncoding="UTF-8"%> 3 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> 4 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> 5 | <%@ taglib prefix="t" tagdir="/WEB-INF/tags"%> 6 | 7 | 8 | | 9 | 10 | 11 | 12 | 13 |

About...

14 |
15 |
-------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/contacts.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" 2 | pageEncoding="UTF-8"%> 3 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> 4 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> 5 | <%@ taglib prefix="t" tagdir="/WEB-INF/tags"%> 6 | 7 | 8 | | 9 | 10 | 11 | 12 | 13 |

Contacts...

14 |
15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/exception/ServiceException.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.exception; 2 | 3 | /** 4 | * Common service-layer exception 5 | * 6 | * @author Oleg Filippov 7 | */ 8 | public class ServiceException extends RuntimeException { 9 | 10 | private static final long serialVersionUID = -1603048637032593628L; 11 | 12 | public ServiceException(Throwable cause) { 13 | super(cause); 14 | } 15 | 16 | public ServiceException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | 20 | public ServiceException(String message) { 21 | super(message); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/exception/NotFoundException.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | /** 7 | * Throws if searching item (Article, Tag, Category...) not found 8 | * 9 | * @author Oleg Filippov 10 | */ 11 | @ResponseStatus(value=HttpStatus.NOT_FOUND) 12 | public class NotFoundException extends RuntimeException { 13 | 14 | private static final long serialVersionUID = 7582550023114509895L; 15 | 16 | public NotFoundException(String message) { 17 | super(message); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/home.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" 2 | pageEncoding="UTF-8"%> 3 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 4 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> 5 | <%@ taglib prefix="t" tagdir="/WEB-INF/tags"%> 6 | 7 | 8 | | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/database.properties: -------------------------------------------------------------------------------- 1 | #JDBC 2 | jdbc.driverClassName=org.h2.Driver 3 | jdbc.url=jdbc:h2:~/newsportal 4 | jdbc.username=sa 5 | jdbc.password= 6 | 7 | #HIBERNATE 8 | hibernate.dialect=org.hibernate.dialect.H2Dialect 9 | hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactory 10 | hibernate.cache.use_second_level_cache=true 11 | hibernate.cache.use_query_cache=true 12 | #for test 13 | hibernate.hbm2ddl.auto=update 14 | hibernate.show_sql=false 15 | hibernate.format_sql=false 16 | hibernate.use_sql_comments=false 17 | 18 | #DBCP 19 | dbcp.initialSize=10 20 | dbcp.maxActive=50 21 | dbcp.maxIdle=50 22 | dbcp.maxWait=6000 -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/service/UserService.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.service; 2 | 3 | import net.filippov.newsportal.domain.User; 4 | 5 | /** 6 | * Provides user-related operations 7 | * 8 | * @author Oleg Filippov 9 | */ 10 | public interface UserService extends AbstractService { 11 | 12 | /** 13 | * Get user from repository by it's login 14 | * 15 | * @param login user login 16 | * @return user 17 | */ 18 | User getByLogin(String login); 19 | 20 | /** 21 | * Get user from repository by it's email 22 | * 23 | * @param email user email 24 | * @return user 25 | */ 26 | User getByEmail(String email); 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/service/impl/CommentServiceImpl.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.service.impl; 2 | 3 | import net.filippov.newsportal.domain.Comment; 4 | import net.filippov.newsportal.repository.GenericRepository; 5 | import net.filippov.newsportal.service.CommentService; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Service; 9 | 10 | @Service("CommentService") 11 | public class CommentServiceImpl extends AbstractServiceImpl 12 | implements CommentService { 13 | 14 | @Autowired 15 | public CommentServiceImpl(GenericRepository repository) { 16 | super(repository); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/exception/NotUniqueUserFieldException.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.exception; 2 | 3 | /** 4 | * Throws if user with specified field (login, email) already exists 5 | * 6 | * @author Oleg Filippov 7 | */ 8 | public class NotUniqueUserFieldException extends RuntimeException { 9 | 10 | private static final long serialVersionUID = 7464088027168044522L; 11 | 12 | public NotUniqueUserFieldException(String message) { 13 | super(message); 14 | } 15 | 16 | public NotUniqueUserFieldException(Throwable cause) { 17 | super(cause); 18 | } 19 | 20 | public NotUniqueUserFieldException(String message, Throwable cause) { 21 | super(message, cause); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/search.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" 2 | pageEncoding="UTF-8"%> 3 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 4 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> 5 | <%@ taglib prefix="t" tagdir="/WEB-INF/tags"%> 6 | 7 | 8 | | 9 | 10 | 11 | 12 | 13 |

14 |
15 | 16 | 17 |
18 |
-------------------------------------------------------------------------------- /src/main/resources/META-INF/spring/applicationContext-core.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/success.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" 2 | pageEncoding="UTF-8"%> 3 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> 4 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> 5 | <%@ taglib prefix="t" tagdir="/WEB-INF/tags"%> 6 | 7 | 8 | | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |

17 |
18 |
19 |
-------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/web/constants/View.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.web.constants; 2 | 3 | /** 4 | * JSP views names as constants 5 | * 6 | * @author Oleg Filippov 7 | */ 8 | public class View { 9 | 10 | public static final String ABOUT = "about"; 11 | public static final String EDIT_ARTICLE = "article-edit"; 12 | public static final String VIEW_ARTICLE = "article-view"; 13 | public static final String CONTACTS = "contacts"; 14 | public static final String ERROR = "error"; 15 | public static final String HOME = "home"; 16 | public static final String PROFILE = "profile"; 17 | public static final String SEARCH = "search"; 18 | public static final String SIGN_IN = "signin"; 19 | public static final String SIGN_UP = "signup"; 20 | public static final String SUCCESS = "success"; 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/net/filippov/newsportal/domain/NewsTest.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.domain; 2 | 3 | import java.util.Date; 4 | 5 | import org.junit.Assert; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | 9 | public class NewsTest { 10 | 11 | private Article news; 12 | 13 | @Before 14 | public void init() { 15 | news = new Article(); 16 | } 17 | 18 | @Test 19 | public void testCreatedNotNull() { 20 | Assert.assertNotNull(news.getCreated()); 21 | } 22 | 23 | @Test 24 | public void testSetModified() throws InterruptedException { 25 | news.setLastModified(new Date()); 26 | Date prevModified = news.getLastModified(); 27 | 28 | Thread.sleep(10); 29 | news.setLastModified(new Date()); 30 | 31 | Assert.assertTrue(news.getLastModified().after(prevModified)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/webapp/resources/css/app/signin-signup.css: -------------------------------------------------------------------------------- 1 | .form-signin { 2 | max-width: 330px; 3 | padding: 15px; 4 | margin: 0 auto; 5 | } 6 | 7 | .form-signin .form-signin-heading,.form-signin .checkbox { 8 | margin-bottom: 10px; 9 | } 10 | 11 | .form-signin .checkbox { 12 | font-weight: normal; 13 | } 14 | 15 | .form-signin .form-control { 16 | position: relative; 17 | height: auto; 18 | -webkit-box-sizing: border-box; 19 | -moz-box-sizing: border-box; 20 | box-sizing: border-box; 21 | padding: 10px; 22 | font-size: 16px; 23 | } 24 | 25 | .form-signin .form-control:focus { 26 | z-index: 2; 27 | } 28 | 29 | .form-signin input[type="text"] { 30 | margin-bottom: 10px; 31 | border-bottom-right-radius: 0; 32 | border-bottom-left-radius: 0; 33 | } 34 | 35 | .form-signin input[type="password"] { 36 | margin-bottom: 10px; 37 | border-top-left-radius: 0; 38 | border-top-right-radius: 0; 39 | } 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Newsportal 2 | =========== 3 | 4 | Simple project using Spring MVC, Spring Security, Hibernate, Twitter Bootstrap, H2 db 5 | 6 | **Work in progress** 7 | 8 | [![](http://s28.postimg.org/8s8xl0obd/Home.jpg)](http://s28.postimg.org/egf8bwsnx/Home.png) 9 | [![](http://s11.postimg.org/sxeurrm9b/View_article.jpg)](http://s11.postimg.org/3rdwkxkz7/View_article.png) 10 | [![](http://s10.postimg.org/sxxqhk1h1/Edit_article.jpg)](http://s10.postimg.org/riw5su0e1/Edit_article.png) 11 | 12 | ### How to run 13 | 1. mvn install 14 | 2. mvn tomcat7:run 15 | 3. http://localhost:8080/newsportal/ 16 | 17 | Available users: admin (admin), author (author), user (user) 18 | 19 | ### Features 20 | - Localization (en, ru) 21 | - Generic JPA repository 22 | - Tags autocomplete 23 | - js-validation 24 | - more... 25 | 26 | ### Libraries used 27 | - Spring MVC 3.2, Spring Security 3.1, Hibernate 4.2 28 | - [Twitter Bootstrap 3](http://getbootstrap.com/), [Summernote WYSIWYG editor](http://hackerwins.github.io/summernote/) 29 | - H2 db -------------------------------------------------------------------------------- /.springBeans: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1 4 | 5 | 6 | 7 | 8 | 9 | 10 | src/main/webapp/WEB-INF/spring/webmvc-config.xml 11 | 12 | 13 | 14 | 15 | true 16 | false 17 | 18 | src/main/resources/META-INF/spring/applicationContext-core.xml 19 | src/main/resources/META-INF/spring/applicationContext-security.xml 20 | src/main/webapp/WEB-INF/spring/webmvc-config.xml 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/error.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" 2 | pageEncoding="UTF-8"%> 3 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> 4 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> 5 | <%@ taglib prefix="t" tagdir="/WEB-INF/tags"%> 6 | 7 | 8 | | 9 | 10 | 11 | 12 | 13 |
14 |

15 |
16 |
17 |

18 | 19 |

20 |

21 | 22 |

23 | 24 | 30 | 31 |
32 |
-------------------------------------------------------------------------------- /src/test/resources/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/service/TagService.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.service; 2 | 3 | import java.util.List; 4 | import java.util.Set; 5 | 6 | import net.filippov.newsportal.domain.Tag; 7 | 8 | /** 9 | * Provides tag-related operations 10 | * 11 | * @author Oleg Filippov 12 | */ 13 | public interface TagService extends AbstractService { 14 | 15 | /** 16 | * Get tag from repository by it's name 17 | * 18 | * @param name tag name 19 | * @return tag 20 | */ 21 | Tag getByName(String name); 22 | 23 | /** 24 | * Get all tag names 25 | * 26 | * @return list of tags 27 | */ 28 | List getAllNames(); 29 | 30 | /** 31 | * Get json of all tag names 32 | * 33 | * @return json 34 | */ 35 | String getAutocompleteJson(); 36 | 37 | /** 38 | * Get set of tags from comma-separated string of tags 39 | * 40 | * @param tagString string of tags 41 | * @return set of tags 42 | */ 43 | Set getTagsFromString(String tagString); 44 | 45 | /** 46 | * Get comma-separated string of tags from set of tags 47 | * 48 | * @param tags set of tags 49 | * @return string of tags 50 | */ 51 | String getTagString(Set tags); 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/domain/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.domain; 2 | 3 | import java.io.Serializable; 4 | 5 | import javax.persistence.Column; 6 | import javax.persistence.GeneratedValue; 7 | import javax.persistence.GenerationType; 8 | import javax.persistence.Id; 9 | import javax.persistence.MappedSuperclass; 10 | import javax.persistence.Version; 11 | 12 | /** 13 | * Base class for all entity-classes. Defines the primary id and version. 14 | * 15 | * @author Oleg Filippov 16 | */ 17 | @MappedSuperclass 18 | public abstract class BaseEntity implements Serializable { 19 | 20 | private static final long serialVersionUID = 1520556867799623763L; 21 | 22 | /** 23 | * Primary key of the persistent object 24 | */ 25 | @Id 26 | @GeneratedValue(strategy = GenerationType.AUTO) 27 | @Column(nullable = false, updatable = false) 28 | private Long id; 29 | 30 | /** 31 | * Version of the persistent object 32 | */ 33 | @Version 34 | @Column(nullable = false, insertable = false, columnDefinition = "INT DEFAULT 0") 35 | Integer version; 36 | 37 | /** 38 | * Get the primary key of the persistent object 39 | * 40 | * @return the id 41 | */ 42 | public Long getId() { 43 | return id; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/tags/category-tags.tag: -------------------------------------------------------------------------------- 1 | <%@ tag language="java" description="Displays category abd the list of tags" 2 | pageEncoding="UTF-8"%> 3 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 4 | <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> 5 | <%@ attribute name="article" type="net.filippov.newsportal.domain.Article" 6 | required="true" description="Current article"%> 7 | 8 |
9 |
10 | 11 | 12 | 13 | ${article.author.login} 14 | 15 |   16 | 17 |  |  18 | 19 | 20 | 21 | 22 |  |  23 | 24 | 25 | 26 | 27 |   28 | 29 | 30 |
31 |
-------------------------------------------------------------------------------- /src/main/webapp/resources/js/app/article-edit.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | // Ajax request for tags json 3 | $.get('tags/autocomplete', function (data) { 4 | tagsAutocomplete(JSON.parse(data)); 5 | }); 6 | }); 7 | 8 | //Tokenfield tags using Twitter Typeahead Bloodhound autocomplete 9 | function tagsAutocomplete(tags) { 10 | var engine = new Bloodhound({ 11 | datumTokenizer: function(d) { 12 | return Bloodhound.tokenizers.whitespace(d.value); 13 | }, 14 | queryTokenizer: Bloodhound.tokenizers.whitespace, 15 | local: $.map(tags, function(tag) { return { value: tag }; }) 16 | }); 17 | engine.initialize(); 18 | 19 | $('#tags').tokenfield({ 20 | limit: 5, 21 | minLength: 2, 22 | typeahead: { 23 | source: engine.ttAdapter() 24 | } 25 | }); 26 | } 27 | 28 | // Upload image in the editor 29 | function sendFile(file, editor, welEditable) { 30 | data = new FormData(); 31 | data.append("file", file); 32 | $.ajax({ 33 | data: data, 34 | type: "POST", 35 | url: 'uploadimage', 36 | cache: false, 37 | contentType: false, 38 | processData: false, 39 | success: function(response) { 40 | if (/^images/.test(response)) { 41 | editor.insertImage(welEditable, response); 42 | $("#resp").hide(); 43 | } else { 44 | $("#resp").text(response).show(); 45 | } 46 | } 47 | }); 48 | } -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/service/util/QueryParameters.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.service.util; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | /** 7 | * Class builds map of parameters for a named query 8 | * 9 | * @author Oleg Filippov 10 | */ 11 | public class QueryParameters { 12 | 13 | /** 14 | * Map of parameters 15 | */ 16 | private Map parameters; 17 | 18 | /** 19 | * Private constructor 20 | */ 21 | private QueryParameters(String name, Object value) { 22 | parameters = new HashMap(); 23 | parameters.put(name, value); 24 | } 25 | 26 | /** 27 | * Set parameter 28 | * 29 | * @param name of parameter 30 | * @param value of parameter 31 | * @return new QueryParameters object 32 | */ 33 | public static QueryParameters setParam(String name, Object value) { 34 | return new QueryParameters(name, value); 35 | } 36 | 37 | /** 38 | * Add one more parameter 39 | * 40 | * @param name of parameter 41 | * @param value of parameter 42 | * @return this object 43 | */ 44 | public QueryParameters add(String name, Object value) { 45 | parameters.put(name, value); 46 | return this; 47 | } 48 | 49 | /** 50 | * Build Map with parameters. Must be envoked at the end 51 | * 52 | * @return result Map 53 | */ 54 | public Map buildMap() { 55 | return parameters; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/service/impl/CategoryServiceImpl.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.service.impl; 2 | 3 | import static net.filippov.newsportal.service.util.QueryParameters.setParam; 4 | 5 | import javax.persistence.PersistenceException; 6 | 7 | import net.filippov.newsportal.domain.Category; 8 | import net.filippov.newsportal.exception.ServiceException; 9 | import net.filippov.newsportal.repository.GenericRepository; 10 | import net.filippov.newsportal.service.CategoryService; 11 | 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Service; 14 | 15 | /** 16 | * Implementation of {@link CategoryService} 17 | * 18 | * @author Oleg Filippov 19 | */ 20 | @Service("CategoryService") 21 | public class CategoryServiceImpl extends AbstractServiceImpl 22 | implements CategoryService { 23 | 24 | @Autowired 25 | public CategoryServiceImpl(GenericRepository repository) { 26 | super(repository); 27 | } 28 | 29 | /** 30 | * @see net.filippov.newsportal.service.CategoryService#getByName(java.lang.String) 31 | */ 32 | @Override 33 | public Category getByName(String name) { 34 | try { 35 | return repository.getByNamedQuery("Category.GET_BY_NAME", 36 | setParam("name", name).buildMap()); 37 | } catch (PersistenceException e) { 38 | String message = String.format("Unable to get category=%s", name); 39 | throw new ServiceException(message, e); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/profile.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" 2 | pageEncoding="UTF-8"%> 3 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> 4 | <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> 5 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> 6 | <%@ taglib prefix="t" tagdir="/WEB-INF/tags"%> 7 | 8 | 9 | | 10 | 11 | 12 | 13 | 14 |

15 | ${user.login} 16 |

17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
:
:${user.name}
:${user.email}
${user.articleCount}
${user.commentCount}
41 |
42 |
-------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/tags/article-list.tag: -------------------------------------------------------------------------------- 1 | <%@ tag language="java" description="Displays the list of articles" 2 | pageEncoding="UTF-8"%> 3 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 4 | <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> 5 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> 6 | <%@ taglib prefix="t" tagdir="/WEB-INF/tags"%> 7 | <%@ attribute name="articlesByPage" required="true" 8 | type="java.util.ArrayList" description="List of articles"%> 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |

18 | 19 | 20 | 21 | 22 |

23 | 24 | 25 |

26 | 27 | 28 | 29 | 30 | 31 |

32 | 33 | 34 | » 35 | 36 |

37 |
38 |
39 |
40 | 41 |

42 |
43 |
44 |
-------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/security/CustomAuthenticationSuccessHandler.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.security; 2 | 3 | import java.io.IOException; 4 | 5 | import javax.servlet.ServletException; 6 | import javax.servlet.http.HttpServletRequest; 7 | import javax.servlet.http.HttpServletResponse; 8 | 9 | import net.filippov.newsportal.domain.User; 10 | import net.filippov.newsportal.service.UserService; 11 | 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.security.core.Authentication; 14 | import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler; 15 | import org.springframework.stereotype.Component; 16 | 17 | /** 18 | * Puts logged user id into session 19 | * 20 | * @author Oleg Filippov 21 | */ 22 | @Component("AuthenticationSuccessHandler") 23 | public class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler { 24 | 25 | @Autowired 26 | private UserService service; 27 | 28 | /** 29 | * @see org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler#onAuthenticationSuccess(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse, org.springframework.security.core.Authentication) 30 | */ 31 | @Override 32 | public void onAuthenticationSuccess(HttpServletRequest request, 33 | HttpServletResponse response, Authentication auth) 34 | throws IOException, ServletException { 35 | 36 | User user = service.getByLogin(auth.getName()); 37 | request.getSession().setAttribute("loggedUserId", user.getId()); 38 | 39 | super.onAuthenticationSuccess(request, response, auth); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/signin.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" 2 | pageEncoding="UTF-8"%> 3 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 4 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> 5 | <%@ taglib prefix="t" tagdir="/WEB-INF/tags"%> 6 | 7 | 8 | | 9 | 10 | 11 | 12 | 13 | 45 | 46 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/service/AbstractService.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.service; 2 | 3 | import java.io.Serializable; 4 | import java.util.List; 5 | 6 | /** 7 | * Abstract service providing basic transactional and nontransactional operations 8 | * 9 | * @author Oleg Filippov 10 | */ 11 | public interface AbstractService { 12 | 13 | /** 14 | * Save object into repository 15 | * 16 | * @param obj to save 17 | */ 18 | void add(T obj); 19 | 20 | /** 21 | * Save object transactionally 22 | * 23 | * @param obj to save 24 | */ 25 | void addTransactionally(T obj); 26 | 27 | /** 28 | * Get object from repository 29 | * 30 | * @param id of object 31 | * @return persistent object or null if not found 32 | */ 33 | T get(Long id); 34 | 35 | /** 36 | * Get object from repository transactionally 37 | * 38 | * @param id of object 39 | * @return persistent object or null if not found 40 | */ 41 | T getTransactionally(Long id); 42 | 43 | /** 44 | * Update object 45 | * 46 | * @param obj to update 47 | */ 48 | void update(T obj); 49 | 50 | /** 51 | * Update object transactionally 52 | * 53 | * @param obj to update 54 | */ 55 | void updateTransactionally(T obj); 56 | 57 | /** 58 | * Delete object transactionally 59 | * 60 | * @param obj to delete 61 | */ 62 | void deleteTransactionally(T obj); 63 | 64 | /** 65 | * Delete object by it's id transactionally 66 | * 67 | * @param id of the object to delete 68 | */ 69 | void deleteByIdTransactionally(Long id); 70 | 71 | /** 72 | * Get all objects from repository transactionally 73 | * 74 | * @return list of objects 75 | */ 76 | List getAllTransactionally(); 77 | } 78 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring/applicationContext-security.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 16 | 19 | 21 | 22 | 23 | 24 | 26 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/main/webapp/resources/css/app/header.css: -------------------------------------------------------------------------------- 1 | .navbar-default { 2 | background-color: #6a93c4; 3 | border-color: #ffffff; 4 | } 5 | .navbar-default .navbar-brand { 6 | color: #ffffff; 7 | } 8 | .navbar-default .navbar-brand:hover, .navbar-default .navbar-brand:focus { 9 | color: #203664; 10 | } 11 | .navbar-default .navbar-text { 12 | color: #ffffff; 13 | } 14 | .navbar-default .navbar-nav > li > a { 15 | color: #ffffff; 16 | } 17 | .navbar-default .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus { 18 | color: #203664; 19 | } 20 | .navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a:hover, .navbar-default .navbar-nav > .active > a:focus { 21 | color: #203664; 22 | background-color: #ffffff; 23 | } 24 | .navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .open > a:hover, .navbar-default .navbar-nav > .open > a:focus { 25 | color: #203664; 26 | background-color: #ffffff; 27 | } 28 | .navbar-default .navbar-toggle { 29 | border-color: #ffffff; 30 | } 31 | .navbar-default .navbar-toggle:hover, .navbar-default .navbar-toggle:focus { 32 | background-color: #ffffff; 33 | } 34 | .navbar-default .navbar-toggle .icon-bar { 35 | background-color: #ffffff; 36 | } 37 | .navbar-default .navbar-collapse, 38 | .navbar-default .navbar-form { 39 | border-color: #ffffff; 40 | } 41 | .navbar-default .navbar-link { 42 | color: #ffffff; 43 | } 44 | .navbar-default .navbar-link:hover { 45 | color: #203664; 46 | } 47 | 48 | @media (max-width: 767px) { 49 | .navbar-default .navbar-nav .open .dropdown-menu > li > a { 50 | color: #ffffff; 51 | } 52 | .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { 53 | color: #203664; 54 | } 55 | .navbar-default .navbar-nav .open .dropdown-menu > .active > a, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { 56 | color: #203664; 57 | background-color: #ffffff; 58 | } 59 | } -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/web/ErrorController.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.web; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | 5 | import net.filippov.newsportal.web.constants.View; 6 | import net.filippov.newsportal.web.constants.URL; 7 | 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.stereotype.Controller; 11 | import org.springframework.ui.Model; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | 14 | /** 15 | * Controller for errors 16 | * 17 | * @author Oleg Filippov 18 | */ 19 | @Controller 20 | public class ErrorController { 21 | 22 | private static final Logger LOG = LoggerFactory.getLogger(ErrorController.class); 23 | 24 | /** 25 | * Gets status-code, request_uri and exception from {@link HttpServletRequest} 26 | * and sets them as model-attributes 27 | * 28 | * @return error-page 29 | */ 30 | @RequestMapping(value = URL.ERROR) 31 | public String errorPage(Model model, HttpServletRequest request) { 32 | 33 | Integer statusCode = (Integer) request 34 | .getAttribute("javax.servlet.error.status_code"); 35 | 36 | switch (statusCode) { 37 | case 400: 38 | model.addAttribute("messageProperty", "error.400"); 39 | break; 40 | case 404: 41 | model.addAttribute("messageProperty", "error.404"); 42 | break; 43 | case 500: 44 | model.addAttribute("messageProperty", "error.500"); 45 | break; 46 | default: 47 | model.addAttribute("messageProperty", "error.default"); 48 | } 49 | 50 | String requestUrl = (String) request 51 | .getAttribute("javax.servlet.error.request_uri"); 52 | if (requestUrl == null) 53 | requestUrl = "Unknown"; 54 | 55 | Throwable ex = (Throwable) request.getAttribute("javax.servlet.error.exception"); 56 | if (ex != null) { 57 | LOG.error(ex.getCause().getMessage(), ex.getCause()); 58 | // hidden attribute 59 | model.addAttribute("exception", ex.getCause()); 60 | } 61 | 62 | model.addAttribute("statusCode", statusCode); 63 | model.addAttribute("requestUrl", requestUrl); 64 | 65 | return View.ERROR; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/webapp/resources/css/app/common.css: -------------------------------------------------------------------------------- 1 | a, a#author { 2 | color: #3659BC; 3 | } 4 | 5 | a:hover, a:focus { 6 | color: #3659BC; 7 | text-decoration: underline; 8 | } 9 | 10 | a.sidebar-categories { 11 | color: #868686; 12 | } 13 | 14 | .category-tags a, a.sidebar-tags { 15 | color: #757575; 16 | } 17 | 18 | .category-tags hr { 19 | margin-top: 2px; 20 | margin-bottom: 2px; 21 | } 22 | 23 | .panel-default > .panel-heading { 24 | color: #FFF; 25 | background-color: #6A93C4; 26 | } 27 | 28 | .article-preview { 29 | margin-top: 10px; 30 | margin-bottom: 10px; 31 | } 32 | 33 | .article-content { 34 | margin-top: 15px; 35 | margin-bottom: 10px; 36 | } 37 | 38 | .comment { 39 | background-color: #ebf8fe; 40 | border: 2px solid #666; 41 | margin: 8px; 42 | padding: 10px; 43 | border-radius: 8px; 44 | box-shadow: 2px 2px 2px #999; 45 | } 46 | 47 | .comment-message { 48 | color: #FFFFFF; 49 | background-color: #CD5C5C; 50 | } 51 | 52 | table.profile th { 53 | padding: 2px; 54 | text-align:right; 55 | font-weight:normal; 56 | } 57 | 58 | table.profile td { 59 | padding-left: 5px; 60 | font-weight:bold; 61 | } 62 | 63 | select.form-control { 64 | width: 50%; 65 | margin-top: 10px; 66 | } 67 | 68 | .pagination > .active > a, .pagination > .active > span, .pagination > .active > a:hover, .pagination > .active > span:hover, .pagination > .active > a:focus, .pagination > .active > span:focus { 69 | color: #FFFFFF; 70 | border-color: #696969; 71 | background-color: #696969; 72 | } 73 | 74 | .pagination > li > a:hover, .pagination > li > span:hover, .pagination > li > a:focus, .pagination > li > span:focus { 75 | color: #2D2D2D; 76 | background-color: #EEE; 77 | } 78 | 79 | .pagination > li > a, .pagination > li > span, .pagination > li > a, .pagination > li > span { 80 | color: #2D2D2D; 81 | } 82 | 83 | /* Tag sizes */ 84 | .tag0 { font-size: 1.00em; } 85 | .tag1 { font-size: 1.10em; } 86 | .tag2 { font-size: 1.20em; } 87 | .tag3 { font-size: 1.30em; } 88 | .tag4 { font-size: 1.40em; } 89 | .tag5 { font-size: 1.50em; } 90 | .tag6 { font-size: 1.60em; } 91 | .tag7 { font-size: 1.70em; } 92 | .tag8 { font-size: 1.80em; } 93 | .tag9 { font-size: 1.90em; } -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/tags/pagination.tag: -------------------------------------------------------------------------------- 1 | <%@ tag language="java" description="Displays a list of news" pageEncoding="UTF-8"%> 2 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 3 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> 4 | <%@ attribute name="pageCount" required="true" description="Number of pages"%> 5 | <%@ attribute name="currentPage" required="true"%> 6 | 7 | 8 | 9 | 10 | 11 |
12 |
    13 | 14 | 15 |
  • 1
  • 16 | 17 | 18 | 19 |
  • ...
  • 20 |
    21 | 22 | 23 |
  • 2
  • 24 |
    25 |
    26 |
    27 |
    28 | 29 | 30 | 31 | 32 | 33 |
  • ${pageNum-1}
  • 34 |
    35 | 36 | 37 |
  • ${pageNum-1}
  • 38 |
    39 |
    40 |
    41 |
    42 | 43 | 44 | 45 | 46 | 47 | 48 |
  • ...
  • 49 |
    50 | 51 | 52 |
  • ${pageCount-1}
  • 53 |
    54 |
    55 |
    56 |
  • ${pageCount}
  • 57 |
    58 |
59 |
60 |
-------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/common/sidebar.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 2 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> 3 | <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%> 4 | 5 | 6 | 7 | 8 |

9 |
10 |
11 | 12 | 13 | 17 |
18 | 19 | 20 |
21 |
22 |
23 |
    24 | 25 | 26 |
  1. 27 | 28 | 29 | 30 | 31 |
  2. 32 |
    33 |
34 |
35 |
36 |
37 | 38 | 39 |
40 |
41 |
42 | 43 | 44 | 46 | 47 | 48 | 49 |
50 |
51 |
52 | 53 | 54 |
55 |
56 |
57 |
    58 |
  1. Project on GitHub
  2. 59 |
  3. Summernote WYSIWYG editor
  4. 60 |
  5. Typeahead
  6. 61 |
62 |
63 |
-------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/security/CustomUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.security; 2 | 3 | import java.util.Collection; 4 | import java.util.HashSet; 5 | 6 | import net.filippov.newsportal.domain.UserRole; 7 | import net.filippov.newsportal.service.UserService; 8 | 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.security.core.GrantedAuthority; 11 | import org.springframework.security.core.userdetails.User; 12 | import org.springframework.security.core.userdetails.UserDetails; 13 | import org.springframework.security.core.userdetails.UserDetailsService; 14 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 15 | import org.springframework.stereotype.Component; 16 | 17 | /** 18 | * Implementation of {@link UserDetailsService} 19 | * 20 | * @author Oleg Filippov 21 | */ 22 | @Component("UserDetailsService") 23 | public class CustomUserDetailsService implements UserDetailsService { 24 | 25 | @Autowired 26 | private UserService service; 27 | 28 | /** 29 | * @see org.springframework.security.core.userdetails.UserDetailsService#loadUserByUsername(java.lang.String) 30 | */ 31 | @Override 32 | public UserDetails loadUserByUsername(String login) 33 | throws UsernameNotFoundException { 34 | 35 | if ("".equals(login)) { 36 | throw new UsernameNotFoundException("User not found"); 37 | } 38 | 39 | net.filippov.newsportal.domain.User user = service.getByLogin(login); 40 | 41 | if (user == null) { 42 | throw new UsernameNotFoundException("User not found"); 43 | } 44 | 45 | boolean accountNonExpired = true; 46 | boolean credentialsNonExpired = true; 47 | 48 | return new User( 49 | user.getLogin(), 50 | user.getPassword(), 51 | user.isEnabled(), 52 | accountNonExpired, 53 | credentialsNonExpired, 54 | !user.isLocked(), 55 | getAuthorities(user.getRoles())); 56 | } 57 | 58 | /** 59 | * Get set of roles. Roles must be {@link GrantedAuthority} implementation 60 | * 61 | * @param roles 62 | * @return 63 | */ 64 | public Collection getAuthorities( 65 | Collection roles) { 66 | 67 | Collection resultRoles = new HashSet(); 68 | for (UserRole role : roles) { 69 | resultRoles.add(role); 70 | } 71 | return resultRoles; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/web/constants/URL.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.web.constants; 2 | 3 | /** 4 | * Collects all URL's as static constants in one place 5 | * 6 | * @author Oleg Filippov 7 | */ 8 | public class URL { 9 | 10 | /** 11 | * Main URL's 12 | */ 13 | public static final String HOME = "/"; 14 | public static final String HOME_CUSTOM_PAGE = "/page/{number}"; 15 | public static final String ABOUT = "/about"; 16 | public static final String CONTACTS = "/contacts"; 17 | public static final String ERROR = "/error"; 18 | 19 | /** 20 | * User-related URL's 21 | */ 22 | public static final String SIGN_IN = "/signin"; 23 | public static final String SIGN_IN_FAILURE = "/signinfailure"; 24 | public static final String SIGN_UP = "/signup"; 25 | public static final String CHECK_LOGIN = "/check-login"; 26 | public static final String CHECK_EMAIL = "/check-email"; 27 | public static final String USER_PROFILE = "/user/{id}"; 28 | 29 | /** 30 | * Upload URL's 31 | */ 32 | public static final String UPLOAD_IMAGE = "**/uploadimage"; 33 | public static final String SHOW_IMAGE = "**/images/{name}.{type}"; 34 | 35 | /** 36 | * Article-related URL's 37 | */ 38 | public static final String SHOW_ARTICLE = "/{id}"; 39 | public static final String ADD_COMMENT = "/{id}/addcomment"; 40 | public static final String ADD_ARTICLE = "/add"; 41 | public static final String EDIT_ARTICLE = "/{id}/edit"; 42 | public static final String DELETE_ARTICLE = "/{id}/delete"; 43 | public static final String CANCEL = "/{id}/cancel"; 44 | public static final String TAGS_AUTOCOMPLETE = "**/tags/autocomplete"; 45 | 46 | /** 47 | * Search URL's 48 | */ 49 | public static final String SEARCH_BY_CATEGORY = "/category/{name}"; 50 | public static final String SEARCH_BY_CATEGORY_CUSTOM_PAGE = "/category/{name}/page/{number}"; 51 | public static final String SEARCH_BY_FRAGMENT = "/search/{fragment}"; 52 | public static final String SEARCH_BY_FRAGMENT_CUSTOM_PAGE = "/search/{fragment}/page/{number}"; 53 | public static final String SEARCH_BY_FRAGMENT_SUBMIT = "/search"; 54 | public static final String SEARCH_BY_TAG = "/tags/{name}"; 55 | public static final String SEARCH_BY_TAG_CUSTOM_PAGE = "/tags/{name}/page/{number}"; 56 | public static final String SEARCH_BY_USER = "/user/{id}/articles"; 57 | public static final String SEARCH_BY_USER_CUSTOM_PAGE = "/user/{id}/articles/page/{number}"; 58 | } 59 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 8 | 9 | contextConfigLocation 10 | 11 | classpath:/META-INF/spring/applicationContext-core.xml 12 | classpath:/META-INF/spring/applicationContext-persistence.xml 13 | classpath:/META-INF/spring/applicationContext-security.xml 14 | 15 | 16 | 17 | 18 | 19 | org.springframework.web.context.ContextLoaderListener 20 | 21 | 22 | 23 | springSecurityFilterChain 24 | org.springframework.web.filter.DelegatingFilterProxy 25 | 26 | 27 | 28 | springSecurityFilterChain 29 | /* 30 | 31 | 32 | 33 | encodingFilter 34 | org.springframework.web.filter.CharacterEncodingFilter 35 | 36 | encoding 37 | UTF-8 38 | 39 | 40 | forceEncoding 41 | true 42 | 43 | 44 | 45 | encodingFilter 46 | /* 47 | 48 | 49 | 50 | 51 | appServlet 52 | org.springframework.web.servlet.DispatcherServlet 53 | 54 | contextConfigLocation 55 | /WEB-INF/spring/webmvc-config.xml 56 | 57 | 1 58 | 59 | 60 | 61 | appServlet 62 | / 63 | 64 | 65 | 66 | 67 | COOKIE 68 | 69 | 70 | 71 | 72 | /error 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/tags/template.tag: -------------------------------------------------------------------------------- 1 | <%@ tag language="java" description="page template" pageEncoding="UTF-8"%> 2 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 3 | <%@ attribute name="title"%> 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | " /> 12 | " /> 13 | " /> 14 | " /> 15 | " /> 16 | 17 | " /> 18 | " /> 19 | " /> 20 | " /> 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ${title} 33 | 34 | 35 |
36 | 37 |
38 |
39 |
40 | 41 |
42 |
43 | 44 |
45 |
46 |
47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /src/main/resources/ehcache.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 17 | 18 | 19 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | 37 | 43 | 44 | 45 | 46 | 52 | 53 | 54 | 55 | 61 | 62 | 63 | 64 | 70 | 71 | 72 | 73 | 76 | 77 | 78 | 79 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/web/CustomExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.web; 2 | 3 | import java.io.IOException; 4 | 5 | import net.filippov.newsportal.exception.UnacceptableFileFormatException; 6 | 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.context.ApplicationContext; 11 | import org.springframework.context.i18n.LocaleContextHolder; 12 | import org.springframework.web.bind.annotation.ControllerAdvice; 13 | import org.springframework.web.bind.annotation.ExceptionHandler; 14 | import org.springframework.web.bind.annotation.ResponseBody; 15 | import org.springframework.web.multipart.MaxUploadSizeExceededException; 16 | import org.springframework.web.multipart.MultipartException; 17 | 18 | /** 19 | * Exception-handler 20 | * Catches an exception, logs it and sends validation message as @ResponseBody 21 | * 22 | * @author Oleg Filippov 23 | */ 24 | @ControllerAdvice 25 | public class CustomExceptionHandler { 26 | 27 | private static final Logger LOG = LoggerFactory.getLogger(CustomExceptionHandler.class); 28 | 29 | @Autowired 30 | private ApplicationContext context; 31 | 32 | /** 33 | * Handles {@link MaxUploadSizeExceededException} 34 | * Image size is more than allowed 35 | * 36 | * @param e exception to catch 37 | * @return validation message 38 | */ 39 | @ExceptionHandler(MaxUploadSizeExceededException.class) 40 | @ResponseBody 41 | public String handleMaxUploadSizeException(MaxUploadSizeExceededException e) { 42 | 43 | LOG.error(e.getMessage(), e); 44 | String message = context.getMessage("validation.file.maxSize", null, 45 | LocaleContextHolder.getLocale()); 46 | return message; 47 | } 48 | 49 | /** 50 | * Handles {@link UnacceptableFileFormatException} 51 | * An attempt to upload the image with not allowed format 52 | * 53 | * @param e exception to catch 54 | * @return validation message 55 | */ 56 | @ExceptionHandler(UnacceptableFileFormatException.class) 57 | @ResponseBody 58 | public String handleFileFormatException(UnacceptableFileFormatException e) { 59 | 60 | LOG.error(e.getMessage(), e); 61 | String message = context.getMessage("validation.file.type", null, 62 | LocaleContextHolder.getLocale()); 63 | return message; 64 | } 65 | 66 | /** 67 | * Handles {@link IOException}, {@link MultipartException} 68 | * File-system exceptions during image-upload process 69 | * 70 | * @param e exception to catch 71 | * @return validation message 72 | */ 73 | @ExceptionHandler(value = {IOException.class, MultipartException.class}) 74 | @ResponseBody 75 | public String handleCustomException(Exception e) { 76 | 77 | LOG.error(e.getMessage(), e); 78 | String message = context.getMessage("validation.file.custom", null, 79 | LocaleContextHolder.getLocale()); 80 | return message; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/resources/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/web/FileUploadController.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.web; 2 | 3 | import java.io.File; 4 | import java.io.FileInputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.util.Calendar; 8 | 9 | import javax.servlet.http.HttpServletResponse; 10 | 11 | import net.filippov.newsportal.exception.UnacceptableFileFormatException; 12 | import net.filippov.newsportal.web.constants.URL; 13 | 14 | import org.apache.commons.io.FileUtils; 15 | import org.springframework.stereotype.Controller; 16 | import org.springframework.util.FileCopyUtils; 17 | import org.springframework.web.bind.annotation.PathVariable; 18 | import org.springframework.web.bind.annotation.RequestMapping; 19 | import org.springframework.web.bind.annotation.RequestMethod; 20 | import org.springframework.web.bind.annotation.RequestParam; 21 | import org.springframework.web.bind.annotation.ResponseBody; 22 | import org.springframework.web.multipart.MultipartFile; 23 | 24 | /** 25 | * Controller for file uploading operations 26 | * 27 | * @author Oleg Filippov 28 | */ 29 | @Controller 30 | public class FileUploadController { 31 | 32 | /** 33 | * Constants operating with images 34 | */ 35 | private static final String ARTICLE_IMAGES_PATH = "c:/Newsportal/article_images/"; 36 | private static final String JPG_CONTENT_TYPE = "image/jpeg"; 37 | private static final String PNG_CONTENT_TYPE = "image/png"; 38 | 39 | /** 40 | * Upload image submit 41 | */ 42 | @RequestMapping(method = RequestMethod.POST, value = URL.UPLOAD_IMAGE) 43 | @ResponseBody 44 | public String uploadimage(@RequestParam("file") MultipartFile image) 45 | throws IOException { 46 | 47 | String imageName = Calendar.getInstance().getTimeInMillis() 48 | + image.getOriginalFilename(); 49 | 50 | if (!image.isEmpty()) { 51 | String imageType = image.getContentType(); 52 | if (!(imageType.equals(JPG_CONTENT_TYPE) || imageType 53 | .equals(PNG_CONTENT_TYPE))) { 54 | 55 | // TODO: additional validation based on the content of the file 56 | 57 | throw new UnacceptableFileFormatException(); 58 | } 59 | 60 | File file = new File(ARTICLE_IMAGES_PATH + imageName); 61 | FileUtils.writeByteArrayToFile(file, image.getBytes()); 62 | } 63 | 64 | return "images/" + imageName; 65 | } 66 | 67 | /** 68 | * Get image from file-system 69 | * 70 | * @param imageName image-name 71 | * @param type extension of image 72 | * @param response {@link HttpServletResponse} 73 | * @throws IOException 74 | */ 75 | @RequestMapping(method = RequestMethod.GET, value = URL.SHOW_IMAGE) 76 | public void showImg(@PathVariable("name") String imageName, 77 | @PathVariable("type") String type, HttpServletResponse response) 78 | throws IOException { 79 | 80 | try (InputStream in = new FileInputStream(ARTICLE_IMAGES_PATH 81 | + imageName + "." + type)) { 82 | FileCopyUtils.copy(in, response.getOutputStream()); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/webapp/resources/js/bootstrap-maxlength.min.js: -------------------------------------------------------------------------------- 1 | /* ========================================================== 2 | * 3 | * bootstrap-maxlength.js v 1.5.0 4 | * Copyright 2013 Maurizio Napoleoni @mimonap 5 | * Licensed under MIT License 6 | * URL: https://github.com/mimo84/bootstrap-maxlength/blob/master/LICENSE 7 | * 8 | * ========================================================== */ 9 | 10 | !function(a){"use strict";a.fn.extend({maxlength:function(b,c){function d(a){var c=a.val(),d=c.match(/\n/g),f=0,g=0;return b.utf8?(f=d?e(d):0,g=e(a.val())+f):(f=d?d.length:0,g=a.val().length+f),g}function e(a){for(var b=0,c=0;cd?b++:b+=d>127&&2048>d?2:3}return b}function f(a,c,e){var f=!0;return!b.alwaysShow&&e-d(a)>c&&(f=!1),f}function g(a,b){var c=b-d(a);return c}function h(a){a.css({display:"block"})}function i(a){a.css({display:"none"})}function j(a,c){var d="";return b.message?d=b.message.replace("%charsTyped%",c).replace("%charsRemaining%",a-c).replace("%charsTotal%",a):(b.preText&&(d+=b.preText),d+=b.showCharsTyped?c:a-c,b.showMaxLength&&(d+=b.separator+a),b.postText&&(d+=b.postText)),d}function k(a,c,d,e){e.html(j(d,d-a)),a>0?f(c,b.threshold,d)?h(e.removeClass(b.limitReachedClass).addClass(b.warningClass)):i(e):h(e.removeClass(b.warningClass).addClass(b.limitReachedClass))}function l(b){var c=b[0];return a.extend({},"function"==typeof c.getBoundingClientRect?c.getBoundingClientRect():{width:c.offsetWidth,height:c.offsetHeight},b.offset())}function m(a,c){var d=l(a),e=a.outerWidth(),f=c.outerWidth(),g=c.width(),h=c.height();switch(b.placement){case"bottom":c.css({top:d.top+d.height,left:d.left+d.width/2-g/2});break;case"top":c.css({top:d.top-h,left:d.left+d.width/2-g/2});break;case"left":c.css({top:d.top+d.height/2-h/2,left:d.left-g});break;case"right":c.css({top:d.top+d.height/2-h/2,left:d.left+d.width});break;case"bottom-right":c.css({top:d.top+d.height,left:d.left+d.width});break;case"top-right":c.css({top:d.top-h,left:d.left+e});break;case"top-left":c.css({top:d.top-h,left:d.left-f});break;case"bottom-left":c.css({top:d.top+a.outerHeight(),left:d.left-f});break;case"centered-right":c.css({top:d.top+h/2,left:d.left+e-f-3})}}function n(a){return a.attr("maxlength")||a.attr("size")}var o=a("body"),p={alwaysShow:!1,threshold:10,warningClass:"label label-success",limitReachedClass:"label label-important",separator:" / ",preText:"",postText:"",showMaxLength:!0,placement:"bottom",showCharsTyped:!0,validate:!1,utf8:!1};return a.isFunction(b)&&!c&&(c=b,b={}),b=a.extend(p,b),this.each(function(){var c,d,e=a(this);e.focus(function(){var b=j(c,"0");c=n(e),d=a('').css({display:"none",position:"absolute",whiteSpace:"nowrap",zIndex:1099}).html(b),e.is("textarea")&&(e.data("maxlenghtsizex",e.outerWidth()),e.data("maxlenghtsizey",e.outerHeight()),e.mouseup(function(){(e.outerWidth()!==e.data("maxlenghtsizex")||e.outerHeight()!==e.data("maxlenghtsizey"))&&m(e,d),e.data("maxlenghtsizex",e.outerWidth()),e.data("maxlenghtsizey",e.outerHeight())})),o.append(d);var f=g(e,n(e));k(f,e,c,d),m(e,d)}),e.blur(function(){d.remove()}),e.keyup(function(a){var f=g(e,n(e)),h=!0;return a.keyCode||a.which,b.validate&&0>f?h=!1:k(f,e,c,d),h})})}})}(jQuery); -------------------------------------------------------------------------------- /src/main/webapp/resources/css/tokenfield-typeahead.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * bootstrap-tokenfield 3 | * https://github.com/sliptree/bootstrap-tokenfield 4 | * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT 5 | */.twitter-typeahead{width:100%;position:relative;vertical-align:top}.twitter-typeahead .tt-input,.twitter-typeahead .tt-hint{margin:0;width:100%;vertical-align:middle;background-color:#fff}.twitter-typeahead .tt-hint{color:#999;z-index:1;border:1px solid transparent}.twitter-typeahead .tt-input{color:#555;z-index:2}.twitter-typeahead .tt-input,.twitter-typeahead .tt-hint{height:34px;padding:6px 12px;font-size:14px;line-height:1.428571429}.twitter-typeahead .input-sm.tt-input,.twitter-typeahead .hint-sm.tt-hint{border-radius:3px}.twitter-typeahead .input-lg.tt-input,.twitter-typeahead .hint-lg.tt-hint{border-radius:6px}.input-group .twitter-typeahead:first-child .tt-input,.input-group .twitter-typeahead:first-child .tt-hint{border-radius:4px 0 0 4px!important}.input-group .twitter-typeahead:last-child .tt-input,.input-group .twitter-typeahead:last-child .tt-hint{border-radius:0 4px 4px 0!important}.input-group.input-group-sm .twitter-typeahead:first-child .tt-input,.input-group.input-group-sm .twitter-typeahead:first-child .tt-hint{border-radius:3px 0 0 3px!important}.input-group.input-group-sm .twitter-typeahead:last-child .tt-input,.input-group.input-group-sm .twitter-typeahead:last-child .tt-hint{border-radius:0 3px 3px 0!important}.input-sm.tt-input,.hint-sm.tt-hint,.input-group.input-group-sm .tt-input,.input-group.input-group-sm .tt-hint{height:30px;padding:5px 10px;font-size:12px;line-height:1.5}.input-group.input-group-lg .twitter-typeahead:first-child .tt-input,.input-group.input-group-lg .twitter-typeahead:first-child .tt-hint{border-radius:6px 0 0 6px!important}.input-group.input-group-lg .twitter-typeahead:last-child .tt-input,.input-group.input-group-lg .twitter-typeahead:last-child .tt-hint{border-radius:0 6px 6px 0!important}.input-lg.tt-input,.hint-lg.tt-hint,.input-group.input-group-lg .tt-input,.input-group.input-group-lg .tt-hint{height:45px;padding:10px 16px;font-size:18px;line-height:1.33}.tt-dropdown-menu{width:100%;min-width:160px;margin-top:2px;padding:5px 0;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);*border-right-width:2px;*border-bottom-width:2px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box}.tt-suggestion{display:block;padding:3px 20px}.tt-suggestion.tt-cursor{color:#262626;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0)}.tt-suggestion.tt-cursor a{color:#fff}.tt-suggestion p{margin:0}.tokenfield .twitter-typeahead{width:auto}.tokenfield .twitter-typeahead .tt-hint{padding:0;height:20px}.tokenfield.input-sm .twitter-typeahead .tt-input,.tokenfield.input-sm .twitter-typeahead .tt-hint{height:18px;font-size:12px;line-height:1.5}.tokenfield.input-lg .twitter-typeahead .tt-input,.tokenfield.input-lg .twitter-typeahead .tt-hint{height:23px;font-size:18px;line-height:1.33}.tokenfield .twitter-typeahead .tt-suggestions{font-size:14px} -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/spring/webmvc-config.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 50 | 51 | 52 | 53 | 54 | 56 | 57 | 58 | 59 | 61 | 63 | 64 | 65 | 66 | 67 | 68 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/domain/UserRole.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.domain; 2 | 3 | import java.util.Set; 4 | 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.FetchType; 8 | import javax.persistence.ManyToMany; 9 | import javax.persistence.NamedQueries; 10 | import javax.persistence.NamedQuery; 11 | import javax.persistence.Table; 12 | 13 | import org.hibernate.annotations.Cache; 14 | import org.hibernate.annotations.CacheConcurrencyStrategy; 15 | import org.hibernate.annotations.Immutable; 16 | import org.springframework.security.core.GrantedAuthority; 17 | 18 | /** 19 | * Implementation of {@link GrantedAuthority} 20 | * Represents user-roles (ROLE_USER, ROLE_AUTHOR, ROLE_ADMIN) 21 | * 22 | * @author Oleg Filippov 23 | */ 24 | @Entity 25 | @Table(name = "role") 26 | @Immutable 27 | @Cache(usage = CacheConcurrencyStrategy.READ_ONLY) 28 | @NamedQueries({ 29 | @NamedQuery( 30 | name = "UserRole.GET_BY_AUTHORITY", 31 | query = "FROM UserRole ur WHERE ur.authority = :authority") 32 | }) 33 | public class UserRole extends BaseEntity implements GrantedAuthority { 34 | 35 | private static final long serialVersionUID = 3571970651167544190L; 36 | 37 | /** 38 | * Name of role 39 | */ 40 | @Column(name = "authority", nullable = false, unique = true, length = 20) 41 | private String authority; 42 | 43 | /** 44 | * Set of users having this role 45 | */ 46 | @ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY) 47 | private Set users; 48 | 49 | /** 50 | * Default constructor 51 | */ 52 | public UserRole() {} 53 | 54 | /** 55 | * @return users granted with this role 56 | */ 57 | public Set getUsers() { 58 | return users; 59 | } 60 | 61 | /** 62 | * @param users users granted with this role 63 | */ 64 | public void setUsers(Set users) { 65 | this.users = users; 66 | } 67 | 68 | /** 69 | * @see org.springframework.security.core.GrantedAuthority#getAuthority() 70 | */ 71 | @Override 72 | public String getAuthority() { 73 | return authority; 74 | } 75 | 76 | /** 77 | * @param authority 78 | */ 79 | public void setAuthority(String authority) { 80 | this.authority = authority; 81 | } 82 | 83 | /** 84 | * @see java.lang.Object#hashCode() 85 | */ 86 | @Override 87 | public int hashCode() { 88 | final int prime = 31; 89 | int result = 17; 90 | result = prime * result 91 | + ((getAuthority() == null) ? 0 : getAuthority().hashCode()); 92 | return result; 93 | } 94 | 95 | /** 96 | * @see java.lang.Object#equals(java.lang.Object) 97 | */ 98 | @Override 99 | public boolean equals(Object obj) { 100 | if (this == obj) 101 | return true; 102 | if (obj == null) 103 | return false; 104 | if (getClass() != obj.getClass()) 105 | return false; 106 | 107 | UserRole other = (UserRole) obj; 108 | 109 | if (getAuthority() != null 110 | ? !getAuthority().equals(other.getAuthority()) 111 | : other.getAuthority() != null) { 112 | return false; 113 | } 114 | return true; 115 | } 116 | 117 | /** 118 | * @see java.lang.Object#toString() 119 | */ 120 | @Override 121 | public String toString() { 122 | return String.format("UserRole[id=%d, authority=%s]", 123 | getId(), getAuthority()); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/webapp/resources/js/summernote-ru-RU.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | $.extend($.summernote.lang, { 3 | 'ru-RU': { 4 | font: { 5 | bold: 'Полужирный', 6 | italic: 'Курсив', 7 | underline: 'Подчёркнутый', 8 | strike: 'Зачёркнутый', 9 | clear: 'Убрать стили шрифта', 10 | height: 'Высота линии', 11 | size: 'Размер шрифта' 12 | }, 13 | image: { 14 | image: 'Картинка', 15 | insert: 'Вставить картинку', 16 | resizeFull: 'Восстановить размер', 17 | resizeHalf: 'Уменьшить до 50%', 18 | resizeQuarter: 'Уменьшить до 25%', 19 | floatLeft: 'Расположить слева', 20 | floatRight: 'Расположить справа', 21 | floatNone: 'Расположение по-умолчанию', 22 | dragImageHere: 'Перетащите сюда картинку', 23 | selectFromFiles: 'Выбрать из файлов', 24 | url: 'URL картинки', 25 | remove: 'Удалить картинку' 26 | }, 27 | link: { 28 | link: 'Ссылка', 29 | insert: 'Вставить ссылку', 30 | unlink: 'Убрать ссылку', 31 | edit: 'Редактировать', 32 | textToDisplay: 'Отображаемый текст', 33 | url: 'URL для перехода', 34 | openInNewWindow: 'Открывать в новом окне' 35 | }, 36 | video: { 37 | video: 'Видео', 38 | videoLink: 'Ссылка на видео', 39 | insert: 'Вставить видео', 40 | url: 'URL видео', 41 | providers: '(YouTube, Vimeo, Vine, Instagram или DailyMotion)' 42 | }, 43 | table: { 44 | table: 'Таблица' 45 | }, 46 | hr: { 47 | insert: 'Вставить горизонтальную линию' 48 | }, 49 | style: { 50 | style: 'Стиль', 51 | normal: 'Нормальный', 52 | blockquote: 'Цитата', 53 | pre: 'Код', 54 | h1: 'Заголовок 1', 55 | h2: 'Заголовок 2', 56 | h3: 'Заголовок 3', 57 | h4: 'Заголовок 4', 58 | h5: 'Заголовок 5', 59 | h6: 'Заголовок 6' 60 | }, 61 | lists: { 62 | unordered: 'Маркированный список', 63 | ordered: 'Нумерованный список' 64 | }, 65 | options: { 66 | help: 'Помощь', 67 | fullscreen: 'На весь экран', 68 | codeview: 'Исходный код' 69 | }, 70 | paragraph: { 71 | paragraph: 'Параграф', 72 | outdent: 'Уменьшить отступ', 73 | indent: 'Увеличить отступ', 74 | left: 'Выровнять по левому краю', 75 | center: 'Выровнять по центру', 76 | right: 'Выровнять по правому краю', 77 | justify: 'Растянуть по ширине' 78 | }, 79 | color: { 80 | recent: 'Последний цвет', 81 | more: 'Еще цвета', 82 | background: 'Цвет фона', 83 | foreground: 'Цвет шрифта', 84 | transparent: 'Прозрачный', 85 | setTransparent: 'Сделать прозрачным', 86 | reset: 'Сброс', 87 | resetToDefault: 'Восстановить умолчания' 88 | }, 89 | shortcut: { 90 | shortcuts: 'Сочетания клавиш', 91 | close: 'Закрыть', 92 | textFormatting: 'Форматирование текста', 93 | action: 'Действие', 94 | paragraphFormatting: 'Форматирование параграфа', 95 | documentStyle: 'Стиль документа', 96 | extraKeys: 'Дополнительные комбинации' 97 | }, 98 | history: { 99 | undo: 'Отменить', 100 | redo: 'Повтор' 101 | } 102 | } 103 | }); 104 | })(jQuery); 105 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring/applicationContext-persistence.xml: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 42 | 43 | 44 | 45 | 46 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ${hibernate.cache.region.factory_class} 60 | ${hibernate.cache.use_second_level_cache} 61 | ${hibernate.cache.use_query_cache} 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/main/webapp/resources/js/validator.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Validator v0.3.0 for Bootstrap 3, by @1000hz 3 | * Copyright 2014 Spiceworks, Inc. 4 | * Licensed under http://opensource.org/licenses/MIT 5 | * 6 | * https://github.com/1000hz/bootstrap-validator 7 | */ 8 | 9 | +function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=c,this.toggleSubmit(),this.$element.on("input.bs.validator change.bs.validator focusout.bs.validator",a.proxy(this.validateInput,this)),this.$element.find("[data-match]").each(function(){var b=a(this),c=b.data("match");a(c).on("input.bs.validator",function(){b.val()&&b.trigger("input")})})};b.DEFAULTS={delay:500,errors:{match:"Does not match",minlength:"Not long enough"}},b.VALIDATORS={"native":function(a){var b=a[0];return b.checkValidity?b.checkValidity():!0},match:function(b){var c=b.data("match");return!b.val()||b.val()===a(c).val()},minlength:function(a){var b=a.data("minlength");return!a.val()||a.val().length>=b}},b.prototype.validateInput=function(b){var c,d=a(b.target),e=d.data("bs.errors");this.$element.trigger(b=a.Event("validate.bs.validator",{relatedTarget:d[0]})),b.isDefaultPrevented()||(d.data("bs.errors",c=this.runValidators(d)),c.length?this.showErrors(d):this.clearErrors(d),e&&c.toString()===e.toString()||(b=c.length?a.Event("invalid.bs.validator",{relatedTarget:d[0],detail:c}):a.Event("valid.bs.validator",{relatedTarget:d[0],detail:e}),this.$element.trigger(b)),this.toggleSubmit(),this.$element.trigger(a.Event("validated.bs.validator",{relatedTarget:d[0]})))},b.prototype.runValidators=function(c){{var d=[];[b.VALIDATORS.native]}return a.each(b.VALIDATORS,a.proxy(function(a,b){if((c.data(a)||"native"==a)&&!b.call(this,c)){var e=c.data(a+"-error")||c.data("error")||"native"==a&&c[0].validationMessage||this.options.errors[a];!~d.indexOf(e)&&d.push(e)}},this)),d},b.prototype.validate=function(){var a=this.options.delay;return this.options.delay=0,this.$element.find(":input").trigger("input"),this.options.delay=a,this},b.prototype.showErrors=function(b){function c(){var c=b.closest(".form-group"),d=c.find(".help-block.with-errors"),e=b.data("bs.errors");e.length&&(e=a("
    ").addClass("list-unstyled").append(a.map(e,function(b){return a("
  • ").text(b)})),void 0===d.data("bs.originalContent")&&d.data("bs.originalContent",d.html()),d.empty().append(e),c.addClass("has-error"))}this.options.delay?(window.clearTimeout(b.data("bs.timeout")),b.data("bs.timeout",window.setTimeout(c,this.options.delay))):c()},b.prototype.clearErrors=function(a){var b=a.closest(".form-group"),c=b.find(".help-block.with-errors");c.html(c.data("bs.originalContent")),b.removeClass("has-error")},b.prototype.hasErrors=function(){function b(){return!!(a(this).data("bs.errors")||[]).length}return!!this.$element.find(":input").filter(b).length},b.prototype.isIncomplete=function(){function b(){return"checkbox"===this.type?!this.checked:"radio"===this.type?!a('[name="'+this.name+'"]:checked').length:""===a.trim(this.value)}return!!this.$element.find("[required]").filter(b).length},b.prototype.toggleSubmit=function(){var a=this.$element.find(":submit");a.attr("disabled",this.isIncomplete()||this.hasErrors())};var c=a.fn.validator;a.fn.validator=function(c){return this.each(function(){var d=a(this),e=a.extend({},b.DEFAULTS,d.data(),"object"==typeof c&&c),f=d.data("bs.validator");f||d.data("bs.validator",f=new b(this,e)),"string"==typeof c&&f[c]()})},a.fn.validator.Constructor=b,a.fn.validator.noConflict=function(){return a.fn.validator=c,this},a(window).on("load",function(){a('form[data-toggle="validator"]').each(function(){var b=a(this);b.validator(b.data())})})}(jQuery); -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/domain/Category.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.domain; 2 | 3 | import java.util.Set; 4 | 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.FetchType; 8 | import javax.persistence.NamedQueries; 9 | import javax.persistence.NamedQuery; 10 | import javax.persistence.OneToMany; 11 | import javax.persistence.Table; 12 | 13 | import org.hibernate.annotations.Cache; 14 | import org.hibernate.annotations.CacheConcurrencyStrategy; 15 | import org.hibernate.annotations.Formula; 16 | 17 | /** 18 | * Represents category of article 19 | * 20 | * @author Oleg Filippov 21 | */ 22 | @Entity 23 | @Table(name = "category") 24 | @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) 25 | @NamedQueries({ 26 | @NamedQuery( 27 | name = "Category.GET_ALL", 28 | query = "FROM Category c ORDER BY c.name"), 29 | @NamedQuery( 30 | name = "Category.GET_BY_NAME", 31 | query = "FROM Category c WHERE c.name = :name") 32 | }) 33 | public class Category extends BaseEntity { 34 | 35 | private static final long serialVersionUID = 7369591777044660460L; 36 | 37 | /** 38 | * Category name 39 | */ 40 | @Column(name = "name", nullable = false, unique = true, length = 30) 41 | private String name; 42 | 43 | /** 44 | * Article count having this category 45 | */ 46 | @Formula("SELECT COUNT(id) FROM Article a WHERE a.category_id = id") 47 | private int articleCount; 48 | 49 | /** 50 | * Articles having this category 51 | */ 52 | @OneToMany(mappedBy = "category", fetch = FetchType.LAZY) 53 | private Set
    articles; 54 | 55 | /** 56 | * Default constructor 57 | */ 58 | public Category() {} 59 | 60 | /** 61 | * @return name of this category 62 | */ 63 | public String getName() { 64 | return name; 65 | } 66 | 67 | /** 68 | * @param name category name to set 69 | */ 70 | public void setName(String name) { 71 | this.name = name; 72 | } 73 | 74 | /** 75 | * @return article count having this category 76 | */ 77 | public int getArticleCount() { 78 | return articleCount; 79 | } 80 | 81 | /** 82 | * @return articles having this category 83 | */ 84 | public Set
    getArticles() { 85 | return articles; 86 | } 87 | 88 | /** 89 | * @param articles articles having this category 90 | */ 91 | public void setArticles(Set
    articles) { 92 | this.articles = articles; 93 | } 94 | 95 | /** 96 | * @see java.lang.Object#hashCode() 97 | */ 98 | @Override 99 | public int hashCode() { 100 | final int prime = 31; 101 | int result = 17; 102 | result = prime * result + ((name == null) ? 0 : name.hashCode()); 103 | return result; 104 | } 105 | 106 | /** 107 | * @see java.lang.Object#equals(java.lang.Object) 108 | */ 109 | @Override 110 | public boolean equals(Object obj) { 111 | if (this == obj) 112 | return true; 113 | if (obj == null) 114 | return false; 115 | if (getClass() != obj.getClass()) 116 | return false; 117 | 118 | Category other = (Category) obj; 119 | 120 | if (getName() != null 121 | ? !getName().equals(other.getName()) 122 | : other.getName() != null) { 123 | return false; 124 | } 125 | return true; 126 | } 127 | 128 | /** 129 | * @see java.lang.Object#toString() 130 | */ 131 | @Override 132 | public String toString() { 133 | return String.format("Category[id=%d, name=%s]", getId(), getName()); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/webapp/resources/css/bootstrap-tokenfield.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * bootstrap-tokenfield 3 | * https://github.com/sliptree/bootstrap-tokenfield 4 | * Copyright 2013-2014 Sliptree and other contributors; Licensed MIT 5 | */@-webkit-keyframes 'blink'{0%{border-color:#ededed}100%{border-color:#b94a48}}@-moz-keyframes 'blink'{0%{border-color:#ededed}100%{border-color:#b94a48}}@keyframes 'blink'{0%{border-color:#ededed}100%{border-color:#b94a48}}.tokenfield{height:auto;min-height:34px;padding-bottom:0}.tokenfield.focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.tokenfield .token{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;display:inline-block;border:1px solid #d9d9d9;background-color:#ededed;white-space:nowrap;margin:-1px 5px 5px 0;height:22px;vertical-align:top;cursor:default}.tokenfield .token:hover{border-color:#b9b9b9}.tokenfield .token.active{border-color:#52a8ec;border-color:rgba(82,168,236,.8)}.tokenfield .token.duplicate{border-color:#ebccd1;-webkit-animation-name:blink;animation-name:blink;-webkit-animation-duration:.1s;animation-duration:.1s;-webkit-animation-direction:normal;animation-direction:normal;-webkit-animation-timing-function:ease;animation-timing-function:ease;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.tokenfield .token.invalid{background:0 0;border:1px solid transparent;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;border-bottom:1px dotted #d9534f}.tokenfield .token.invalid.active{background:#ededed;border:1px solid #ededed;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.tokenfield .token .token-label{display:inline-block;overflow:hidden;text-overflow:ellipsis;padding-left:4px;vertical-align:top}.tokenfield .token .close{font-family:Arial;display:inline-block;line-height:100%;font-size:1.1em;line-height:1.49em;margin-left:5px;float:none;height:100%;vertical-align:top;padding-right:4px}.tokenfield .token-input{background:0 0;width:60px;min-width:60px;border:0;height:20px;padding:0;margin-bottom:6px;-webkit-box-shadow:none;box-shadow:none}.tokenfield .token-input:focus{border-color:transparent;outline:0;-webkit-box-shadow:none;box-shadow:none}.tokenfield.disabled{cursor:not-allowed;background-color:#eee}.tokenfield.disabled .token-input{cursor:not-allowed}.tokenfield.disabled .token:hover{cursor:not-allowed;border-color:#d9d9d9}.tokenfield.disabled .token:hover .close{cursor:not-allowed;opacity:.2;filter:alpha(opacity=20)}.has-warning .tokenfield.focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-error .tokenfield.focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-success .tokenfield.focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.tokenfield.input-sm,.input-group-sm .tokenfield{min-height:30px;padding-bottom:0}.input-group-sm .token,.tokenfield.input-sm .token{height:20px;margin-bottom:4px}.input-group-sm .token-input,.tokenfield.input-sm .token-input{height:18px;margin-bottom:5px}.tokenfield.input-lg,.input-group-lg .tokenfield{min-height:45px;padding-bottom:4px}.input-group-lg .token,.tokenfield.input-lg .token{height:25px}.input-group-lg .token-label,.tokenfield.input-lg .token-label{line-height:23px}.input-group-lg .token .close,.tokenfield.input-lg .token .close{line-height:1.3em}.input-group-lg .token-input,.tokenfield.input-lg .token-input{height:23px;line-height:23px;margin-bottom:6px;vertical-align:top}.tokenfield.rtl{direction:rtl;text-align:right}.tokenfield.rtl .token{margin:-1px 0 5px 5px}.tokenfield.rtl .token .token-label{padding-left:0;padding-right:4px} -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/domain/Tag.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.domain; 2 | 3 | import java.util.Set; 4 | 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.FetchType; 8 | import javax.persistence.ManyToMany; 9 | import javax.persistence.NamedQueries; 10 | import javax.persistence.NamedQuery; 11 | import javax.persistence.Table; 12 | 13 | import org.hibernate.annotations.Cache; 14 | import org.hibernate.annotations.CacheConcurrencyStrategy; 15 | import org.hibernate.annotations.Formula; 16 | 17 | /** 18 | * Represents tag of article 19 | * 20 | * @author Oleg Filippov 21 | */ 22 | @Entity 23 | @Table(name = "tag") 24 | @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) 25 | @NamedQueries({ 26 | @NamedQuery( 27 | name = "Tag.GET_ALL", 28 | query = "FROM Tag t ORDER BY t.name"), 29 | @NamedQuery( 30 | name = "Tag.GET_ALL_NAMES", 31 | query = "SELECT t.name FROM Tag t ORDER BY t.name"), 32 | @NamedQuery( 33 | name = "Tag.GET_BY_NAME", 34 | query = "FROM Tag t WHERE t.name = :name") 35 | }) 36 | public class Tag extends BaseEntity { 37 | 38 | private static final long serialVersionUID = 984410247121721301L; 39 | 40 | /** 41 | * Tag name 42 | */ 43 | @Column(name = "name", nullable = false, unique = true, length = 20) 44 | private String name; 45 | 46 | /** 47 | * Article count tagged with this tag 48 | */ 49 | @Formula("SELECT COUNT(a.id) FROM article_tag at " 50 | + "JOIN Article a ON at.article_id = a.id " 51 | + "JOIN Tag t ON at.tag_id = t.id WHERE t.id = id") 52 | private int articleCount; 53 | 54 | /** 55 | * Articles tagged with this tag 56 | */ 57 | @ManyToMany(mappedBy = "tags", fetch = FetchType.LAZY) 58 | private Set
    articles; 59 | 60 | /** 61 | * Default constructor 62 | */ 63 | public Tag() {} 64 | 65 | /** 66 | * @return tag name 67 | */ 68 | public String getName() { 69 | return name; 70 | } 71 | 72 | /** 73 | * @param name tag name to set 74 | */ 75 | public void setName(String name) { 76 | this.name = name; 77 | } 78 | 79 | /** 80 | * @return article count tagged with this tag 81 | */ 82 | public int getArticleCount() { 83 | return articleCount; 84 | } 85 | 86 | /** 87 | * @return scale of this tag 88 | */ 89 | public int getScale() { 90 | // need to change 91 | return articleCount > 9 ? 9 : articleCount; 92 | } 93 | 94 | /** 95 | * @return articles tagged with this tag 96 | */ 97 | public Set
    getArticles() { 98 | return articles; 99 | } 100 | 101 | /** 102 | * @param articles articles tagged with this tag 103 | */ 104 | public void setArticles(Set
    articles) { 105 | this.articles = articles; 106 | } 107 | 108 | /** 109 | * @see java.lang.Object#hashCode() 110 | */ 111 | @Override 112 | public int hashCode() { 113 | final int prime = 31; 114 | int result = 17; 115 | result = prime * result 116 | + ((getName() == null) ? 0 : getName().hashCode()); 117 | return result; 118 | } 119 | 120 | /** 121 | * @see java.lang.Object#equals(java.lang.Object) 122 | */ 123 | @Override 124 | public boolean equals(Object obj) { 125 | if (this == obj) 126 | return true; 127 | if (obj == null) 128 | return false; 129 | if (getClass() != obj.getClass()) 130 | return false; 131 | 132 | Tag other = (Tag) obj; 133 | 134 | if (getName() != null 135 | ? !getName().equals(other.getName()) 136 | : other.getName() != null) { 137 | return false; 138 | } 139 | return true; 140 | } 141 | 142 | /** 143 | * @see java.lang.Object#toString() 144 | */ 145 | @Override 146 | public String toString() { 147 | return String.format("Tag[id=%d, name=%s]", getId(), getName()); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/service/ArticleService.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.service; 2 | 3 | import java.util.Map; 4 | 5 | import net.filippov.newsportal.domain.Article; 6 | import net.filippov.newsportal.exception.NotFoundException; 7 | 8 | /** 9 | * Provides article-related operations 10 | * 11 | * @author Oleg Filippov 12 | */ 13 | public interface ArticleService extends AbstractService
    { 14 | 15 | /** 16 | * Save article into repository 17 | * 18 | * @param article to save 19 | * @param authorId article's author id 20 | * @param categoryName name of article's category 21 | * @param tagString string of comma separated tags 22 | */ 23 | void add(Article article, Long authorId, String categoryName, String tagString); 24 | 25 | /** 26 | * Get article from repository by it's id and increase view count if: 27 | * 1. user is anonymous 28 | * 2. logged user is not the author of this article 29 | * 3. requested URL is not equal to current article URL 30 | * 31 | * @param articleId id of article 32 | * @param userId logged user id 33 | * @param needIncreaseViewCount requested URL is not equal to current article URL 34 | * @return article 35 | * @throws NotFoundException 36 | */ 37 | Article get(Long articleId, Long userId, boolean needIncreaseViewCount) 38 | throws NotFoundException; 39 | 40 | /** 41 | * Update article 42 | * 43 | * @param article to update 44 | * @param categoryName name of article's category 45 | * @param tagString string of comma separated tags 46 | */ 47 | void update(Article article, String categoryName, String tagString); 48 | 49 | /** 50 | * Get articles by specified page 51 | * 52 | * @param page 53 | * @param articlesPerPage number of articles per page 54 | * @return Map of articles and number of pages 55 | */ 56 | Map getByPage(int page, int articlesPerPage); 57 | 58 | /** 59 | * Get articles by category on specified page 60 | * 61 | * @param page 62 | * @param articlesPerPage number of articles per page 63 | * @param name of category 64 | * @return Map of articles and number of pages 65 | * @throws NotFoundException if category not found 66 | */ 67 | Map getByPageByCategoryName(int page, int articlesPerPage, String name) 68 | throws NotFoundException; 69 | 70 | /** 71 | * Search article content by fragment on specified page 72 | * 73 | * @param page 74 | * @param articlesPerPage number of articles per page 75 | * @param fragment to search 76 | * @return Map of articles and number of pages 77 | */ 78 | Map getByPageByFragment(int page, int articlesPerPage, String fragment); 79 | 80 | /** 81 | * Get articles by tag on specified page 82 | * 83 | * @param page 84 | * @param articlesPerPage number of articles per page 85 | * @param name of tag 86 | * @return Map of articles and number of pages 87 | * @throws NotFoundException if tag not found 88 | */ 89 | Map getByPageByTagName(int page, int articlesPerPage, String name) 90 | throws NotFoundException; 91 | 92 | /** 93 | * Get articles by user id on specified page 94 | * 95 | * @param page 96 | * @param articlesPerPage number of articles per page 97 | * @param id of user 98 | * @return Map of articles and number of pages 99 | * @throws NotFoundException if user not found 100 | */ 101 | Map getByPageByUserId(int page, int articlesPerPage, Long id) 102 | throws NotFoundException; 103 | 104 | /** 105 | * Save a comment by it's content 106 | * 107 | * @param content comment content 108 | * @param authorId author primary key 109 | * @param articleId article primary key 110 | */ 111 | void addComment(String content, Long authorId, Long articleId); 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/web/MainController.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.web; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | import javax.servlet.http.HttpSession; 7 | 8 | import net.filippov.newsportal.domain.Article; 9 | import net.filippov.newsportal.exception.NotFoundException; 10 | import net.filippov.newsportal.service.CategoryService; 11 | import net.filippov.newsportal.service.ArticleService; 12 | import net.filippov.newsportal.service.TagService; 13 | import net.filippov.newsportal.web.constants.URL; 14 | import net.filippov.newsportal.web.constants.View; 15 | import net.filippov.newsportal.web.constants.Common; 16 | 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.stereotype.Controller; 19 | import org.springframework.web.bind.annotation.PathVariable; 20 | import org.springframework.web.bind.annotation.RequestMapping; 21 | import org.springframework.web.bind.annotation.RequestMethod; 22 | import org.springframework.web.servlet.ModelAndView; 23 | 24 | /** 25 | * Controller for home-page and static pages (about, contacts) 26 | * 27 | * @author Oleg Filippov 28 | */ 29 | @Controller 30 | public class MainController { 31 | 32 | private ArticleService articleService; 33 | private CategoryService categoryService; 34 | private TagService tagService; 35 | 36 | /** 37 | * Constructor autowiring needed services 38 | */ 39 | @Autowired 40 | public MainController(ArticleService articleService, 41 | CategoryService categoryService, 42 | TagService tagService) { 43 | this.articleService = articleService; 44 | this.categoryService = categoryService; 45 | this.tagService = tagService; 46 | } 47 | 48 | /** 49 | * Home-page, get news on first page 50 | */ 51 | @RequestMapping(method = RequestMethod.GET, value = URL.HOME) 52 | public ModelAndView home(HttpSession session) { 53 | 54 | return homeModelAndView(1, session); 55 | } 56 | 57 | /** 58 | * Home-page, get news on custom page 59 | */ 60 | @RequestMapping(method = RequestMethod.GET,value = URL.HOME_CUSTOM_PAGE) 61 | public ModelAndView home(@PathVariable("number") Integer pageNumber, 62 | HttpSession session) { 63 | 64 | if (pageNumber == 1) { 65 | return new ModelAndView("redirect:" + URL.HOME); 66 | } 67 | 68 | return homeModelAndView(pageNumber, session); 69 | } 70 | 71 | /** 72 | * Fills ModelAndView with news data 73 | */ 74 | private ModelAndView homeModelAndView(Integer pageNumber, HttpSession session) { 75 | 76 | if (pageNumber < 1) { 77 | throw new NotFoundException("Page < 1"); 78 | } 79 | 80 | Map articlesData = articleService.getByPage(pageNumber, Common.ARTICLES_PER_PAGE); 81 | Integer pageCount = (Integer) articlesData.get("pageCount"); 82 | 83 | @SuppressWarnings("unchecked") 84 | List
    articlesByPage = (List
    ) articlesData.get("articlesByPage"); 85 | 86 | if (session.getAttribute("categories") == null) { 87 | session.setAttribute("categories", categoryService.getAllTransactionally()); 88 | } 89 | if (session.getAttribute("tags") == null) { 90 | session.setAttribute("tags", tagService.getAllTransactionally()); 91 | } 92 | 93 | return new ModelAndView(View.HOME) 94 | .addObject("pageCount", pageCount) 95 | .addObject("articlesByPage", articlesByPage) 96 | .addObject("currentPage", pageNumber) 97 | .addObject("requestUrl", URL.HOME); 98 | } 99 | 100 | /** 101 | * About-page 102 | */ 103 | @RequestMapping(method = RequestMethod.GET, value = URL.ABOUT) 104 | public String aboutPage() { 105 | 106 | return View.ABOUT; 107 | } 108 | 109 | /** 110 | * Contacts-page 111 | */ 112 | @RequestMapping(method = RequestMethod.GET, value = URL.CONTACTS) 113 | public String contactsPage() { 114 | 115 | return View.CONTACTS; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/webapp/resources/msg/messages.properties: -------------------------------------------------------------------------------- 1 | # Common 2 | project.title=News-portal 3 | 4 | # PAGES 5 | header.homeUrl=Home 6 | header.aboutUrl=About 7 | header.contactsUrl=Contacts 8 | header.signin=Sign In 9 | header.logout=Logout 10 | header.signup=Sign Up 11 | header.profile=Profile 12 | header.search=Search 13 | 14 | sidebar.about=About 15 | sidebar.categories=Categories 16 | sidebar.tags=Tags 17 | sidebar.links=My links 18 | 19 | home.pageTitle=Home 20 | home.addArticleUrl=Add article 21 | home.noArticles=No articles 22 | home.firstPage=First 23 | home.lastPage=Last 24 | 25 | search.pageTitle=Search results 26 | search.result.byCategory=Articles by category '%s': 27 | search.result.byFragment=Search results for '%s': 28 | search.result.byTag=Articles by tag '%s': 29 | search.result.byUser=Articles by user '%s': 30 | 31 | signin.pageTitle=Sign in 32 | signin.heading=Please sign in 33 | signin.rememberMe=Remember me 34 | signin.signInUrl=Sign In 35 | signin.username=Username 36 | signin.password=Password 37 | 38 | signup.pageTitle=Sign Up 39 | signup.submitButton=Sign Up 40 | 41 | article-view.noComments=There are no comments yet 42 | article-view.loginComments=Please {0} or {1} to leave comments 43 | article-view.loginComments.signin=Sign In 44 | article-view.loginComments.signup=Sign Up 45 | article-view.leaveComment=Leave a comment 46 | article-view.commentsToArticle=Comments\: 47 | article-view.addCommentButton=Send 48 | article-view.resetCommentButton=Reset 49 | 50 | article-edit.pageTitle=Edit article 51 | article-edit.saveButton=Save 52 | article-edit.cancelButton=Cancel 53 | article-edit.tags=Add tags 54 | article-edit.category=Choose category 55 | 56 | profile.pageTitle=Profile 57 | profile.header=User profile of 58 | profile.articleCount=Articles: 59 | profile.commentCount=Comments: 60 | 61 | about.pageTitle=About 62 | 63 | contacts.pageTitle=Contacts 64 | 65 | success.pageTitle=Success 66 | success.registration=Registration has been completed successfully! 67 | success.article.deleted=Article has been deleted! 68 | 69 | error.pageTitle=Error 70 | error.status=Status 71 | error.url=URL 72 | error.signIn=Wrong username or password! 73 | error.accessDenied=Access denied! 74 | error.400=Bad request! 75 | error.404=The requested URL was not found! 76 | error.500=Internal server error! 77 | error.default=Unknown error! 78 | 79 | # ENTITY 80 | user.login=Login 81 | user.password=Password 82 | user.passwordConfirm=Confirm password 83 | user.email=E-mail 84 | user.name=Fullname 85 | user.registered=Registered 86 | 87 | article.title=Title (max of 100 characters) 88 | article.created=Created 89 | article.lastModified=Last modified 90 | article.author=Author 91 | article.views=Views 92 | article.comments=Comments 93 | article.preview=Preview (max of 255 characters) 94 | article.content=Content 95 | article.showArticleUrl=View details 96 | article.editArticleUrl=Edit 97 | article.deleteArticleUrl=Delete 98 | 99 | # VALIDATION 100 | validation.user.loginSize=Login must be at least 4 characters (A-z, 0-9) 101 | validation.user.passwordSize=Password must be at least 7 characters 102 | validation.user.passwordMatch=Passwords must match 103 | validation.user.unique.login=This login is already taken 104 | validation.user.unique.email=User with this e-mail already exists 105 | validation.user.emailNotBlank=e-mail cannot be empty 106 | validation.user.emailValid=Please enter a vaild e-mail 107 | validation.user.nameNotBlank=Name cannot be empty 108 | 109 | validation.article.title=Title cannot be empty 110 | validation.article.preview=Preview cannot be empty 111 | validation.article.content=Content cannot be empty 112 | 113 | validation.comment.content=Comment cannot be empty 114 | 115 | validation.file.maxSize=Image size must be less than 500KB 116 | validation.file.type=Allowed types: JPG, PNG 117 | validation.file.custom=There was a problem while uploading image. Try again -------------------------------------------------------------------------------- /src/main/webapp/resources/msg/messages_en.properties: -------------------------------------------------------------------------------- 1 | # Common 2 | project.title=News-portal 3 | 4 | # PAGES 5 | header.homeUrl=Home 6 | header.aboutUrl=About 7 | header.contactsUrl=Contacts 8 | header.signin=Sign In 9 | header.logout=Logout 10 | header.signup=Sign Up 11 | header.profile=Profile 12 | header.search=Search 13 | 14 | sidebar.about=About 15 | sidebar.categories=Categories 16 | sidebar.tags=Tags 17 | sidebar.links=My links 18 | 19 | home.pageTitle=Home 20 | home.addArticleUrl=Add article 21 | home.noArticles=No articles 22 | home.firstPage=First 23 | home.lastPage=Last 24 | 25 | search.pageTitle=Search results 26 | search.result.byCategory=Articles by category '%s': 27 | search.result.byFragment=Search results for '%s': 28 | search.result.byTag=Articles by tag '%s': 29 | search.result.byUser=Articles by user '%s': 30 | 31 | signin.pageTitle=Sign in 32 | signin.heading=Please sign in 33 | signin.rememberMe=Remember me 34 | signin.signInUrl=Sign In 35 | signin.username=Username 36 | signin.password=Password 37 | 38 | signup.pageTitle=Sign Up 39 | signup.submitButton=Sign Up 40 | 41 | article-view.noComments=There are no comments yet 42 | article-view.loginComments=Please {0} or {1} to leave comments 43 | article-view.loginComments.signin=Sign In 44 | article-view.loginComments.signup=Sign Up 45 | article-view.leaveComment=Leave a comment 46 | article-view.commentsToArticle=Comments\: 47 | article-view.addCommentButton=Send 48 | article-view.resetCommentButton=Reset 49 | 50 | article-edit.pageTitle=Edit article 51 | article-edit.saveButton=Save 52 | article-edit.cancelButton=Cancel 53 | article-edit.tags=Add tags 54 | article-edit.category=Choose category 55 | 56 | profile.pageTitle=Profile 57 | profile.header=User profile of 58 | profile.articleCount=Articles: 59 | profile.commentCount=Comments: 60 | 61 | about.pageTitle=About 62 | 63 | contacts.pageTitle=Contacts 64 | 65 | success.pageTitle=Success 66 | success.registration=Registration has been completed successfully! 67 | success.article.deleted=Article has been deleted! 68 | 69 | error.pageTitle=Error 70 | error.status=Status 71 | error.url=URL 72 | error.signIn=Wrong username or password! 73 | error.accessDenied=Access denied! 74 | error.400=Bad request! 75 | error.404=The requested URL was not found! 76 | error.500=Internal server error! 77 | error.default=Unknown error! 78 | 79 | # ENTITY 80 | user.login=Login 81 | user.password=Password 82 | user.passwordConfirm=Confirm password 83 | user.email=E-mail 84 | user.name=Fullname 85 | user.registered=Registered 86 | 87 | article.title=Title (max of 100 characters) 88 | article.created=Created 89 | article.lastModified=Last modified 90 | article.author=Author 91 | article.views=Views 92 | article.comments=Comments 93 | article.preview=Preview (max of 255 characters) 94 | article.content=Content 95 | article.showArticleUrl=View details 96 | article.editArticleUrl=Edit 97 | article.deleteArticleUrl=Delete 98 | 99 | # VALIDATION 100 | validation.user.loginSize=Login must be at least 4 characters (A-z, 0-9) 101 | validation.user.passwordSize=Password must be at least 7 characters 102 | validation.user.passwordMatch=Passwords must match 103 | validation.user.unique.login=This login is already taken 104 | validation.user.unique.email=User with this e-mail already exists 105 | validation.user.emailNotBlank=e-mail cannot be empty 106 | validation.user.emailValid=Please enter a vaild e-mail 107 | validation.user.nameNotBlank=Name cannot be empty 108 | 109 | validation.article.title=Title cannot be empty 110 | validation.article.preview=Preview cannot be empty 111 | validation.article.content=Content cannot be empty 112 | 113 | validation.comment.content=Comment cannot be empty 114 | 115 | validation.file.maxSize=Image size must be less than 500KB 116 | validation.file.type=Allowed types: JPG, PNG 117 | validation.file.custom=There was a problem while uploading image. Try again -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/repository/GenericRepository.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.repository; 2 | 3 | import java.io.Serializable; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | /** 8 | * Generic repository providing basic and custom operations using named queries 9 | * 10 | * @param The persistent type 11 | * @param The primary key type 12 | * 13 | * @author Oleg Filippov 14 | */ 15 | public interface GenericRepository { 16 | 17 | /** 18 | * Set generic type 19 | * @param type 20 | */ 21 | void setType(Class type); 22 | 23 | /** 24 | * Add object into database 25 | * @param object to be inserted into database 26 | * @return id of the object has been inserted into database 27 | */ 28 | void add(T newInstance); 29 | 30 | /** 31 | * Get object from database 32 | * @param id of the object to be selected from database 33 | * @return persistent instance of object or null if the object does not exist 34 | */ 35 | T get(PK id); 36 | 37 | /** 38 | * 39 | * Update object in database 40 | * @param transientObject to be updated 41 | */ 42 | T update(T transientObject); 43 | 44 | /** 45 | * Delete object from database 46 | * @param persistentObject to be deleted 47 | */ 48 | void delete(T persistentObject); 49 | 50 | /** 51 | * Delete object from database by id 52 | * @param id of the object to be deleted from database 53 | */ 54 | void deleteById(PK id); 55 | 56 | /** 57 | * Get object from database using a named query 58 | * @param namedQuery to use 59 | * @param map of parameters to set 60 | * @return persistent instance of object or null if the object does not exist 61 | */ 62 | T getByNamedQuery(String namedQuery, Map parameters); 63 | 64 | /** 65 | * Get list of objects from database 66 | * @return list of persistent objects 67 | */ 68 | List getAll(); 69 | 70 | /** 71 | * Get list of objects from database using a named query 72 | * @param namedQuery to use 73 | * @return list of persistent objects 74 | */ 75 | List getAllByNamedQuery(String namedQuery); 76 | 77 | /** 78 | * Get list of objects from database using a named query 79 | * @param namedQuery to use 80 | * @param firstResult position of the first result, numbered from 0 81 | * @param resultLimit results limit 82 | * @return list of persistent objects 83 | */ 84 | List getAllByNamedQuery(String namedQuery, int firstResult, int resultLimit); 85 | 86 | /** 87 | * Get list of objects from database using a named query 88 | * @param namedQuery to use 89 | * @param parameters Map of parameters to set 90 | * @return list of persistent objects 91 | */ 92 | List getAllByNamedQuery(String namedQuery, Map parameters); 93 | 94 | /** 95 | * Get list of objects from database using a named query 96 | * @param namedQuery to use 97 | * @param parameters Map of parameters to set 98 | * @param firstResult position of the first result, numbered from 0 99 | * @param resultLimit results limit 100 | * @return list of persistent objects 101 | */ 102 | List getAllByNamedQuery(String namedQuery, Map parameters, 103 | int firstResult, int resultLimit); 104 | 105 | /** 106 | * Get names from table 107 | * @param namedQuery to use 108 | * @param resultLimit results limit 109 | * @return 110 | */ 111 | List getAllNamesByNamedQuery(String namedQuery, int resultLimit); 112 | 113 | /** 114 | * Count all objects in database 115 | * @return the number of objects 116 | */ 117 | int getCount(); 118 | 119 | /** 120 | * Count all objects in database using a named query 121 | * @param namedQuery to use 122 | * @param parameters Map of parameters to set 123 | * @return the number of objects 124 | */ 125 | int getCountByNamedQuery(String namedQuery, Map parameters); 126 | } 127 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/article-edit.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" 2 | pageEncoding="UTF-8"%> 3 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> 4 | <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> 5 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> 6 | <%@ taglib prefix="t" tagdir="/WEB-INF/tags"%> 7 | 8 | 9 | | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
    18 |

    19 |
    20 | 21 | " value="${article.title}" 23 | required data-error=""> 24 | 25 |
    26 | 27 | 28 | 36 | 37 | 38 |

    39 | " /> 41 | 42 | 43 |
    44 |

    45 |
    46 | 47 | 51 | 52 |
    53 | 54 | 55 |
    56 |

    57 |

    58 |
    59 | 60 | 61 | 62 |
    63 | 64 | 65 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
    75 | 76 | 77 | 78 | 96 |
    97 |
    -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/common/header.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" 2 | pageEncoding="UTF-8"%> 3 | <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> 4 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> 5 | <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 113 | 114 | 121 | 122 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/service/impl/TagServiceImpl.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.service.impl; 2 | 3 | import java.util.HashSet; 4 | import java.util.List; 5 | import java.util.Set; 6 | 7 | import javax.persistence.PersistenceException; 8 | 9 | import net.filippov.newsportal.domain.Tag; 10 | import net.filippov.newsportal.exception.ServiceException; 11 | import net.filippov.newsportal.repository.GenericRepository; 12 | import net.filippov.newsportal.service.TagService; 13 | import net.filippov.newsportal.web.constants.Common; 14 | import static net.filippov.newsportal.service.util.QueryParameters.setParam; 15 | 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.stereotype.Service; 18 | import org.springframework.transaction.annotation.Transactional; 19 | 20 | /** 21 | * Implementation of {@link TagService} 22 | * 23 | * @author Oleg Filippov 24 | */ 25 | @Service("TagService") 26 | public class TagServiceImpl extends AbstractServiceImpl implements TagService { 27 | 28 | /** 29 | * Constructor autowiring tag-repository 30 | */ 31 | @Autowired 32 | public TagServiceImpl(GenericRepository repository) { 33 | super(repository); 34 | } 35 | 36 | /** 37 | * @see net.filippov.newsportal.service.TagService#getByName(java.lang.String) 38 | */ 39 | @Override 40 | public Tag getByName(String name) { 41 | try { 42 | return repository.getByNamedQuery("Tag.GET_BY_NAME", 43 | setParam("name", name).buildMap()); 44 | } catch (PersistenceException e) { 45 | String message = String.format("Unable to get tag=%s", name); 46 | throw new ServiceException(message, e); 47 | } 48 | } 49 | 50 | /** 51 | * @see net.filippov.newsportal.service.impl.AbstractServiceImpl#getAllTransactionally() 52 | */ 53 | @Override 54 | @Transactional 55 | public List getAllTransactionally() { 56 | try { 57 | return repository.getAllByNamedQuery( 58 | "Tag.GET_ALL", 0, Common.TAG_MAX_COUNT); 59 | } catch (PersistenceException e) { 60 | throw new ServiceException("Unable to get all tags", e); 61 | } 62 | } 63 | 64 | /** 65 | * @see net.filippov.newsportal.service.TagService#getAllNames() 66 | */ 67 | @Override 68 | @Transactional 69 | public List getAllNames() { 70 | try { 71 | return repository.getAllNamesByNamedQuery( 72 | "Tag.GET_ALL_NAMES", 0); 73 | } catch (PersistenceException e) { 74 | throw new ServiceException("Unable to get all tag names", e); 75 | } 76 | } 77 | 78 | /** 79 | * @see net.filippov.newsportal.service.TagService#getAutocompleteJson() 80 | */ 81 | @Override 82 | public String getAutocompleteJson() { 83 | List tagNames = this.getAllNames(); 84 | 85 | if (tagNames.isEmpty()) { 86 | return "[]"; 87 | } 88 | 89 | StringBuilder result = new StringBuilder("["); 90 | for (String name : tagNames) { 91 | result.append("\"") 92 | .append(name) 93 | .append("\"") 94 | .append(","); 95 | } 96 | result.delete(result.length()-1, result.length()) 97 | .append("]"); 98 | return result.toString(); 99 | } 100 | 101 | /** 102 | * @see net.filippov.newsportal.service.TagService#getTagsFromString(java.lang.String) 103 | */ 104 | @Override 105 | public Set getTagsFromString(String tagString) { 106 | Set result = new HashSet(); 107 | for (String tagName : tagString.split(",")) { 108 | tagName = tagName.replaceAll("\\s+", ""); 109 | Tag tagObj = this.getByName(tagName); // get persistent object 110 | if (tagObj == null) { 111 | tagObj = new Tag(); 112 | tagObj.setName(tagName); 113 | } 114 | result.add(tagObj); 115 | } 116 | return result; 117 | } 118 | 119 | /** 120 | * @see net.filippov.newsportal.service.TagService#getTagString(java.util.Set) 121 | */ 122 | @Override 123 | public String getTagString(Set tags) { 124 | StringBuilder result = new StringBuilder(); 125 | for (Tag tag : tags) { 126 | result.append(tag.getName()).append(","); 127 | } 128 | result.delete(result.length()-1, result.length()); 129 | return result.toString(); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/web/UserController.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.web; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | import javax.validation.Valid; 5 | 6 | import net.filippov.newsportal.domain.User; 7 | import net.filippov.newsportal.exception.NotUniqueUserFieldException; 8 | import net.filippov.newsportal.service.UserService; 9 | import net.filippov.newsportal.web.constants.URL; 10 | import net.filippov.newsportal.web.constants.View; 11 | 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.security.access.prepost.PreAuthorize; 14 | import org.springframework.stereotype.Controller; 15 | import org.springframework.ui.Model; 16 | import org.springframework.validation.BindingResult; 17 | import org.springframework.web.bind.annotation.ModelAttribute; 18 | import org.springframework.web.bind.annotation.PathVariable; 19 | import org.springframework.web.bind.annotation.RequestMapping; 20 | import org.springframework.web.bind.annotation.RequestMethod; 21 | import org.springframework.web.bind.annotation.RequestParam; 22 | import org.springframework.web.bind.annotation.ResponseBody; 23 | import org.springframework.web.servlet.ModelAndView; 24 | import org.springframework.web.servlet.mvc.support.RedirectAttributes; 25 | 26 | /** 27 | * Controller for user-related actions 28 | * 29 | * @author Oleg Filippov 30 | */ 31 | @Controller 32 | public class UserController { 33 | 34 | @Autowired 35 | private UserService userService; 36 | 37 | /** 38 | * SignIn-page 39 | */ 40 | @RequestMapping(method = RequestMethod.GET, value = URL.SIGN_IN) 41 | public void signinPage() {} 42 | 43 | /** 44 | * SignIn failure 45 | */ 46 | @RequestMapping(method = RequestMethod.GET, value = URL.SIGN_IN_FAILURE) 47 | public ModelAndView signinFailure(Model model) { 48 | 49 | return new ModelAndView(View.SIGN_IN) 50 | .addObject("error", "true"); 51 | } 52 | 53 | /** 54 | * SignUp-page 55 | */ 56 | @RequestMapping(method=RequestMethod.GET, value = URL.SIGN_UP) 57 | public String signupPage() { 58 | 59 | return View.SIGN_UP; 60 | } 61 | 62 | /** 63 | * SignUp submit 64 | */ 65 | @RequestMapping(method=RequestMethod.POST, value = URL.SIGN_UP) 66 | public String registerSubmit(Model model, @Valid @ModelAttribute User user, 67 | BindingResult result, RedirectAttributes attr, HttpServletRequest request) { 68 | 69 | if (result.hasErrors()) { 70 | attr.addFlashAttribute( 71 | "org.springframework.validation.BindingResult.user", result); 72 | attr.addFlashAttribute("user", user); 73 | return "redirect:" + URL.SIGN_UP; 74 | } 75 | 76 | try { 77 | userService.add(user); 78 | } catch (NotUniqueUserFieldException e) { 79 | String field = e.getMessage(); 80 | result.rejectValue(field, "validation.user.unique." + field); 81 | user.setPassword(""); 82 | return View.SIGN_UP; 83 | } 84 | 85 | model.addAttribute("messageProperty", "success.registration"); 86 | model.addAttribute("url", request.getServletContext().getContextPath()); 87 | 88 | return View.SUCCESS; 89 | } 90 | 91 | /** 92 | * Check if login is free 93 | */ 94 | @RequestMapping(method=RequestMethod.GET, value = URL.CHECK_LOGIN) 95 | @ResponseBody 96 | public String checkLogin(@RequestParam String login) { 97 | 98 | return userService.getByLogin(login) == null 99 | ? "ok" 100 | : "no"; 101 | } 102 | 103 | /** 104 | * Check if e-mail is free 105 | */ 106 | @RequestMapping(method=RequestMethod.GET, value = URL.CHECK_EMAIL) 107 | @ResponseBody 108 | public String checkEmail(@RequestParam String email) { 109 | 110 | return userService.getByEmail(email) == null 111 | ? "ok" 112 | : "no"; 113 | } 114 | 115 | /** 116 | * Profile-page 117 | */ 118 | @PreAuthorize("hasRole('ROLE_USER')") 119 | @RequestMapping(method=RequestMethod.GET, value = URL.USER_PROFILE) 120 | public String profilePage(Model model, @PathVariable("id") Long userId) { 121 | 122 | User user = userService.get(userId); 123 | model.addAttribute("user", user); 124 | 125 | return View.PROFILE; 126 | } 127 | 128 | /** 129 | * Fill model attribute 130 | * 131 | * @return new instance of {@link User} 132 | */ 133 | @ModelAttribute("user") 134 | public User populateUser() { 135 | return new User(); 136 | } 137 | } -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/service/impl/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.service.impl; 2 | 3 | import static net.filippov.newsportal.service.util.QueryParameters.setParam; 4 | 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | 8 | import javax.persistence.PersistenceException; 9 | 10 | import net.filippov.newsportal.domain.User; 11 | import net.filippov.newsportal.domain.UserRole; 12 | import net.filippov.newsportal.exception.NotUniqueUserFieldException; 13 | import net.filippov.newsportal.exception.ServiceException; 14 | import net.filippov.newsportal.repository.GenericRepository; 15 | import net.filippov.newsportal.service.UserService; 16 | 17 | import org.hibernate.exception.ConstraintViolationException; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | import org.springframework.beans.factory.annotation.Autowired; 21 | import org.springframework.security.crypto.password.PasswordEncoder; 22 | import org.springframework.stereotype.Service; 23 | import org.springframework.transaction.annotation.Transactional; 24 | 25 | /** 26 | * Implementation of {@link UserService} 27 | * 28 | * @author Oleg Filippov 29 | */ 30 | @Service("UserService") 31 | public class UserServiceImpl extends AbstractServiceImpl implements UserService { 32 | 33 | private static final Logger LOG = LoggerFactory.getLogger(UserService.class); 34 | 35 | private GenericRepository roleRepository; 36 | private PasswordEncoder bCryptPasswordEncoder; 37 | 38 | /** 39 | * Constructor autowiring needed repositories and 40 | * {@link org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder} 41 | */ 42 | @Autowired 43 | public UserServiceImpl(GenericRepository repository, 44 | GenericRepository roleRepository, 45 | PasswordEncoder bCryptPasswordEncoder) { 46 | super(repository); 47 | this.roleRepository = roleRepository; 48 | this.roleRepository.setType(UserRole.class); 49 | this.bCryptPasswordEncoder = bCryptPasswordEncoder; 50 | } 51 | 52 | /** 53 | * Add user into repository with default role ("ROLE_USER") and encoded password 54 | * 55 | * @see net.filippov.newsportal.service.impl.AbstractServiceImpl#add(java.io.Serializable) 56 | */ 57 | @Override 58 | @Transactional 59 | public void add(User user) { 60 | try { 61 | UserRole defaultRole = roleRepository.getByNamedQuery( 62 | "UserRole.GET_BY_AUTHORITY", 63 | setParam("authority", "ROLE_USER").buildMap()); 64 | Set userRoles = new HashSet(); 65 | userRoles.add(defaultRole); 66 | user.setRoles(userRoles); 67 | 68 | String encodedPassword = bCryptPasswordEncoder.encode(user.getPassword()); 69 | user.setPassword(encodedPassword); 70 | 71 | repository.add(user); 72 | LOG.info("ADDED: " + user); 73 | } catch (PersistenceException e) { 74 | if (e.getCause().getClass() == ConstraintViolationException.class) { 75 | ConstraintViolationException ce = (ConstraintViolationException) e.getCause(); 76 | if (ce.getConstraintName().indexOf("LOGIN") != -1) { 77 | throw new NotUniqueUserFieldException("login"); 78 | } else if (ce.getConstraintName().indexOf("EMAIL") != -1) { 79 | throw new NotUniqueUserFieldException("email"); 80 | } 81 | } 82 | String message = String.format("Unable to add %s", user); 83 | throw new ServiceException(message, e); 84 | } 85 | } 86 | 87 | /** 88 | * @see net.filippov.newsportal.service.UserService#getByLogin(java.lang.String) 89 | */ 90 | @Override 91 | @Transactional(readOnly = true) 92 | public User getByLogin(String login) { 93 | try { 94 | User user = repository.getByNamedQuery( 95 | "User.GET_BY_LOGIN", 96 | setParam("login", login).buildMap()); 97 | if (user != null) { 98 | user.getRoles().size(); 99 | } 100 | return user; 101 | } catch (PersistenceException e) { 102 | String message = String.format("Unable to get user by login=%s", login); 103 | throw new ServiceException(message, e); 104 | } 105 | } 106 | 107 | /** 108 | * @see net.filippov.newsportal.service.UserService#getByEmail(java.lang.String) 109 | */ 110 | @Override 111 | @Transactional(readOnly = true) 112 | public User getByEmail(String email) { 113 | try { 114 | User user = repository.getByNamedQuery( 115 | "User.GET_BY_EMAIL", 116 | setParam("email", email).buildMap()); 117 | return user; 118 | } catch (PersistenceException e) { 119 | String message = String.format("Unable to get user by email=%s", email); 120 | throw new ServiceException(message, e); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/domain/Comment.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.domain; 2 | 3 | import java.util.Date; 4 | 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.JoinColumn; 8 | import javax.persistence.ManyToOne; 9 | import javax.persistence.NamedQueries; 10 | import javax.persistence.NamedQuery; 11 | import javax.persistence.Table; 12 | import javax.persistence.Temporal; 13 | import javax.persistence.TemporalType; 14 | 15 | import org.hibernate.annotations.Cache; 16 | import org.hibernate.annotations.CacheConcurrencyStrategy; 17 | import org.hibernate.validator.constraints.NotBlank; 18 | 19 | /** 20 | * Represents comment on article 21 | * 22 | * @author Oleg Filippov 23 | */ 24 | @Entity 25 | @Table(name = "comment") 26 | @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) 27 | @NamedQueries({ 28 | @NamedQuery( 29 | name = "Comment.GET_ALL_BY_ARTICLE_ID", 30 | query = "FROM Comment c WHERE c.article.id = :id ORDER BY c.created DESC") 31 | }) 32 | public class Comment extends BaseEntity { 33 | 34 | private static final long serialVersionUID = -4252140027137381170L; 35 | 36 | /** 37 | * Comment content 38 | */ 39 | @NotBlank(message = "{validation.comment.content}") 40 | @Column(name = "content", length = 500) 41 | private String content; 42 | 43 | /** 44 | * Creation date and time 45 | */ 46 | @Column(name = "created", insertable = false, updatable = false, 47 | columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP") 48 | @Temporal(TemporalType.TIMESTAMP) 49 | private final Date created; 50 | 51 | /** 52 | * Author of this comment 53 | */ 54 | @ManyToOne 55 | @JoinColumn(name = "user_id") 56 | private User author; 57 | 58 | /** 59 | * Article which owns comment 60 | */ 61 | @ManyToOne 62 | @JoinColumn(name = "article_id") 63 | private Article article; 64 | 65 | /** 66 | * Default constructor. For Hibernate use only 67 | */ 68 | protected Comment() { 69 | created = new Date(); 70 | } 71 | 72 | /** 73 | * Constructor initializing required fields 74 | * 75 | * @param author 76 | * @param article 77 | * @param content 78 | */ 79 | public Comment(User author, Article article, String content) { 80 | created = new Date(); 81 | this.author = author; 82 | this.article = article; 83 | this.content = content; 84 | } 85 | 86 | /** 87 | * @return comment content 88 | */ 89 | public String getContent() { 90 | return content; 91 | } 92 | 93 | /** 94 | * @param content comment content to set 95 | */ 96 | public void setContent(String content) { 97 | this.content = content; 98 | } 99 | 100 | /** 101 | * @return comment creation date and time 102 | */ 103 | public Date getCreated() { 104 | return created; 105 | } 106 | 107 | /** 108 | * @return author of this comment 109 | */ 110 | public User getAuthor() { 111 | return author; 112 | } 113 | 114 | /** 115 | * @param author author of this comment to set 116 | */ 117 | public void setAuthor(User author) { 118 | this.author = author; 119 | } 120 | 121 | /** 122 | * @return article which owns this comment 123 | */ 124 | public Article getArticle() { 125 | return article; 126 | } 127 | 128 | /** 129 | * @param article article which owns this comment 130 | */ 131 | public void setArticle(Article article) { 132 | this.article = article; 133 | } 134 | 135 | /** 136 | * @see java.lang.Object#hashCode() 137 | */ 138 | @Override 139 | public int hashCode() { 140 | final int prime = 31; 141 | int result = 17; 142 | result = prime * result 143 | + ((getContent() == null) ? 0 : getContent().hashCode()); 144 | result = prime * result 145 | + ((getCreated() == null) ? 0 : getCreated().hashCode()); 146 | return result; 147 | } 148 | 149 | /** 150 | * @see java.lang.Object#equals(java.lang.Object) 151 | */ 152 | @Override 153 | public boolean equals(Object obj) { 154 | if (this == obj) 155 | return true; 156 | if (obj == null) 157 | return false; 158 | if (getClass() != obj.getClass()) 159 | return false; 160 | 161 | Comment other = (Comment) obj; 162 | 163 | if (getContent() != null 164 | ? !getContent().equals(other.getContent()) 165 | : other.getContent() != null) { 166 | return false; 167 | } 168 | if (getCreated() != null 169 | ? getCreated().compareTo(other.getCreated()) != 0 170 | : other.getCreated() != null) { 171 | return false; 172 | } 173 | return true; 174 | } 175 | 176 | /** 177 | * @see java.lang.Object#toString() 178 | */ 179 | @Override 180 | public String toString() { 181 | return String.format("Comment[id=%d, author=%s, article_id=%s]", 182 | getId(), 183 | getAuthor() == null ? "null" : getAuthor().getLogin(), 184 | getArticle() == null ? "null" : getArticle().getId()); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/service/impl/AbstractServiceImpl.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.service.impl; 2 | 3 | import java.io.Serializable; 4 | import java.lang.reflect.ParameterizedType; 5 | import java.lang.reflect.Type; 6 | import java.util.List; 7 | 8 | import javax.persistence.PersistenceException; 9 | 10 | import net.filippov.newsportal.exception.ServiceException; 11 | import net.filippov.newsportal.repository.GenericRepository; 12 | import net.filippov.newsportal.service.AbstractService; 13 | 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.transaction.annotation.Transactional; 17 | 18 | /** 19 | * Implementation of {@link AbstractService} 20 | * 21 | * @param 22 | * 23 | * @author Oleg Filippov 24 | */ 25 | public abstract class AbstractServiceImpl 26 | implements AbstractService { 27 | 28 | private static final Logger LOG = LoggerFactory.getLogger(AbstractService.class); 29 | 30 | /** 31 | * Generic repository 32 | */ 33 | protected GenericRepository repository; 34 | 35 | /** 36 | * Object class simple name 37 | */ 38 | private String className; 39 | 40 | /** 41 | * Constructor autowiring generic repository 42 | */ 43 | @SuppressWarnings("unchecked") 44 | public AbstractServiceImpl(GenericRepository repository) { 45 | Type t = getClass().getGenericSuperclass(); 46 | ParameterizedType pt = (ParameterizedType) t; 47 | Class type = (Class) pt.getActualTypeArguments()[0]; 48 | className = type.getSimpleName(); 49 | 50 | this.repository = repository; 51 | this.repository.setType(type); 52 | } 53 | 54 | /** 55 | * @see net.filippov.newsportal.service.AbstractService#add(java.io.Serializable) 56 | */ 57 | @Override 58 | public void add(T obj) { 59 | try { 60 | repository.add(obj); 61 | LOG.info("ADDED: " + obj); 62 | } catch (PersistenceException e) { 63 | String message = String.format("Unable to add %s", obj); 64 | throw new ServiceException(message, e); 65 | } 66 | } 67 | 68 | /** 69 | * @see net.filippov.newsportal.service.AbstractService#addTransactionally(java.io.Serializable) 70 | */ 71 | @Override 72 | @Transactional 73 | public void addTransactionally(T obj) { 74 | this.add(obj); 75 | } 76 | 77 | /** 78 | * @see net.filippov.newsportal.service.AbstractService#get(java.lang.Long) 79 | */ 80 | @Override 81 | public T get(Long id) { 82 | try { 83 | return repository.get(id); 84 | } catch (PersistenceException e) { 85 | String message = String.format("Unable to get %s id=%d", className, id); 86 | throw new ServiceException(message, e); 87 | } 88 | } 89 | 90 | /** 91 | * @see net.filippov.newsportal.service.AbstractService#getTransactionally(java.lang.Long) 92 | */ 93 | @Override 94 | @Transactional 95 | public T getTransactionally(Long id) { 96 | return this.get(id); 97 | } 98 | 99 | /** 100 | * @see net.filippov.newsportal.service.AbstractService#update(java.io.Serializable) 101 | */ 102 | @Override 103 | public void update(T obj) { 104 | try { 105 | repository.update(obj); 106 | LOG.info("UPDATED: " + obj); 107 | } catch (PersistenceException e) { 108 | String message = String.format("Unable to update %s", obj); 109 | throw new ServiceException(message, e); 110 | } 111 | } 112 | 113 | /** 114 | * @see net.filippov.newsportal.service.AbstractService#updateTransactionally(java.io.Serializable) 115 | */ 116 | @Override 117 | @Transactional 118 | public void updateTransactionally(T obj) { 119 | this.update(obj); 120 | } 121 | 122 | /** 123 | * @see net.filippov.newsportal.service.AbstractService#deleteTransactionally(java.io.Serializable) 124 | */ 125 | @Override 126 | @Transactional 127 | public void deleteTransactionally(T obj) { 128 | try { 129 | repository.delete(obj); 130 | LOG.info("DELETED: " + obj); 131 | } catch (PersistenceException e) { 132 | String message = String.format("Unable to delete %s", obj); 133 | throw new ServiceException(message, e); 134 | } 135 | } 136 | 137 | /** 138 | * @see net.filippov.newsportal.service.AbstractService#deleteByIdTransactionally(java.lang.Long) 139 | */ 140 | @Override 141 | @Transactional 142 | public void deleteByIdTransactionally(Long id) { 143 | try { 144 | repository.deleteById(id); 145 | LOG.info("DELETED: {}[id={}]", className, id); 146 | } catch (PersistenceException e) { 147 | String message = String.format("Unable to delete %s by id=", className, id); 148 | throw new ServiceException(message, e); 149 | } 150 | } 151 | 152 | /** 153 | * @see net.filippov.newsportal.service.AbstractService#getAllTransactionally() 154 | */ 155 | @Override 156 | @Transactional 157 | public List getAllTransactionally() { 158 | try { 159 | return repository.getAll(); 160 | } catch (PersistenceException e) { 161 | String message = String.format("Unable to get all: %s", className); 162 | throw new ServiceException(message, e); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/signup.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" 2 | pageEncoding="UTF-8"%> 3 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> 4 | <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> 5 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> 6 | <%@ taglib prefix="t" tagdir="/WEB-INF/tags"%> 7 | 8 | 9 | | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 85 | 86 | 87 | 128 | 129 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/repository/GenericRepositoryJpaImpl.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.repository; 2 | 3 | import java.io.Serializable; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.Map.Entry; 7 | 8 | import javax.persistence.EntityManager; 9 | import javax.persistence.NoResultException; 10 | import javax.persistence.PersistenceContext; 11 | import javax.persistence.TypedQuery; 12 | import javax.persistence.criteria.CriteriaBuilder; 13 | import javax.persistence.criteria.CriteriaQuery; 14 | 15 | import org.springframework.context.annotation.Scope; 16 | import org.springframework.stereotype.Repository; 17 | 18 | /** 19 | * JPA implementation of {@link GenericRepository} 20 | * 21 | * @param The persistent type 22 | * @param The primary key type 23 | * 24 | * @see net.filippov.newsportal.repository.GenericRepository 25 | * 26 | * @author Oleg Filippov 27 | */ 28 | @Repository 29 | @Scope("prototype") 30 | public class GenericRepositoryJpaImpl 31 | implements GenericRepository { 32 | 33 | /** 34 | * Persistent object class 35 | */ 36 | private Class type; 37 | 38 | /** 39 | * @see net.filippov.newsportal.repository.GenericRepository#setType(java.lang.Class) 40 | */ 41 | public void setType(Class type) { 42 | this.type = type; 43 | } 44 | 45 | @PersistenceContext 46 | protected EntityManager manager; 47 | 48 | /** 49 | * @see net.filippov.newsportal.repository.GenericRepository#add(java.lang.Object) 50 | */ 51 | @Override 52 | public void add(T obj) { 53 | manager.persist(obj); 54 | } 55 | 56 | /** 57 | * @see net.filippov.newsportal.repository.GenericRepository#get(java.io.Serializable) 58 | */ 59 | @Override 60 | public T get(PK id) { 61 | return manager.find(type, id); 62 | } 63 | 64 | /** 65 | * @see net.filippov.newsportal.repository.GenericRepository#update(java.lang.Object) 66 | */ 67 | @Override 68 | public T update(T obj) { 69 | return manager.merge(obj); 70 | } 71 | 72 | /** 73 | * @see net.filippov.newsportal.repository.GenericRepository#delete(java.lang.Object) 74 | */ 75 | @Override 76 | public void delete(T obj) { 77 | manager.remove(obj); 78 | } 79 | 80 | /** 81 | * @see net.filippov.newsportal.repository.GenericRepository#deleteById(java.io.Serializable) 82 | */ 83 | @Override 84 | public void deleteById(PK id) { 85 | T obj = this.get(id); 86 | this.delete(obj); 87 | } 88 | 89 | /** 90 | * @see net.filippov.newsportal.repository.GenericRepository#getByNamedQuery( 91 | * java.lang.String, java.util.Map) 92 | */ 93 | @Override 94 | public T getByNamedQuery(String namedQuery, Map parameters) { 95 | try { 96 | TypedQuery query = manager.createNamedQuery(namedQuery, type); 97 | return setParameters(query, parameters).getSingleResult(); 98 | } catch (NoResultException e) { 99 | return null; 100 | } 101 | } 102 | 103 | /** 104 | * @see net.filippov.newsportal.repository.GenericRepository#getAll() 105 | */ 106 | @Override 107 | public List getAll() { 108 | 109 | return manager.createQuery("FROM " + type.getSimpleName(), type).getResultList(); 110 | } 111 | 112 | /** 113 | * @see net.filippov.newsportal.repository.GenericRepository#getAllByNamedQuery( 114 | * java.lang.String) 115 | */ 116 | @Override 117 | public List getAllByNamedQuery(String namedQuery) { 118 | 119 | return getAllByNamedQuery(namedQuery, 0, 0); 120 | } 121 | 122 | /** 123 | * @see net.filippov.newsportal.repository.GenericRepository#getAllByNamedQuery( 124 | * java.lang.String, int, int) 125 | */ 126 | @Override 127 | public List getAllByNamedQuery(String namedQuery, 128 | int firstResult, int resultLimit) { 129 | 130 | return getAllByNamedQuery(namedQuery, null, firstResult, resultLimit); 131 | } 132 | 133 | /** 134 | * @see net.filippov.newsportal.repository.GenericRepository#getAllByNamedQuery( 135 | * java.lang.String, java.util.Map) 136 | */ 137 | @Override 138 | public List getAllByNamedQuery(String namedQuery, 139 | Map parameters) { 140 | 141 | return getAllByNamedQuery(namedQuery, parameters, 0, 0); 142 | } 143 | 144 | /** 145 | * All getAllByNamedQuery...-methods delegate to this one 146 | * 147 | * @see net.filippov.newsportal.repository.GenericRepository#getAllByNamedQuery( 148 | * java.lang.String, java.util.Map, int, int) 149 | */ 150 | @Override 151 | public List getAllByNamedQuery(String namedQuery, 152 | Map parameters, int firstResult, int resultLimit) { 153 | 154 | TypedQuery query = manager.createNamedQuery(namedQuery, type) 155 | .setFirstResult(firstResult); 156 | if (resultLimit > 0) { 157 | query.setMaxResults(resultLimit); 158 | } 159 | if (parameters != null) { 160 | query = setParameters(query, parameters); 161 | } 162 | return query.getResultList(); 163 | } 164 | 165 | /** 166 | * @see net.filippov.newsportal.repository.GenericRepository#getAllNamesByNamedQuery( 167 | * java.lang.String, java.util.Map, int, int) 168 | */ 169 | @Override 170 | public List getAllNamesByNamedQuery(String namedQuery, int resultLimit) { 171 | 172 | TypedQuery query = manager.createNamedQuery(namedQuery, String.class); 173 | if (resultLimit > 0) { 174 | query.setMaxResults(resultLimit); 175 | } 176 | return query.getResultList(); 177 | } 178 | 179 | /** 180 | * @see net.filippov.newsportal.repository.GenericRepository#getCount() 181 | */ 182 | @Override 183 | public int getCount() { 184 | CriteriaBuilder cBuilder = manager.getCriteriaBuilder(); 185 | CriteriaQuery cQuery = cBuilder.createQuery(Long.class); 186 | cQuery.select(cBuilder.count(cQuery.from(type))); 187 | 188 | return manager.createQuery(cQuery).getSingleResult().intValue(); 189 | } 190 | 191 | /** 192 | * @see net.filippov.newsportal.repository.GenericRepository#getCountByNamedQuery(java.lang.String, java.util.Map) 193 | */ 194 | @Override 195 | public int getCountByNamedQuery(String namedQuery, 196 | Map parameters) { 197 | 198 | TypedQuery query = manager.createNamedQuery(namedQuery, Long.class); 199 | return setParameters(query, parameters).getSingleResult().intValue(); 200 | } 201 | 202 | /** 203 | * Bind parameters to the typed query 204 | * @param query typed query 205 | * @param parameters Map of parameters to set 206 | * @return typed query with parameters 207 | */ 208 | private

    TypedQuery

    setParameters(TypedQuery

    query, 209 | Map parameters) { 210 | 211 | for (Entry entry : parameters.entrySet()) { 212 | query.setParameter(entry.getKey(), entry.getValue()); 213 | } 214 | return query; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/views/article-view.jsp: -------------------------------------------------------------------------------- 1 | <%@ page language="java" contentType="text/html; charset=UTF-8" 2 | pageEncoding="UTF-8"%> 3 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> 4 | <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> 5 | <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> 6 | <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> 7 | <%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%> 8 | <%@ taglib prefix="t" tagdir="/WEB-INF/tags"%> 9 | 10 | ${article.title} | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |

    37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 47 | 48 | 49 | 50 |
    51 |
    52 |
    53 |
    54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | : 62 | 63 | 64 |  |  65 | : 66 | 67 |
    68 |
    69 |
    70 | 71 | 72 |
    73 | 74 |
    75 |
    76 | 77 | 78 |

    79 | 80 | 81 | 82 | ${article.commentCount} 83 | 84 | 85 | 86 | 87 | 88 |

    89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 |
    108 |

    109 | 112 | 113 | 116 | 119 |
    120 |
    121 | 122 | 123 |
    124 | 125 |
    126 | 127 | ${comment.author.login} 128 | 129 |  -  130 | 131 | 132 | 133 | 134 |
    135 | 136 |
    137 |
    138 |
    139 | 140 | 141 | 180 | 181 | -------------------------------------------------------------------------------- /src/main/webapp/resources/css/summernote.css: -------------------------------------------------------------------------------- 1 | .note-editor{border:1px solid #a9a9a9}.note-editor .note-dropzone{position:absolute;z-index:1;display:none;color:#87cefa;background-color:white;border:2px dashed #87cefa;opacity:.95;pointer-event:none}.note-editor .note-dropzone .note-dropzone-message{display:table-cell;font-size:28px;font-weight:bold;text-align:center;vertical-align:middle}.note-editor .note-dropzone.hover{color:#098ddf;border:2px dashed #098ddf}.note-editor.dragover .note-dropzone{display:table}.note-editor.fullscreen{position:fixed;top:0;left:0;z-index:1050;width:100%}.note-editor.fullscreen .note-editable{background-color:white}.note-editor.fullscreen .note-resizebar{display:none}.note-editor.codeview .note-editable{display:none}.note-editor.codeview .note-codable{display:block}.note-editor .note-toolbar{padding-bottom:5px;padding-left:5px;margin:0;background-color:#f5f5f5;border-bottom:1px solid #a9a9a9}.note-editor .note-toolbar>.btn-group{margin-top:5px;margin-right:5px;margin-left:0}.note-editor .note-toolbar .note-table .dropdown-menu{min-width:0;padding:5px}.note-editor .note-toolbar .note-table .dropdown-menu .note-dimension-picker{font-size:18px}.note-editor .note-toolbar .note-table .dropdown-menu .note-dimension-picker .note-dimension-picker-mousecatcher{position:absolute!important;z-index:3;width:10em;height:10em;cursor:pointer}.note-editor .note-toolbar .note-table .dropdown-menu .note-dimension-picker .note-dimension-picker-unhighlighted{position:relative!important;z-index:1;width:5em;height:5em;background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASAgMAAAAroGbEAAAACVBMVEUAAIj4+Pjp6ekKlAqjAAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfYAR0BKhmnaJzPAAAAG0lEQVQI12NgAAOtVatWMTCohoaGUY+EmIkEAEruEzK2J7tvAAAAAElFTkSuQmCC') repeat}.note-editor .note-toolbar .note-table .dropdown-menu .note-dimension-picker .note-dimension-picker-highlighted{position:absolute!important;z-index:2;width:1em;height:1em;background:url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASAgMAAAAroGbEAAAACVBMVEUAAIjd6vvD2f9LKLW+AAAAAXRSTlMAQObYZgAAAAFiS0dEAIgFHUgAAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQfYAR0BKwNDEVT0AAAAG0lEQVQI12NgAAOtVatWMTCohoaGUY+EmIkEAEruEzK2J7tvAAAAAElFTkSuQmCC') repeat}.note-editor .note-toolbar .note-style h1,.note-editor .note-toolbar .note-style h2,.note-editor .note-toolbar .note-style h3,.note-editor .note-toolbar .note-style h4,.note-editor .note-toolbar .note-style h5,.note-editor .note-toolbar .note-style h6,.note-editor .note-toolbar .note-style blockquote{margin:0}.note-editor .note-toolbar .note-color .dropdown-toggle{width:20px;padding-left:5px}.note-editor .note-toolbar .note-color .dropdown-menu{min-width:290px}.note-editor .note-toolbar .note-color .dropdown-menu .btn-group{margin:0}.note-editor .note-toolbar .note-color .dropdown-menu .btn-group:first-child{margin:0 5px}.note-editor .note-toolbar .note-color .dropdown-menu .btn-group .note-palette-title{margin:2px 7px;font-size:12px;text-align:center;border-bottom:1px solid #eee}.note-editor .note-toolbar .note-color .dropdown-menu .btn-group .note-color-reset{padding:0 3px;margin:5px;font-size:12px;cursor:pointer;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px}.note-editor .note-toolbar .note-color .dropdown-menu .btn-group .note-color-reset:hover{background:#eee}.note-editor .note-toolbar .note-para .dropdown-menu{min-width:153px;padding:5px}.note-editor .note-toolbar .note-para li:first-child{margin-bottom:5px}.note-editor .note-statusbar{background-color:#f5f5f5}.note-editor .note-statusbar .note-resizebar{width:100%;height:8px;cursor:s-resize;border-top:1px solid #a9a9a9}.note-editor .note-statusbar .note-resizebar .note-icon-bar{width:20px;margin:1px auto;border-top:1px solid #a9a9a9}.note-editor .note-popover .popover{max-width:none}.note-editor .note-popover .popover .popover-content{padding:5px}.note-editor .note-popover .popover .popover-content a{display:inline-block;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle}.note-editor .note-popover .popover .popover-content .btn-group+.btn-group{margin-left:5px}.note-editor .note-popover .popover .arrow{left:20px}.note-editor .note-handle .note-control-selection{position:absolute;display:none;border:1px solid black}.note-editor .note-handle .note-control-selection>div{position:absolute}.note-editor .note-handle .note-control-selection .note-control-selection-bg{width:100%;height:100%;background-color:black;-webkit-opacity:.3;-khtml-opacity:.3;-moz-opacity:.3;opacity:.3;-ms-filter:alpha(opacity=30);filter:alpha(opacity=30)}.note-editor .note-handle .note-control-selection .note-control-handle{width:7px;height:7px;border:1px solid black}.note-editor .note-handle .note-control-selection .note-control-holder{width:7px;height:7px;border:1px solid black}.note-editor .note-handle .note-control-selection .note-control-sizing{width:7px;height:7px;background-color:white;border:1px solid black}.note-editor .note-handle .note-control-selection .note-control-nw{top:-5px;left:-5px;border-right:0;border-bottom:0}.note-editor .note-handle .note-control-selection .note-control-ne{top:-5px;right:-5px;border-bottom:0;border-left:none}.note-editor .note-handle .note-control-selection .note-control-sw{bottom:-5px;left:-5px;border-top:0;border-right:0}.note-editor .note-handle .note-control-selection .note-control-se{right:-5px;bottom:-5px;cursor:se-resize}.note-editor .note-handle .note-control-selection .note-control-selection-info{right:0;bottom:0;padding:5px;margin:5px;font-size:12px;color:white;background-color:black;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;-webkit-opacity:.7;-khtml-opacity:.7;-moz-opacity:.7;opacity:.7;-ms-filter:alpha(opacity=70);filter:alpha(opacity=70)}.note-editor .note-dialog>div{display:none}.note-editor .note-dialog .note-image-dialog .note-dropzone{min-height:100px;margin-bottom:10px;font-size:30px;line-height:4;color:lightgray;text-align:center;border:4px dashed lightgray}.note-editor .note-dialog .note-help-dialog{font-size:12px;color:#ccc;background:transparent;background-color:#222!important;border:0;-webkit-opacity:.9;-khtml-opacity:.9;-moz-opacity:.9;opacity:.9;-ms-filter:alpha(opacity=90);filter:alpha(opacity=90)}.note-editor .note-dialog .note-help-dialog .modal-content{background:transparent;border:1px solid white;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.note-editor .note-dialog .note-help-dialog a{font-size:12px;color:white}.note-editor .note-dialog .note-help-dialog .title{padding-bottom:5px;font-size:14px;font-weight:bold;color:white;border-bottom:white 1px solid}.note-editor .note-dialog .note-help-dialog .modal-close{font-size:14px;color:#dd0;cursor:pointer}.note-editor .note-dialog .note-help-dialog .note-shortcut-layout{width:100%}.note-editor .note-dialog .note-help-dialog .note-shortcut-layout td{vertical-align:top}.note-editor .note-dialog .note-help-dialog .note-shortcut{margin-top:8px}.note-editor .note-dialog .note-help-dialog .note-shortcut th{font-size:13px;color:#dd0;text-align:left}.note-editor .note-dialog .note-help-dialog .note-shortcut td:first-child{min-width:110px;padding-right:10px;font-family:"Courier New";color:#dd0;text-align:right}.note-editor .note-editable{padding:10px;overflow:scroll;outline:0}.note-editor .note-codable{display:none;width:100%;padding:10px;margin-bottom:0;font-family:Menlo,Monaco,monospace,sans-serif;font-size:14px;color:#ccc;background-color:#222;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;box-shadow:none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box;resize:none}.note-editor .dropdown-menu{min-width:90px}.note-editor .dropdown-menu.right{right:0;left:auto}.note-editor .dropdown-menu.right::before{right:9px;left:auto!important}.note-editor .dropdown-menu.right::after{right:10px;left:auto!important}.note-editor .dropdown-menu li a i{color:deepskyblue;visibility:hidden}.note-editor .dropdown-menu li a.checked i{visibility:visible}.note-editor .note-fontsize-10{font-size:10px}.note-editor .note-color-palette{line-height:1}.note-editor .note-color-palette div .note-color-btn{width:17px;height:17px;padding:0;margin:0;border:1px solid #fff}.note-editor .note-color-palette div .note-color-btn:hover{border:1px solid #000} -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/domain/User.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.domain; 2 | 3 | import java.util.Date; 4 | import java.util.Set; 5 | 6 | import javax.persistence.CascadeType; 7 | import javax.persistence.Column; 8 | import javax.persistence.Entity; 9 | import javax.persistence.FetchType; 10 | import javax.persistence.JoinColumn; 11 | import javax.persistence.JoinTable; 12 | import javax.persistence.ManyToMany; 13 | import javax.persistence.NamedQueries; 14 | import javax.persistence.NamedQuery; 15 | import javax.persistence.OneToMany; 16 | import javax.persistence.Table; 17 | import javax.persistence.Temporal; 18 | import javax.persistence.TemporalType; 19 | import javax.validation.constraints.Size; 20 | 21 | import org.hibernate.annotations.Cache; 22 | import org.hibernate.annotations.CacheConcurrencyStrategy; 23 | import org.hibernate.annotations.Formula; 24 | import org.hibernate.validator.constraints.Email; 25 | import org.hibernate.validator.constraints.NotBlank; 26 | 27 | /** 28 | * Stores information about user 29 | * 30 | * @author Oleg Filippov 31 | */ 32 | @Entity 33 | @Table(name = "user") 34 | @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) 35 | @NamedQueries({ 36 | @NamedQuery( 37 | name = "User.GET_BY_LOGIN", 38 | query = "FROM User u WHERE u.login = :login"), 39 | @NamedQuery( 40 | name = "User.GET_BY_EMAIL", 41 | query = "FROM User u WHERE u.email = :email") 42 | }) 43 | public class User extends BaseEntity { 44 | 45 | private static final long serialVersionUID = 8091488929047153516L; 46 | 47 | /** 48 | * User login 49 | */ 50 | @Size(min = 4, max = 30, message = "{validation.user.loginSize}") 51 | @Column(name = "login", nullable = false, unique = true) 52 | private String login; 53 | 54 | /** 55 | * User password 56 | */ 57 | @Size(min = 7, max = 60, message = "{validation.user.passwordSize}") 58 | @Column(name = "password", nullable = false) 59 | private String password; 60 | 61 | /** 62 | * User fullname 63 | */ 64 | @Size(min = 1, max = 50, message = "{validation.user.nameNotBlank}") 65 | @Column(name = "name", nullable = false) 66 | private String name; 67 | 68 | /** 69 | * User e-mail 70 | */ 71 | @NotBlank(message = "{validation.user.emailNotBlank}") 72 | @Email(message = "{validation.user.emailValid}") 73 | @Column(name = "email", nullable = false, unique = true, length = 50) 74 | private String email; 75 | 76 | /** 77 | * Registration date and time 78 | */ 79 | @Column(name = "registered", insertable = false, updatable = false, 80 | columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP") 81 | @Temporal(TemporalType.TIMESTAMP) 82 | private final Date registered; 83 | 84 | /** 85 | * State of user (locked or not) 86 | */ 87 | @Column(name = "locked", insertable = false, 88 | columnDefinition = "BOOLEAN DEFAULT FALSE") 89 | private boolean locked; 90 | 91 | /** 92 | * State of user (enabled or not) 93 | */ 94 | @Column(name = "enabled", insertable = false, 95 | columnDefinition = "BOOLEAN DEFAULT TRUE") 96 | private boolean enabled; 97 | 98 | /** 99 | * Article count of this user 100 | */ 101 | @Formula("SELECT COUNT(a.id) FROM Article a WHERE a.user_id = id") 102 | private int articleCount; 103 | 104 | /** 105 | * Comment count of this user 106 | */ 107 | @Formula("SELECT COUNT(c.id) FROM Comment c WHERE c.user_id = id") 108 | private int commentCount; 109 | 110 | /** 111 | * User roles 112 | */ 113 | @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) 114 | @JoinTable(name = "user_role", 115 | joinColumns = {@JoinColumn(name = "user_id") }, 116 | inverseJoinColumns = {@JoinColumn(name = "role_id") }) 117 | private Set roles; 118 | 119 | /** 120 | * User articles 121 | */ 122 | @OneToMany(mappedBy = "author", fetch = FetchType.LAZY) 123 | private Set
    articles; 124 | 125 | /** 126 | * User comments 127 | */ 128 | @OneToMany(mappedBy = "author", fetch = FetchType.LAZY) 129 | private Set comments; 130 | 131 | /** 132 | * Default constructor initializing needed fields 133 | */ 134 | public User() { 135 | registered = new Date(); 136 | locked = false; 137 | enabled = true; 138 | } 139 | 140 | /** 141 | * @return user login 142 | */ 143 | public String getLogin() { 144 | return login; 145 | } 146 | 147 | /** 148 | * @param login user login to set 149 | */ 150 | public void setLogin(String login) { 151 | this.login = login; 152 | } 153 | 154 | /** 155 | * @return user password 156 | */ 157 | public String getPassword() { 158 | return password; 159 | } 160 | 161 | /** 162 | * @param password user password to set 163 | */ 164 | public void setPassword(String password) { 165 | this.password = password; 166 | } 167 | 168 | /** 169 | * @return user full name 170 | */ 171 | public String getName() { 172 | return name; 173 | } 174 | 175 | /** 176 | * @param name user full name to set 177 | */ 178 | public void setName(String name) { 179 | this.name = name; 180 | } 181 | 182 | /** 183 | * @return user e-mail 184 | */ 185 | public String getEmail() { 186 | return email; 187 | } 188 | 189 | /** 190 | * @param email user e-mail to set 191 | */ 192 | public void setEmail(String email) { 193 | this.email = email; 194 | } 195 | 196 | /** 197 | * @return registration date and time 198 | */ 199 | public Date getRegistered() { 200 | return registered; 201 | } 202 | 203 | /** 204 | * @return true if user is locked 205 | */ 206 | public boolean isLocked() { 207 | return locked; 208 | } 209 | 210 | /** 211 | * @param locked 212 | */ 213 | public void setLocked(boolean locked) { 214 | this.locked = locked; 215 | } 216 | 217 | /** 218 | * @return true if user is enabled 219 | */ 220 | public boolean isEnabled() { 221 | return enabled; 222 | } 223 | 224 | /** 225 | * @param enabled 226 | */ 227 | public void setEnabled(boolean enabled) { 228 | this.enabled = enabled; 229 | } 230 | 231 | /** 232 | * @return this user article count 233 | */ 234 | public int getArticleCount() { 235 | return articleCount; 236 | } 237 | 238 | /** 239 | * @return this user comment count 240 | */ 241 | public int getCommentCount() { 242 | return commentCount; 243 | } 244 | 245 | /** 246 | * @return user roles 247 | */ 248 | public Set getRoles() { 249 | return roles; 250 | } 251 | 252 | /** 253 | * @param roles user roles to set 254 | */ 255 | public void setRoles(Set roles) { 256 | this.roles = roles; 257 | } 258 | 259 | /** 260 | * @return this user comments 261 | */ 262 | public Set getComments() { 263 | return comments; 264 | } 265 | 266 | /** 267 | * @param comments this user comments to set 268 | */ 269 | public void setComments(Set comments) { 270 | this.comments = comments; 271 | } 272 | 273 | /** 274 | * @return this user articles 275 | */ 276 | public Set
    getArticles() { 277 | return articles; 278 | } 279 | 280 | /** 281 | * @param articles this user articles to set 282 | */ 283 | public void setArticles(Set
    articles) { 284 | this.articles = articles; 285 | } 286 | 287 | /** 288 | * @see java.lang.Object#hashCode() 289 | */ 290 | @Override 291 | public int hashCode() { 292 | final int prime = 31; 293 | int result = 17; 294 | result = prime * result 295 | + ((getLogin() == null) ? 0 : getLogin().hashCode()); 296 | return result; 297 | } 298 | 299 | /** 300 | * @see java.lang.Object#equals(java.lang.Object) 301 | */ 302 | @Override 303 | public boolean equals(Object obj) { 304 | if (this == obj) 305 | return true; 306 | if (obj == null) 307 | return false; 308 | if (getClass() != obj.getClass()) 309 | return false; 310 | 311 | User other = (User) obj; 312 | 313 | if (getLogin() != null 314 | ? !getLogin().equals(other.getLogin()) 315 | : other.getLogin() != null) { 316 | return false; 317 | } 318 | return true; 319 | } 320 | 321 | /** 322 | * @see java.lang.Object#toString() 323 | */ 324 | @Override 325 | public String toString() { 326 | return String.format("User[id=%d, login=%s]", getId(), getLogin()); 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/web/SearchController.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.web; 2 | 3 | import java.util.List; 4 | import java.util.Map; 5 | 6 | import net.filippov.newsportal.domain.Article; 7 | import net.filippov.newsportal.exception.NotFoundException; 8 | import net.filippov.newsportal.service.ArticleService; 9 | import net.filippov.newsportal.web.constants.URL; 10 | import net.filippov.newsportal.web.constants.View; 11 | import net.filippov.newsportal.web.constants.Common; 12 | 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.context.ApplicationContext; 15 | import org.springframework.context.i18n.LocaleContextHolder; 16 | import org.springframework.stereotype.Controller; 17 | import org.springframework.web.bind.annotation.PathVariable; 18 | import org.springframework.web.bind.annotation.RequestMapping; 19 | import org.springframework.web.bind.annotation.RequestMethod; 20 | import org.springframework.web.bind.annotation.RequestParam; 21 | import org.springframework.web.servlet.ModelAndView; 22 | 23 | /** 24 | * Controller for search-actions 25 | * 26 | * @author Oleg Filippov 27 | */ 28 | @Controller 29 | public class SearchController { 30 | 31 | private ArticleService articleService; 32 | private ApplicationContext context; 33 | 34 | /** 35 | * Constructor autowiring needed services 36 | * 37 | * @param articleService {@link ArticleService} 38 | * @param context {@link ApplicationContext} 39 | */ 40 | @Autowired 41 | public SearchController(ArticleService articleService, 42 | ApplicationContext context) { 43 | this.articleService = articleService; 44 | this.context = context; 45 | } 46 | 47 | /** 48 | * Search by category - first page 49 | */ 50 | @RequestMapping(method = RequestMethod.GET, value = URL.SEARCH_BY_CATEGORY) 51 | public ModelAndView viewArticlesByCategory(@PathVariable("name") String categoryName) { 52 | 53 | return categoryModelAndView(1, categoryName); 54 | } 55 | 56 | /** 57 | * Search by category - custom page 58 | */ 59 | @RequestMapping(method = RequestMethod.GET, value = URL.SEARCH_BY_CATEGORY_CUSTOM_PAGE) 60 | public ModelAndView viewArticlesByCategory(@PathVariable("name") String categoryName, 61 | @PathVariable("number") Integer pageNumber) { 62 | 63 | validatePageNumber(pageNumber); 64 | if (pageNumber == 1) { 65 | return new ModelAndView("redirect:" + URL.SEARCH_BY_CATEGORY); 66 | } 67 | return categoryModelAndView(pageNumber, categoryName); 68 | } 69 | 70 | /** 71 | * Prepare searchByCategory-objects for search-ModelAndView 72 | */ 73 | private ModelAndView categoryModelAndView(Integer pageNumber, String categoryName) { 74 | 75 | Map articlesData = articleService.getByPageByCategoryName( 76 | pageNumber, Common.ARTICLES_PER_PAGE, categoryName); 77 | String requestUrl = String.format("/category/%s/", categoryName); 78 | String message = String.format(context.getMessage("search.result.byCategory", null, 79 | LocaleContextHolder.getLocale()), categoryName); 80 | 81 | return searchModelAndView(articlesData, pageNumber, message, requestUrl); 82 | } 83 | 84 | /** 85 | * Search by tag - first page 86 | */ 87 | @RequestMapping(method = RequestMethod.GET, value = URL.SEARCH_BY_TAG) 88 | public ModelAndView viewArticlesByTag(@PathVariable("name") String tagName) { 89 | 90 | return tagModelAndView(1, tagName); 91 | } 92 | 93 | /** 94 | * Search by tag - custom page 95 | */ 96 | @RequestMapping(method = RequestMethod.GET, value = URL.SEARCH_BY_TAG_CUSTOM_PAGE) 97 | public ModelAndView viewArticlesByTag(@PathVariable("name") String tagName, 98 | @PathVariable("number") Integer pageNumber) { 99 | 100 | validatePageNumber(pageNumber); 101 | if (pageNumber == 1) { 102 | return new ModelAndView("redirect:" + URL.SEARCH_BY_TAG); 103 | } 104 | return tagModelAndView(pageNumber, tagName); 105 | } 106 | 107 | /** 108 | * Prepare searchByTag-objects for search-ModelAndView 109 | */ 110 | private ModelAndView tagModelAndView(Integer pageNumber, String tagName) { 111 | 112 | Map articlesData = articleService.getByPageByTagName( 113 | pageNumber, Common.ARTICLES_PER_PAGE, tagName); 114 | String requestUrl = String.format("/tags/%s/", tagName); 115 | String message = String.format(context.getMessage("search.result.byTag", null, 116 | LocaleContextHolder.getLocale()), tagName); 117 | 118 | return searchModelAndView(articlesData, pageNumber, message, requestUrl); 119 | } 120 | 121 | /** 122 | * Search by user - first page 123 | */ 124 | @RequestMapping(method = RequestMethod.GET, value = URL.SEARCH_BY_USER) 125 | public ModelAndView viewArticlesByUser(@PathVariable("id") Long userId) { 126 | 127 | return userModelAndView(1, userId); 128 | } 129 | 130 | /** 131 | * Search by user - first page 132 | */ 133 | @RequestMapping(method = RequestMethod.GET, value = URL.SEARCH_BY_USER_CUSTOM_PAGE) 134 | public ModelAndView viewArticlesByUser(@PathVariable("id") Long userId, 135 | @PathVariable("number") Integer pageNumber) { 136 | 137 | validatePageNumber(pageNumber); 138 | if (pageNumber == 1) { 139 | return new ModelAndView("redirect:" + URL.SEARCH_BY_USER); 140 | } 141 | return userModelAndView(pageNumber, userId); 142 | } 143 | 144 | /** 145 | * Prepare searchByUser-objects for search-ModelAndView 146 | */ 147 | private ModelAndView userModelAndView(Integer pageNumber, Long userId) { 148 | 149 | Map articlesData = articleService.getByPageByUserId( 150 | pageNumber, Common.ARTICLES_PER_PAGE, userId); 151 | String userLogin = (String) articlesData.get("userLogin"); 152 | String requestUrl = String.format("/user/%d/articles/", userId); 153 | String message = String.format(context.getMessage("search.result.byUser", null, 154 | LocaleContextHolder.getLocale()), userLogin); 155 | 156 | return searchModelAndView(articlesData, pageNumber, message, requestUrl); 157 | } 158 | 159 | /** 160 | * Search by fragment submit-form 161 | */ 162 | @RequestMapping(method = RequestMethod.POST, value = URL.SEARCH_BY_FRAGMENT_SUBMIT) 163 | public String searchSubmit(@RequestParam("fragment") String fragment) { 164 | 165 | return "redirect:/search/" + fragment; 166 | } 167 | 168 | /** 169 | * Search by fragment - first page 170 | */ 171 | @RequestMapping(method = RequestMethod.GET, value = URL.SEARCH_BY_FRAGMENT) 172 | public ModelAndView viewArticlesByFragment(@PathVariable("fragment") String fragment) { 173 | 174 | return fragmentModelAndView(1, fragment); 175 | } 176 | 177 | /** 178 | * Search by fragment - custom page 179 | */ 180 | @RequestMapping(method = RequestMethod.GET, value = URL.SEARCH_BY_FRAGMENT_CUSTOM_PAGE) 181 | public ModelAndView viewArticlesByFragment(@PathVariable("fragment") String fragment, 182 | @PathVariable("number") Integer pageNumber) { 183 | 184 | validatePageNumber(pageNumber); 185 | if (pageNumber == 1) { 186 | return new ModelAndView("redirect:" + URL.SEARCH_BY_FRAGMENT); 187 | } 188 | return fragmentModelAndView(pageNumber, fragment); 189 | } 190 | 191 | /** 192 | * Prepare searchByFragment-objects for search-ModelAndView 193 | */ 194 | private ModelAndView fragmentModelAndView(Integer pageNumber, String fragment) { 195 | 196 | Map articlesData = articleService.getByPageByFragment( 197 | pageNumber, Common.ARTICLES_PER_PAGE, fragment); 198 | String requestUrl = String.format("/search/%s/", fragment); 199 | String message = String.format(context.getMessage("search.result.byFragment", null, 200 | LocaleContextHolder.getLocale()), fragment); 201 | 202 | return searchModelAndView(articlesData, pageNumber, message, requestUrl); 203 | } 204 | 205 | /** 206 | * Fill search-ModelAndView with needed objects 207 | * 208 | * @param articlesData Map with articles 209 | * @param pageNumber number of page 210 | * @param message localized search-message 211 | * @param requestUrl root URL 212 | * @return filled {@link ModelAndView} 213 | */ 214 | private ModelAndView searchModelAndView(Map articlesData, 215 | Integer pageNumber, String message, String requestUrl) { 216 | 217 | // Set required model attributes 218 | ModelAndView mav = new ModelAndView(View.SEARCH) 219 | .addObject("message", message) 220 | .addObject("requestUrl", requestUrl) 221 | .addObject("currentPage", pageNumber); 222 | 223 | if (articlesData.isEmpty()) { // articleCount == 0 224 | return mav; 225 | } 226 | 227 | Integer pageCount = (Integer) articlesData.get("pageCount"); 228 | 229 | @SuppressWarnings("unchecked") 230 | List
    articlesByPage = (List
    ) articlesData.get("articlesByPage"); 231 | 232 | return mav.addObject("pageCount", pageCount) 233 | .addObject("articlesByPage", articlesByPage); 234 | } 235 | 236 | private void validatePageNumber(int pageNumber) { 237 | if (pageNumber < 1) { 238 | throw new NotFoundException("Page < 1"); 239 | } 240 | } 241 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | net.filippov.newsportal 6 | newsportal 7 | Newsportal 8 | war 9 | 1.0.2 10 | 11 | 12 | UTF-8 13 | 1.7 14 | 3.2.4.RELEASE 15 | 3.1.4.RELEASE 16 | 3.0.1 17 | 4.2.7.Final 18 | 4.1.0.Final 19 | 2.6.8 20 | 1.7.0 21 | 1.4 22 | 1.3.174 23 | 1.7.2 24 | 1.2.17 25 | 26 | 27 | 28 | 29 | 30 | org.springframework 31 | spring-core 32 | ${spring.version} 33 | 34 | 35 | 36 | commons-logging 37 | commons-logging 38 | 39 | 40 | 41 | 42 | org.springframework 43 | spring-context 44 | ${spring.version} 45 | 46 | 47 | org.springframework 48 | spring-webmvc 49 | ${spring.version} 50 | 51 | 52 | org.springframework.security 53 | spring-security-core 54 | ${spring.security.version} 55 | 56 | 57 | org.springframework.security 58 | spring-security-web 59 | ${spring.security.version} 60 | 61 | 62 | org.springframework.security 63 | spring-security-config 64 | ${spring.security.version} 65 | 66 | 67 | org.springframework.security 68 | spring-security-taglibs 69 | ${spring.security.version} 70 | 71 | 72 | 73 | 74 | org.springframework 75 | spring-orm 76 | ${spring.version} 77 | 78 | 79 | org.hibernate 80 | hibernate-core 81 | ${hibernate.version} 82 | 83 | 84 | org.hibernate 85 | hibernate-entitymanager 86 | ${hibernate.version} 87 | 88 | 89 | org.hibernate 90 | hibernate-ehcache 91 | ${hibernate.version} 92 | 93 | 94 | commons-dbcp 95 | commons-dbcp 96 | ${dbcp.version} 97 | 98 | 99 | com.h2database 100 | h2 101 | ${h2database.version} 102 | 103 | 104 | 105 | 106 | javax.servlet 107 | javax.servlet-api 108 | 3.0.1 109 | provided 110 | 111 | 112 | javax.servlet.jsp 113 | jsp-api 114 | 2.1 115 | provided 116 | 117 | 118 | javax.servlet 119 | jstl 120 | 1.2 121 | 122 | 123 | 124 | 125 | javax.inject 126 | javax.inject 127 | 1 128 | 129 | 130 | 131 | 132 | org.hibernate 133 | hibernate-validator 134 | ${hibernate.validator.version} 135 | 136 | 137 | net.sf.ehcache 138 | ehcache-core 139 | ${ehcache.version} 140 | 141 | 142 | 143 | 144 | org.aspectj 145 | aspectjrt 146 | ${aspectj.version} 147 | 148 | 149 | 150 | 151 | org.slf4j 152 | slf4j-api 153 | ${slf4j.version} 154 | 155 | 156 | org.slf4j 157 | jcl-over-slf4j 158 | ${slf4j.version} 159 | runtime 160 | 161 | 162 | org.slf4j 163 | slf4j-log4j12 164 | ${slf4j.version} 165 | runtime 166 | 167 | 168 | log4j 169 | apache-log4j-extras 170 | ${log4j.version} 171 | 172 | 173 | javax.mail 174 | mail 175 | 176 | 177 | javax.jms 178 | jms 179 | 180 | 181 | com.sun.jdmk 182 | jmxtools 183 | 184 | 185 | com.sun.jmx 186 | jmxri 187 | 188 | 189 | runtime 190 | 191 | 192 | 193 | 194 | junit 195 | junit 196 | 4.7 197 | test 198 | 199 | 200 | org.springframework 201 | spring-test 202 | ${spring.version} 203 | test 204 | 205 | 206 | 207 | 208 | commons-io 209 | commons-io 210 | 1.3.2 211 | 212 | 213 | commons-fileupload 214 | commons-fileupload 215 | 1.2 216 | 217 | 218 | 219 | 220 | 221 | 222 | maven-eclipse-plugin 223 | 2.9 224 | 225 | 226 | org.springframework.ide.eclipse.core.springnature 227 | 228 | 229 | org.springframework.ide.eclipse.core.springbuilder 230 | 231 | true 232 | true 233 | 234 | 235 | 236 | org.apache.maven.plugins 237 | maven-compiler-plugin 238 | 2.5.1 239 | 240 | 1.7 241 | 1.7 242 | -Xlint:all 243 | true 244 | true 245 | true 246 | 247 | 248 | 249 | org.apache.tomcat.maven 250 | tomcat7-maven-plugin 251 | 2.2 252 | 253 | /newsportal 254 | 255 | 256 | 257 | org.codehaus.mojo 258 | exec-maven-plugin 259 | 1.2.1 260 | 261 | org.test.int1.Main 262 | 263 | 264 | 265 | maven-war-plugin 266 | 2.1.1 267 | 268 | WEB-INF/web.xml 269 | 270 | 271 | 272 | 273 | 274 | -------------------------------------------------------------------------------- /src/main/java/net/filippov/newsportal/domain/Article.java: -------------------------------------------------------------------------------- 1 | package net.filippov.newsportal.domain; 2 | 3 | import java.util.Date; 4 | import java.util.Set; 5 | 6 | import javax.persistence.CascadeType; 7 | import javax.persistence.Column; 8 | import javax.persistence.Entity; 9 | import javax.persistence.FetchType; 10 | import javax.persistence.JoinColumn; 11 | import javax.persistence.JoinTable; 12 | import javax.persistence.ManyToMany; 13 | import javax.persistence.ManyToOne; 14 | import javax.persistence.NamedQueries; 15 | import javax.persistence.NamedQuery; 16 | import javax.persistence.OneToMany; 17 | import javax.persistence.OrderBy; 18 | import javax.persistence.Table; 19 | import javax.persistence.Temporal; 20 | import javax.persistence.TemporalType; 21 | 22 | import org.hibernate.annotations.Cache; 23 | import org.hibernate.annotations.CacheConcurrencyStrategy; 24 | import org.hibernate.annotations.Formula; 25 | import org.hibernate.validator.constraints.NotBlank; 26 | 27 | /** 28 | * Represents an article with String content 29 | * 30 | * @author Oleg Filippov 31 | */ 32 | @Entity 33 | @Table(name = "article") 34 | @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE) 35 | @NamedQueries({ 36 | @NamedQuery( 37 | name = "Article.GET_ALL", 38 | query = "FROM Article a ORDER BY a.created DESC"), 39 | @NamedQuery( 40 | name = "Article.GET_ALL_BY_USER_ID", 41 | query = "FROM Article a WHERE a.author.id = :id ORDER BY a.created DESC"), 42 | @NamedQuery( 43 | name = "Article.GET_ALL_BY_CATEGORY_NAME", 44 | query = "FROM Article a WHERE a.category.name = :name ORDER BY a.created DESC"), 45 | @NamedQuery( 46 | name = "Article.GET_ALL_BY_TAG_NAME", 47 | query = "SELECT a FROM Tag t JOIN t.articles a WHERE t.name = :name ORDER BY a.created DESC"), 48 | @NamedQuery( 49 | name = "Article.GET_ALL_BY_FRAGMENT", 50 | query = "FROM Article a WHERE a.content LIKE :fragment ORDER BY a.created DESC"), 51 | @NamedQuery( 52 | name = "Article.GET_COUNT_BY_FRAGMENT", 53 | query = "SELECT COUNT(a.id) FROM Article a WHERE a.content LIKE :fragment") 54 | }) 55 | public class Article extends BaseEntity { 56 | 57 | private static final long serialVersionUID = 38150497082508411L; 58 | 59 | /** 60 | * Article title 61 | */ 62 | @NotBlank(message = "{validation.article.title}") 63 | @Column(name = "title", nullable = false, length = 100) 64 | private String title; 65 | 66 | /** 67 | * Article preview 68 | */ 69 | @NotBlank(message = "{validation.article.preview}") 70 | @Column(name = "preview", nullable = false) 71 | private String preview; 72 | 73 | /** 74 | * Article content 75 | */ 76 | @NotBlank(message = "{validation.article.content}") 77 | @Column(name = "content", nullable = false, length = 65535) 78 | private String content; 79 | 80 | /** 81 | * Article creation date and time 82 | */ 83 | @Column(name = "created", insertable = false, updatable = false, 84 | columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP") 85 | @Temporal(TemporalType.TIMESTAMP) 86 | private final Date created; 87 | 88 | /** 89 | * Article modification date and time 90 | */ 91 | @Column(name = "last_modified") 92 | @Temporal(TemporalType.TIMESTAMP) 93 | private Date lastModified; 94 | 95 | /** 96 | * Article view count 97 | */ 98 | @Column(name = "view_count", insertable = false, columnDefinition = "INT DEFAULT 0") 99 | private int viewCount; 100 | 101 | /** 102 | * Article comment count 103 | */ 104 | @Formula("SELECT COUNT(id) FROM Comment c WHERE c.article_id = id") 105 | private int commentCount; 106 | 107 | /** 108 | * Author of this article 109 | */ 110 | @ManyToOne(fetch = FetchType.EAGER) 111 | @JoinColumn(name = "user_id", nullable = false, updatable = false) 112 | private User author; 113 | 114 | /** 115 | * Category of this article 116 | */ 117 | @ManyToOne 118 | @JoinColumn(name = "category_id") 119 | private Category category; 120 | 121 | /** 122 | * Comments to this article 123 | */ 124 | @OneToMany(mappedBy = "article", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) 125 | @OrderBy("created desc") 126 | private Set comments; 127 | 128 | /** 129 | * Tags of this article 130 | */ 131 | @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.MERGE}) 132 | @JoinTable(name = "article_tag", 133 | joinColumns = {@JoinColumn(name = "article_id") }, 134 | inverseJoinColumns = {@JoinColumn(name = "tag_id") }) 135 | @OrderBy("name") 136 | private Set tags; 137 | 138 | /** 139 | * Default constructor initializing needed fields 140 | */ 141 | public Article() { 142 | created = new Date(); 143 | viewCount = 0; 144 | } 145 | 146 | /** 147 | * @return article title 148 | */ 149 | public String getTitle() { 150 | return title; 151 | } 152 | 153 | /** 154 | * @param title article title to set 155 | */ 156 | public void setTitle(String title) { 157 | this.title = title; 158 | } 159 | 160 | /** 161 | * @return article preview 162 | */ 163 | public String getPreview() { 164 | return preview; 165 | } 166 | 167 | /** 168 | * @param preview article preview to set 169 | */ 170 | public void setPreview(String preview) { 171 | this.preview = preview; 172 | } 173 | 174 | /** 175 | * @return article content 176 | */ 177 | public String getContent() { 178 | return content; 179 | } 180 | 181 | /** 182 | * @param content article content to set 183 | */ 184 | public void setContent(String content) { 185 | this.content = content; 186 | } 187 | 188 | /** 189 | * @return article date and time of creation 190 | */ 191 | public Date getCreated() { 192 | return created; 193 | } 194 | 195 | /** 196 | * @return article modification date and time 197 | */ 198 | public Date getLastModified() { 199 | return lastModified; 200 | } 201 | 202 | /** 203 | * @param lastModified date and time when article was last modified 204 | */ 205 | public void setLastModified(Date lastModified) { 206 | this.lastModified = lastModified; 207 | } 208 | 209 | /** 210 | * @return article view count 211 | */ 212 | public int getViewCount() { 213 | return viewCount; 214 | } 215 | 216 | /** 217 | * @param viewCount article view count to set 218 | */ 219 | public void setViewCount(int viewCount) { 220 | this.viewCount = viewCount; 221 | } 222 | 223 | /** 224 | * @return article comment count 225 | */ 226 | public int getCommentCount() { 227 | return commentCount; 228 | } 229 | 230 | /** 231 | * @return author of this article 232 | */ 233 | public User getAuthor() { 234 | return author; 235 | } 236 | 237 | /** 238 | * @param author author of this article to set 239 | */ 240 | public void setAuthor(User author) { 241 | this.author = author; 242 | } 243 | 244 | /** 245 | * @return category of this article 246 | */ 247 | public Category getCategory() { 248 | return category; 249 | } 250 | 251 | /** 252 | * @param category category of this article to set 253 | */ 254 | public void setCategory(Category category) { 255 | this.category = category; 256 | } 257 | 258 | /** 259 | * @return comments to this article 260 | */ 261 | public Set getComments() { 262 | return comments; 263 | } 264 | 265 | /** 266 | * @param comments comments to this article to set 267 | */ 268 | public void setComments(Set comments) { 269 | this.comments = comments; 270 | } 271 | 272 | /** 273 | * @return tags of this article 274 | */ 275 | public Set getTags() { 276 | return tags; 277 | } 278 | 279 | /** 280 | * @param tags tags of this article to set 281 | */ 282 | public void setTags(Set tags) { 283 | this.tags = tags; 284 | } 285 | 286 | /** 287 | * @see java.lang.Object#hashCode() 288 | */ 289 | @Override 290 | public int hashCode() { 291 | final int prime = 31; 292 | int result = 17; 293 | result = prime * result 294 | + ((getTitle() == null) ? 0 : getTitle().hashCode()); 295 | result = prime * result 296 | + ((getCreated() == null) ? 0 : getCreated().hashCode()); 297 | return result; 298 | } 299 | 300 | /** 301 | * @see java.lang.Object#equals(java.lang.Object) 302 | */ 303 | @Override 304 | public boolean equals(Object obj) { 305 | if (this == obj) 306 | return true; 307 | if (obj == null) 308 | return false; 309 | if (getClass() != obj.getClass()) 310 | return false; 311 | 312 | Article other = (Article) obj; 313 | 314 | if (getTitle() != null 315 | ? !getTitle().equals(other.getTitle()) 316 | : other.getTitle() != null) { 317 | return false; 318 | } 319 | if (getCreated() != null 320 | ? getCreated().compareTo(other.getCreated()) != 0 321 | : other.getCreated() != null) { 322 | return false; 323 | } 324 | return true; 325 | } 326 | 327 | /** 328 | * @see java.lang.Object#toString() 329 | */ 330 | @Override 331 | public String toString() { 332 | return String.format("Article[id=%d, author=%s]", 333 | getId(), getAuthor() == null ? "null" : getAuthor().getLogin()); 334 | } 335 | } 336 | --------------------------------------------------------------------------------