├── .gitignore
├── LICENSE.txt
├── README.md
├── pom.xml
└── src
├── main
├── java
│ └── alexp
│ │ └── blog
│ │ ├── controller
│ │ ├── CommentController.java
│ │ ├── ExceptionController.java
│ │ ├── ForbiddenException.java
│ │ ├── LoginController.java
│ │ ├── PostsController.java
│ │ ├── ResourceNotFoundException.java
│ │ └── UsersController.java
│ │ ├── model
│ │ ├── Comment.java
│ │ ├── CommentRating.java
│ │ ├── Post.java
│ │ ├── PostEditDto.java
│ │ ├── PostRating.java
│ │ ├── Rating.java
│ │ ├── Role.java
│ │ ├── Tag.java
│ │ └── User.java
│ │ ├── repository
│ │ ├── CommentRatingRepository.java
│ │ ├── CommentRepository.java
│ │ ├── PostRatingRepository.java
│ │ ├── PostRepository.java
│ │ ├── RoleRepository.java
│ │ ├── TagRepository.java
│ │ └── UserRepository.java
│ │ ├── service
│ │ ├── ActionExpiredException.java
│ │ ├── AlreadyVotedException.java
│ │ ├── AuthException.java
│ │ ├── AvatarService.java
│ │ ├── AvatarServiceImpl.java
│ │ ├── CommentService.java
│ │ ├── CommentServiceImpl.java
│ │ ├── FileNameGenerator.java
│ │ ├── FileNameGeneratorImpl.java
│ │ ├── MarkdownConverter.java
│ │ ├── PostService.java
│ │ ├── PostServiceImpl.java
│ │ ├── UnsupportedFormatException.java
│ │ ├── UploadedAvatarInfo.java
│ │ ├── UserService.java
│ │ └── UserServiceImpl.java
│ │ ├── utils
│ │ ├── JsonUtils.java
│ │ ├── LocalDatePersistenceConverter.java
│ │ └── LocalDateTimePersistenceConverter.java
│ │ └── validator
│ │ └── UserValidator.java
├── resources
│ ├── META-INF
│ │ └── persistence.xml
│ ├── Messages.properties
│ ├── database.xml
│ ├── datasource-dev.xml
│ ├── datasource-test.xml
│ ├── dummy-data.sql
│ ├── security-tables.sql
│ ├── uploading-dev.properties
│ ├── uploading-prod.properties
│ └── uploading-test.properties
└── webapp
│ ├── WEB-INF
│ ├── mvc-dispatcher-servlet.xml
│ ├── spring-security.xml
│ ├── templates
│ │ ├── 403.html
│ │ ├── 404.html
│ │ ├── about.html
│ │ ├── editpost.html
│ │ ├── editprofile.html
│ │ ├── error.html
│ │ ├── fragments
│ │ │ ├── comments.html
│ │ │ └── loginform.html
│ │ ├── layouts
│ │ │ └── blog.html
│ │ ├── logout.html
│ │ ├── post.html
│ │ ├── posts.html
│ │ ├── profile.html
│ │ ├── registration.html
│ │ └── settings.html
│ └── web.xml
│ ├── css
│ ├── blog.css
│ └── external
│ │ ├── fileupload.css
│ │ └── pagedown.css
│ ├── favicon.ico
│ ├── images
│ ├── 404.png
│ ├── ajax-loader.gif
│ ├── cat-working.png
│ ├── no-avatar-big.png
│ ├── no-avatar-small.png
│ └── success-tick.png
│ └── js
│ ├── admin.js
│ ├── comments.js
│ ├── common.js
│ ├── editpost.js
│ ├── editprofile.js
│ ├── external
│ ├── bootbox.min.js
│ ├── fileupload
│ │ ├── jquery.fileupload.js
│ │ └── jquery.iframe-transport.js
│ ├── markdown
│ │ ├── Markdown.Converter.js
│ │ ├── Markdown.Editor.js
│ │ └── Markdown.Sanitizer.js
│ └── xregexp-all-min.js
│ ├── login.js
│ ├── registration.js
│ ├── settings.js
│ ├── voting.js
│ └── wmd-buttons.png
└── test
├── java
└── alexp
│ └── blog
│ ├── AbstractIntegrationTest.java
│ ├── controller
│ ├── CommentControllerIT.java
│ ├── LoginControllerIT.java
│ ├── PostsControllerIT.java
│ ├── UserAvatarUploadIT.java
│ └── UsersControllerIT.java
│ ├── model
│ ├── CommentTest.java
│ ├── PostTest.java
│ └── UserTest.java
│ ├── service
│ ├── CommentServiceTest.java
│ ├── MarkdownConverterTest.java
│ ├── PostServiceTest.java
│ └── UserServiceTest.java
│ └── utils
│ ├── HsqldbSequenceResetter.java
│ └── SecurityUtils.java
└── resources
└── alexp
└── blog
└── controller
├── data-avatar-added.xml
├── data-comment-added-and-edited.xml
├── data-comment-added-and-marked-deleted.xml
├── data-comment-added.xml
├── data-comment-edited.xml
├── data-comment-marked-deleted.xml
├── data-comment-reply-added-limit-exceeded.xml
├── data-comment-reply-added.xml
├── data-comments-voted.xml
├── data-email-changed.xml
├── data-password-changed.xml
├── data-post-deleted.xml
├── data-post-edited.xml
├── data-post-hidden.xml
├── data-posts-added.xml
├── data-posts-voted.xml
├── data-profile-changed.xml
├── data-user-added.xml
├── data.xml
└── img.jpg
/.gitignore:
--------------------------------------------------------------------------------
1 | target
2 | *.iml
3 | .idea
4 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 AlexP11223
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | A simple blog created during Java/OOP course in university.
2 |
3 | Username/password auth, admin can add/edit/delete blog posts, users (after registration) can add comments, vote (+1/-1) for posts and comments.
4 |
5 | Can be useful for someone who is learning the techonologies/frameworks used here.
6 |
7 |
8 | - Spring MVC, Spring Security. Spring profiles.
9 | - Hibernate, MySQL database.
10 | - Thymeleaf, Bootstrap, jQuery.
11 | - [PageDown](https://code.google.com/archive/p/pagedown/) (Markdown editor), and [Txtmark](https://github.com/rjeschke/txtmark) in backend.
12 | - Tests (unit and integration). JUnit, Mockito, DBUnit with HSQLDB in-memory database (and Spring-Test/Spring-Test-DBUnit of course, since I was using Spring).
13 |
14 | Features:
15 | - Posts and comments support Markdown syntax.
16 | - Post can be split into two parts to show only the first (short) part on the main page and the full content when navigated to the post page.
17 | - Comment hierarchy (up to 6 levels): user can reply to the specified comment instead of just adding comment at the bottom.
18 | - Users can fill some info ("about me" text, website link) and upload avatar image, that will be displayed in their profiles (`/users/username` url or link on each username in the comments section), and small avatar image is also displayed in each comment.
19 | - Users can change e-mail and password ("Edit settings" button).
20 | - Admin can edit posts, hide posts (will not be visible for other users) or completely delete it from the database. (buttons at the bottom of each post, the delete button is shown only after the post is hidden)
21 | - Users can delete their comments during 10 minutes after it was added and edit during 3 hours. Admin can edit/delete any comment.
22 | - Search posts by tag: click on a tag under post title or via input field in the right column, it also allows multiple comma-separated tags ("java, databases" will show posts that have only both of these tags).
23 | - Users can like (+1) and dislike (-1) posts and comments.
24 | - List of 10 latest posts and TOP 10 posts (by user rating sum) in the right column.
25 |
26 |
27 | #Installation
28 |
29 | Requirements:
30 | - JDK 8.
31 | - Maven.
32 | - Database, such as MySQL.
33 | - Web server, such as Apache Tomcat 8.0 (tested only on Tomcat, probably works on other web servers too, maybe requires minor modifications).
34 |
35 | #
36 |
37 | 1. Obtain the project source files (`git clone` or download and extract zip archive).
38 | 2. Modify configuration if needed.
39 |
40 | Spring profiles are used to apply different configuration files for development (**dev**), real server (**prod**) and tests (**test**).
41 |
42 | * **uploading-<profile>.properties** files (in `src\main\resources` directory) contain directory path for user uploaded files (such as avatar images).
43 |
44 | Example (**uploading-prod.properties**):
45 |
uploading.dirpath=/var/blog/upload/
46 | * **datasource-<profile>.xml** files (in `src\main\resources directory`) contain database configuration: database driver, address, name, username/password, ...
47 |
48 | <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
49 | <property name="driverClassName" value="com.mysql.jdbc.Driver " />
50 | <property name="url" value="jdbc:mysql://localhost:3306/blog " />
51 | <property name="username" value="BlogDbUser " />
52 | <property name="password" value="BlogDbPassword " />
53 | ...
54 | <entry key="hibernate.dialect" value="org.hibernate.dialect.MySQL5Dialect "/>
55 | ...
56 |
57 | The database must contain schema with the specified name (**blog** by default) and user with the specified name/password.
58 |
59 | If you want to use another database instead of MySQL you will need to modify `driverClassName`, `url` and `hibernate.dialect` values (highlighted above) and supply the JDBC driver (such as by adding Maven dependency to pom.xml).
60 |
61 | This line specifies SQL files that will be executed when database is created (by default it fills database with some demo data and creates a table for Spring Security “remember me” feature)
62 |
63 | <entry key="hibernate.hbm2ddl.import_files" value="/security-tables.sql,/dummy-data.sql " />
64 |
65 | Also note this line:
66 | <entry key="hibernate.hbm2ddl.auto" value="create "/>
67 | It will drop and create database tables each time you deploy the project.
68 |
69 | See Hibernate, JDBC and Spring documentation for more information about possible configuration parameters.
70 |
71 | By default the same **datasource-dev.xml** file is used for both dev and prod profiles. If needed, you can create a separate file (datasource-prod.xml) and modify <beans profile="prod">
node in **database.xml** to use it:
72 | <beans profile="prod">
73 | <import resource="classpath:/datasource-prod.xml"/>
74 | </beans>
75 |
76 | 3. Run Maven `verify` goal. This will download all dependencies, run JUnit tests and build WAR file. Check Maven output to see if all tests and build are completed successfully.
77 | 4. Set `-Dspring.profiles.active` system property to specify profile that will be used. If not set the default profile is **dev**.
78 |
79 | For example in `/bin/setenv.sh`:
80 | JAVA_OPTS="$JAVA_OPTS -Dspring.profiles.active=prod "
81 | 5. Deploy the WAR file to Tomcat (or other web server).
82 | 6. Go to `http://your-server-address/blog` (if deployed with default Tomcat settings) to see if it is working.
83 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/controller/CommentController.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.controller;
2 |
3 | import alexp.blog.model.Comment;
4 | import alexp.blog.model.Post;
5 | import alexp.blog.service.*;
6 | import alexp.blog.utils.JsonUtils;
7 | import org.springframework.beans.factory.annotation.Autowired;
8 | import org.springframework.security.access.prepost.PreAuthorize;
9 | import org.springframework.stereotype.Controller;
10 | import org.springframework.ui.ModelMap;
11 | import org.springframework.validation.BindingResult;
12 | import org.springframework.web.bind.annotation.*;
13 |
14 | import javax.validation.Valid;
15 | import java.util.List;
16 |
17 | @Controller
18 | public class CommentController {
19 |
20 | @Autowired
21 | private PostService postService;
22 |
23 | @Autowired
24 | private UserService userService;
25 |
26 | @Autowired
27 | private CommentService commentService;
28 |
29 | @RequestMapping(value = "/posts/{postId}/comments", method = RequestMethod.GET)
30 | public String showComments(@PathVariable("postId") Long postId, ModelMap model) {
31 | Post post = postService.getPost(postId);
32 |
33 | if (post == null)
34 | throw new ResourceNotFoundException();
35 |
36 | if (post.isHidden() && !userService.isAdmin())
37 | throw new ResourceNotFoundException();
38 |
39 | List comments = post.topLevelComments();
40 |
41 | model.addAttribute("comments", comments);
42 | model.addAttribute("post", post);
43 |
44 | return "fragments/comments :: commentList";
45 | }
46 |
47 | @PreAuthorize("hasRole('ROLE_USER')")
48 | @RequestMapping(value = "/posts/{postId}/comments/create", method = RequestMethod.POST)
49 | public @ResponseBody String addComment(@Valid @ModelAttribute(value = "comment") Comment comment, BindingResult result,
50 | @PathVariable("postId") Long postId,
51 | @RequestParam(value = "parentId", defaultValue = "") Long parentId) {
52 | if (result.hasErrors()) {
53 | return makeCommentAddResponse("error", result.getAllErrors().get(0).getDefaultMessage());
54 | }
55 |
56 | Post post = postService.getPost(postId);
57 |
58 | if (post == null)
59 | return makeCommentAddResponse("error", "post not found");
60 |
61 | if (post.isHidden() && !userService.isAdmin())
62 | return makeCommentAddResponse("error", "post not found");
63 |
64 | Long addedId = commentService.saveNewComment(comment, post, parentId);
65 |
66 | return makeCommentAddResponse("ok", addedId);
67 | }
68 |
69 | @PreAuthorize("hasRole('ROLE_USER')")
70 | @RequestMapping(value = "/posts/{postId}/comments/{commentId}/delete", method = RequestMethod.POST)
71 | public @ResponseBody String deleteComment(@PathVariable("postId") Long postId, @PathVariable("commentId") Long commentId) {
72 | try {
73 | commentService.deleteComment(commentId);
74 | } catch (ActionExpiredException e) {
75 | return "expired";
76 | }
77 |
78 | return "ok";
79 | }
80 |
81 | @PreAuthorize("hasRole('ROLE_USER')")
82 | @RequestMapping(value = "/posts/{postId}/comments/{commentId}/edit", method = RequestMethod.POST)
83 | public @ResponseBody String editComment(@Valid @ModelAttribute(value = "comment") Comment comment, BindingResult result,
84 | @PathVariable("postId") Long postId, @PathVariable("commentId") Long commentId) {
85 | if (result.hasErrors()) {
86 | return result.getAllErrors().get(0).getDefaultMessage();
87 | }
88 |
89 | try {
90 | commentService.updateComment(comment, commentId);
91 | } catch (ActionExpiredException e) {
92 | return "expired";
93 | }
94 |
95 | return "ok";
96 | }
97 |
98 | @PreAuthorize("hasRole('ROLE_USER')")
99 | @RequestMapping(value = "/posts/{postId}/comments/{commentId}/source", method = RequestMethod.GET,
100 | produces = "text/plain; charset=utf-8")
101 | public @ResponseBody String getCommentSource(@PathVariable("postId") Long postId, @PathVariable("commentId") Long commentId) {
102 | Comment comment = commentService.getComment(commentId);
103 |
104 | if (comment == null)
105 | throw new ResourceNotFoundException();
106 |
107 | return comment.getCommentText();
108 | }
109 |
110 | @PreAuthorize("hasRole('ROLE_USER')")
111 | @RequestMapping(value = "/posts/{postId}/comments/{commentId}/like", method = RequestMethod.POST)
112 | public @ResponseBody String like(@PathVariable("postId") Long postId, @PathVariable("commentId") Long commentId) {
113 | try {
114 | commentService.vote(commentId, true);
115 | } catch (AlreadyVotedException e) {
116 | return "already_voted";
117 | }
118 | catch (ForbiddenException e) {
119 | return "own_comment";
120 | }
121 |
122 | return "ok";
123 | }
124 |
125 | @PreAuthorize("hasRole('ROLE_USER')")
126 | @RequestMapping(value = "/posts/{postId}/comments/{commentId}/dislike", method = RequestMethod.POST)
127 | public @ResponseBody String dislike(@PathVariable("postId") Long postId, @PathVariable("commentId") Long commentId) {
128 | try {
129 | commentService.vote(commentId, false);
130 | } catch (AlreadyVotedException e) {
131 | return "already_voted";
132 | }
133 | catch (ForbiddenException e) {
134 | return "own_comment";
135 | }
136 |
137 | return "ok";
138 | }
139 |
140 | private String makeCommentAddResponse(String status, String msg, Long id) {
141 | return "{" + JsonUtils.toJsonField("status", status) +
142 | (id == null ? "" : (", " + JsonUtils.toJsonField("id", id.toString()))) +
143 | (msg == null ? "" : (", " + JsonUtils.toJsonField("message", msg))) +
144 | "}";
145 | }
146 |
147 | private String makeCommentAddResponse(String status, Long id) {
148 | return makeCommentAddResponse(status, null, id);
149 | }
150 |
151 | private String makeCommentAddResponse(String status, String msg) {
152 | return makeCommentAddResponse(status, msg, null);
153 | }
154 |
155 | private String makeCommentAddResponse(String status) {
156 | return makeCommentAddResponse(status, null, null);
157 | }
158 |
159 | }
160 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/controller/ExceptionController.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.controller;
2 |
3 | import org.springframework.stereotype.Controller;
4 | import org.springframework.web.bind.annotation.RequestMapping;
5 |
6 | import javax.servlet.http.HttpServletRequest;
7 |
8 | @Controller
9 | public class ExceptionController {
10 |
11 | @RequestMapping("/404")
12 | public String notFoundErrorHandler(HttpServletRequest request, Exception e) {
13 | return "404";
14 | }
15 | @RequestMapping("/error")
16 | public String defaultErrorHandler(HttpServletRequest request, Exception e) {
17 | return "error";
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/controller/ForbiddenException.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.controller;
2 |
3 | import org.springframework.http.HttpStatus;
4 | import org.springframework.web.bind.annotation.ResponseStatus;
5 |
6 | @ResponseStatus(value = HttpStatus.FORBIDDEN)
7 | public class ForbiddenException extends RuntimeException {
8 |
9 | public ForbiddenException(String message) {
10 | super(message);
11 | }
12 | public ForbiddenException() {
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/controller/LoginController.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.controller;
2 |
3 | import alexp.blog.service.UserService;
4 | import org.springframework.beans.factory.annotation.Autowired;
5 | import org.springframework.stereotype.Controller;
6 | import org.springframework.ui.ModelMap;
7 | import org.springframework.web.bind.annotation.RequestMapping;
8 | import org.springframework.web.bind.annotation.RequestMethod;
9 | import org.springframework.web.bind.annotation.ResponseBody;
10 |
11 | import javax.servlet.http.HttpServletRequest;
12 |
13 | @Controller
14 | public class LoginController {
15 |
16 | @Autowired
17 | private UserService userService;
18 |
19 | @RequestMapping(value = "/login_success", method= RequestMethod.GET)
20 | public @ResponseBody String loginSuccess(HttpServletRequest request) {
21 | return "ok";
22 | }
23 |
24 | @RequestMapping(value = "/login_error", method= RequestMethod.GET)
25 | public @ResponseBody String loginError(HttpServletRequest request) {
26 | return "failed";
27 | }
28 |
29 | // POST request must be used for logout if CSRF enabled, so this page contains hidden form to submit via JS
30 | @RequestMapping(value = "/logout", method= RequestMethod.GET)
31 | public String logout() {
32 | if (!userService.isAuthenticated()) {
33 | return "redirect:posts";
34 | }
35 |
36 | return "logout";
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/controller/ResourceNotFoundException.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.controller;
2 |
3 | import org.springframework.http.HttpStatus;
4 | import org.springframework.web.bind.annotation.ResponseStatus;
5 |
6 | @ResponseStatus(value = HttpStatus.NOT_FOUND)
7 | public class ResourceNotFoundException extends RuntimeException {
8 |
9 | }
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/model/Comment.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.model;
2 |
3 | import alexp.blog.service.MarkdownConverter;
4 | import alexp.blog.utils.LocalDateTimePersistenceConverter;
5 | import org.hibernate.validator.constraints.NotBlank;
6 |
7 | import javax.persistence.*;
8 | import java.time.LocalDateTime;
9 | import java.time.ZoneId;
10 | import java.util.ArrayList;
11 | import java.util.List;
12 | import java.util.Optional;
13 |
14 | @Entity
15 | @Table(name = "comments")
16 | public class Comment {
17 |
18 | @javax.persistence.Id
19 | @GeneratedValue
20 | private Long Id;
21 |
22 | @Lob
23 | @Column(nullable = false)
24 | @NotBlank
25 | private String commentText;
26 |
27 | @Column(nullable = false)
28 | @Convert(converter = LocalDateTimePersistenceConverter.class)
29 | private LocalDateTime dateTime;
30 |
31 | @Column(nullable = true)
32 | @Convert(converter = LocalDateTimePersistenceConverter.class)
33 | private LocalDateTime modifiedDateTime;
34 |
35 | @ManyToOne(fetch = FetchType.EAGER)
36 | @JoinColumn(name = "user_id", nullable = false)
37 | private User user;
38 |
39 | @ManyToOne(fetch = FetchType.LAZY)
40 | @JoinColumn(name = "post_id", nullable = false)
41 | private Post post;
42 |
43 | @Column(nullable = false)
44 | private boolean deleted = false;
45 |
46 | // simple (adjacency list) comments hierarchy implementation
47 | // probably not the most performance efficient choice since it will have additional DB queries for each level in each subtree
48 |
49 | @ManyToOne(fetch = FetchType.EAGER)
50 | @JoinColumn(name = "parent_id")
51 | private Comment parentComment;
52 |
53 | @OneToMany(mappedBy="parentComment", cascade = CascadeType.ALL)
54 | @OrderBy("dateTime ASC")
55 | private List childrenComments = new ArrayList<>();
56 |
57 | @OneToMany(cascade = CascadeType.ALL, mappedBy = "comment")
58 |
59 | private List commentRatings = new ArrayList<>();
60 |
61 | public int commentLevel() {
62 | Comment comment = this;
63 | int level = 0;
64 | while ((comment = comment.getParentComment()) != null)
65 | level++;
66 | return level;
67 | }
68 |
69 | public boolean userCanDelete() {
70 | return LocalDateTime.now().isBefore(maxDeleteTime());
71 | }
72 |
73 | public LocalDateTime maxDeleteTime() {
74 | return dateTime.plusMinutes(10);
75 | }
76 |
77 | // should refactor to store dates in UTC in database
78 |
79 | public long maxDeleteTimeUnixTimestamp() {
80 | return maxDeleteTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
81 | }
82 |
83 | public boolean userCanEdit() {
84 | return LocalDateTime.now().isBefore(maxEditTime());
85 | }
86 |
87 | public LocalDateTime maxEditTime() {
88 | return dateTime.plusMinutes(180);
89 | }
90 |
91 | public long maxEditTimeUnixTimestamp() {
92 | return maxEditTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
93 | }
94 |
95 | public int getRatingSum() {
96 | return commentRatings.stream().mapToInt(Rating::getValue).sum();
97 | }
98 |
99 | public short getUserVoteValue(Long userId) {
100 | if (userId == null)
101 | return 0;
102 |
103 | Optional rating = commentRatings.stream().filter(r -> r.getUser().getId().equals(userId)).findFirst();
104 | return rating.isPresent() ? rating.get().getValue() : 0;
105 | }
106 |
107 | public Long getId() {
108 | return Id;
109 | }
110 |
111 | public void setId(Long id) {
112 | Id = id;
113 | }
114 |
115 | public String getCommentText() {
116 | return commentText;
117 | }
118 |
119 | public String getCommentTextHtml() {
120 | return MarkdownConverter.toHtml(getCommentText());
121 | }
122 |
123 | public LocalDateTime getDateTime() {
124 | return dateTime;
125 | }
126 |
127 | public void setDateTime(LocalDateTime dateTime) {
128 | this.dateTime = dateTime;
129 | }
130 |
131 | public LocalDateTime getModifiedDateTime() {
132 | return modifiedDateTime;
133 | }
134 |
135 | public void setModifiedDateTime(LocalDateTime modifiedDateTime) {
136 | this.modifiedDateTime = modifiedDateTime;
137 | }
138 |
139 | public void setCommentText(String commentText) {
140 | this.commentText = commentText;
141 | }
142 |
143 | public User getUser() {
144 | return user;
145 | }
146 |
147 | public void setUser(User user) {
148 | this.user = user;
149 | }
150 |
151 | public Post getPost() {
152 | return post;
153 | }
154 |
155 | public void setPost(Post post) {
156 | this.post = post;
157 | }
158 |
159 | public boolean isDeleted() {
160 | return deleted;
161 | }
162 |
163 | public void setDeleted(boolean deleted) {
164 | this.deleted = deleted;
165 | }
166 |
167 | public Comment getParentComment() {
168 | return parentComment;
169 | }
170 |
171 | public void setParentComment(Comment parentComment) {
172 | this.parentComment = parentComment;
173 | }
174 |
175 | public List getChildrenComments() {
176 | return childrenComments;
177 | }
178 |
179 | public void setChildrenComments(List childrenComments) {
180 | this.childrenComments = childrenComments;
181 | }
182 |
183 | public List getCommentRatings() {
184 | return commentRatings;
185 | }
186 |
187 | public void setCommentRatings(List commentRatings) {
188 | this.commentRatings = commentRatings;
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/model/CommentRating.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.model;
2 |
3 | import javax.persistence.*;
4 |
5 | @Entity
6 | @Table(name = "comment_ratings",
7 | uniqueConstraints = @UniqueConstraint(columnNames = {"comment_id", "user_id"}))
8 | public class CommentRating extends Rating {
9 |
10 | @Id
11 | @GeneratedValue
12 | private Long Id;
13 |
14 | @ManyToOne(fetch = FetchType.LAZY)
15 | @JoinColumn(name = "comment_id", nullable = false)
16 | private Comment comment;
17 |
18 | public CommentRating(User user, short value, Comment comment) {
19 | super(user, value);
20 | this.comment = comment;
21 | }
22 |
23 | public CommentRating() {
24 | }
25 |
26 | public Long getId() {
27 | return Id;
28 | }
29 |
30 | public void setId(Long id) {
31 | Id = id;
32 | }
33 |
34 | public Comment getComment() {
35 | return comment;
36 | }
37 |
38 | public void setComment(Comment comment) {
39 | this.comment = comment;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/model/Post.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.model;
2 |
3 | import alexp.blog.service.MarkdownConverter;
4 | import alexp.blog.utils.LocalDateTimePersistenceConverter;
5 | import org.springframework.util.StringUtils;
6 |
7 | import javax.persistence.*;
8 | import java.time.LocalDateTime;
9 | import java.util.ArrayList;
10 | import java.util.Collection;
11 | import java.util.List;
12 | import java.util.Optional;
13 | import java.util.stream.Collectors;
14 |
15 | @Entity
16 | @Table(name = "posts")
17 | public class Post {
18 |
19 | @Id
20 | @GeneratedValue
21 | private Long Id;
22 |
23 | @Column(length = 250, nullable = false)
24 | private String title;
25 |
26 | @Lob
27 | private String shortTextPart;
28 |
29 | @Lob
30 | @Column(nullable = false)
31 | private String fullPostText;
32 |
33 | @Column(nullable = false)
34 | @Convert(converter = LocalDateTimePersistenceConverter.class)
35 | private LocalDateTime dateTime;
36 |
37 | @Column(nullable = false)
38 | private boolean hidden = false;
39 |
40 | @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE})
41 | @JoinTable(name = "posts_tags",
42 | joinColumns = @JoinColumn(name = "post_id", referencedColumnName = "id"),
43 | inverseJoinColumns = @JoinColumn(name = "tag_id", referencedColumnName = "id"))
44 | @OrderBy("name ASC")
45 | private Collection tags = new ArrayList<>();
46 |
47 | @OneToMany(cascade = CascadeType.ALL, mappedBy = "post")
48 | @org.hibernate.annotations.LazyCollection(org.hibernate.annotations.LazyCollectionOption.EXTRA)
49 | @OrderBy("dateTime ASC")
50 | private List comments = new ArrayList<>();
51 |
52 | @OneToMany(cascade = CascadeType.ALL, mappedBy = "post")
53 | private List postRatings = new ArrayList<>();
54 |
55 | public static String shortPartSeparator() {
56 | return "===cut===";
57 | }
58 |
59 | public boolean hasShortTextPart() {
60 | return !StringUtils.isEmpty(shortTextPart);
61 | }
62 |
63 | public String shortTextPartHtml() {
64 | return MarkdownConverter.toHtml(getShortTextPart());
65 | }
66 |
67 | public String fullPostTextHtml() {
68 | return MarkdownConverter.toHtml(getFullPostText().replace(shortPartSeparator(), ""));
69 | }
70 |
71 | public List topLevelComments() {
72 | return comments.stream().filter(c -> c.getParentComment() == null).collect(Collectors.toList());
73 | }
74 |
75 | public int getRatingSum() {
76 | return postRatings.stream().mapToInt(Rating::getValue).sum();
77 | }
78 |
79 | public short getUserVoteValue(Long userId) {
80 | if (userId == null)
81 | return 0;
82 |
83 | Optional rating = postRatings.stream().filter(r -> r.getUser().getId().equals(userId)).findFirst();
84 | return rating.isPresent() ? rating.get().getValue() : 0;
85 | }
86 |
87 | public Long getId() {
88 | return Id;
89 | }
90 |
91 | public void setId(Long id) {
92 | Id = id;
93 | }
94 |
95 | public String getTitle() {
96 | return title;
97 | }
98 |
99 | public void setTitle(String title) {
100 | this.title = title;
101 | }
102 |
103 | public String getShortTextPart() {
104 | return shortTextPart;
105 | }
106 |
107 | public void setShortTextPart(String shortTextPart) {
108 | this.shortTextPart = shortTextPart;
109 | }
110 |
111 | public String getFullPostText() {
112 | return fullPostText;
113 | }
114 |
115 | public void setFullPostText(String fullPostText) {
116 | this.fullPostText = fullPostText;
117 | }
118 |
119 | public Collection getTags() {
120 | return tags;
121 | }
122 |
123 | public void setTags(Collection tags) {
124 | this.tags = tags;
125 | }
126 |
127 | public LocalDateTime getDateTime() {
128 | return dateTime;
129 | }
130 |
131 | public void setDateTime(LocalDateTime dateTime) {
132 | this.dateTime = dateTime;
133 | }
134 |
135 | public List getComments() {
136 | return comments;
137 | }
138 |
139 | public void setComments(List comments) {
140 | this.comments = comments;
141 | }
142 |
143 | public boolean isHidden() {
144 | return hidden;
145 | }
146 |
147 | public void setHidden(boolean hidden) {
148 | this.hidden = hidden;
149 | }
150 |
151 | public List getPostRatings() {
152 | return postRatings;
153 | }
154 |
155 | public void setPostRatings(List postRatings) {
156 | this.postRatings = postRatings;
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/model/PostEditDto.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.model;
2 |
3 | import org.hibernate.validator.constraints.NotBlank;
4 |
5 | import javax.validation.constraints.Size;
6 |
7 | public class PostEditDto {
8 |
9 | private Long Id = null;
10 |
11 | // haven't figured out how to specify messages for Size.List in the messages file
12 | @Size.List({
13 | @Size(min = 3, message = "Title too short"),
14 | @Size(max = 250, message = "Title too long")
15 | })
16 | @NotBlank
17 | private String title;
18 |
19 | @NotBlank
20 | @Size(min = 50)
21 | private String text;
22 |
23 | @NotBlank
24 | private String tags = "";
25 |
26 | public Long getId() {
27 | return Id;
28 | }
29 |
30 | public void setId(Long id) {
31 | Id = id;
32 | }
33 |
34 | public String getTitle() {
35 | return title;
36 | }
37 |
38 | public void setTitle(String title) {
39 | this.title = title;
40 | }
41 |
42 | public String getText() {
43 | return text;
44 | }
45 |
46 | public void setText(String text) {
47 | this.text = text;
48 | }
49 |
50 | public String getTags() {
51 | return tags;
52 | }
53 |
54 | public void setTags(String tags) {
55 | this.tags = tags;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/model/PostRating.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.model;
2 |
3 | import javax.persistence.*;
4 |
5 | @Entity
6 | @Table(name = "post_ratings",
7 | uniqueConstraints = @UniqueConstraint(columnNames = {"post_id", "user_id"}))
8 | public class PostRating extends Rating {
9 |
10 | @Id
11 | @GeneratedValue
12 | private Long Id;
13 |
14 | @ManyToOne(fetch = FetchType.LAZY)
15 | @JoinColumn(name = "post_id", nullable = false)
16 | private Post post;
17 |
18 | public PostRating(User user, short value, Post post) {
19 | super(user, value);
20 | this.post = post;
21 | }
22 |
23 | public PostRating() {
24 |
25 | }
26 |
27 | public Long getId() {
28 | return Id;
29 | }
30 |
31 | public void setId(Long id) {
32 | Id = id;
33 | }
34 |
35 | public Post getPost() {
36 | return post;
37 | }
38 |
39 | public void setPost(Post post) {
40 | this.post = post;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/model/Rating.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.model;
2 |
3 | import javax.persistence.*;
4 |
5 | @MappedSuperclass
6 | @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
7 | public abstract class Rating {
8 |
9 | public static final short LIKE_VALUE = 1;
10 | public static final short DISLIKE_VALUE = -1;
11 |
12 | @ManyToOne(fetch = FetchType.LAZY)
13 | @JoinColumn(name = "user_id", nullable = false)
14 | protected User user;
15 |
16 | protected short value;
17 |
18 | public Rating(User user, short value) {
19 | this.user = user;
20 | this.value = value;
21 | }
22 |
23 | public Rating() {
24 |
25 | }
26 |
27 | public User getUser() {
28 | return user;
29 | }
30 |
31 | public void setUser(User user) {
32 | this.user = user;
33 | }
34 |
35 | public short getValue() {
36 | return value;
37 | }
38 |
39 | public void setValue(short value) {
40 | this.value = value;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/model/Role.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.model;
2 |
3 | import javax.persistence.*;
4 | import java.util.ArrayList;
5 | import java.util.Collection;
6 |
7 | @Entity
8 | @Table(name = "roles")
9 | public class Role {
10 | @Id
11 | @GeneratedValue
12 | private Long Id;
13 |
14 | @Column(unique = true, nullable = false, length = 50)
15 | private String name;
16 |
17 | @ManyToMany(mappedBy = "roles")
18 | private Collection users = new ArrayList<>();;
19 |
20 | public Long getId() {
21 | return Id;
22 | }
23 |
24 | public void setId(Long id) {
25 | Id = id;
26 | }
27 |
28 | public String getName() {
29 | return name;
30 | }
31 |
32 | public void setName(String name) {
33 | this.name = name;
34 | }
35 |
36 | public Collection getUsers() {
37 | return users;
38 | }
39 |
40 | public void setUsers(Collection users) {
41 | this.users = users;
42 | }
43 |
44 | @Override
45 | public String toString() {
46 | return "Role{" +
47 | "name='" + name + '\'' +
48 | '}';
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/model/Tag.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.model;
2 |
3 | import javax.persistence.*;
4 | import java.util.ArrayList;
5 | import java.util.Collection;
6 |
7 | @Entity
8 | @Table(name = "tags")
9 | public class Tag {
10 |
11 | @Id
12 | @GeneratedValue
13 | private Long Id;
14 |
15 | @Column(length = 30, nullable = false, unique = true)
16 | private String name;
17 |
18 | @ManyToMany(mappedBy = "tags")
19 | private Collection posts = new ArrayList<>();
20 |
21 | public Tag() {
22 | }
23 |
24 | public Tag(String name) {
25 | this.name = name;
26 | }
27 |
28 | public Long getId() {
29 | return Id;
30 | }
31 |
32 | public void setId(Long id) {
33 | Id = id;
34 | }
35 |
36 | public String getName() {
37 | return name;
38 | }
39 |
40 | public void setName(String name) {
41 | this.name = name;
42 | }
43 |
44 | public Collection getPosts() {
45 | return posts;
46 | }
47 |
48 | public void setPosts(Collection posts) {
49 | this.posts = posts;
50 | }
51 |
52 | @Override
53 | public String toString() {
54 | return "Tag{" +
55 | "name='" + name + '\'' +
56 | '}';
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/repository/CommentRatingRepository.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.repository;
2 |
3 | import alexp.blog.model.CommentRating;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 | import org.springframework.data.jpa.repository.Query;
6 | import org.springframework.data.repository.query.Param;
7 |
8 | public interface CommentRatingRepository extends JpaRepository {
9 |
10 | @Query("SELECT r FROM CommentRating r WHERE r.comment.id = :commentId AND r.user.id = :userId")
11 | CommentRating findUserRating(@Param("commentId") Long commentId, @Param("userId") Long userId);
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/repository/CommentRepository.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.repository;
2 |
3 | import alexp.blog.model.Comment;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 |
6 | public interface CommentRepository extends JpaRepository {
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/repository/PostRatingRepository.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.repository;
2 |
3 | import alexp.blog.model.PostRating;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 | import org.springframework.data.jpa.repository.Query;
6 | import org.springframework.data.repository.query.Param;
7 |
8 | public interface PostRatingRepository extends JpaRepository {
9 |
10 | @Query("SELECT r FROM PostRating r WHERE r.post.id = :postId AND r.user.id = :userId")
11 | PostRating findUserRating(@Param("postId") Long postId, @Param("userId") Long userId);
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/repository/PostRepository.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.repository;
2 |
3 | import alexp.blog.model.Post;
4 | import org.springframework.data.domain.Page;
5 | import org.springframework.data.domain.Pageable;
6 | import org.springframework.data.jpa.repository.JpaRepository;
7 | import org.springframework.data.jpa.repository.Query;
8 | import org.springframework.data.repository.query.Param;
9 | import java.util.Collection;
10 | import java.util.List;
11 |
12 | public interface PostRepository extends JpaRepository {
13 |
14 | Page findByHiddenFalse(Pageable pageable);
15 |
16 | // without count
17 | List findByHiddenIs(boolean hidden, Pageable pageable);
18 |
19 | @Query("SELECT p FROM Post p WHERE :tagCount = (SELECT COUNT(DISTINCT t.id) FROM Post p2 JOIN p2.tags t WHERE LOWER(t.name) in (:tags) and p = p2)")
20 | Page findByTags(@Param("tags") Collection tags, @Param("tagCount") Long tagCount, Pageable pageable);
21 |
22 | @Query("SELECT p FROM Post p WHERE :tagCount = (SELECT COUNT(DISTINCT t.id) FROM Post p2 JOIN p2.tags t WHERE p.hidden = false and LOWER(t.name) in (:tags) and p = p2)")
23 | Page findByTagsAndNotHidden(@Param("tags") Collection tags, @Param("tagCount") Long tagCount, Pageable pageable);
24 |
25 | @Query("SELECT p FROM Post p JOIN p.postRatings r WHERE p.hidden = false GROUP BY p ORDER BY SUM(r.value) DESC")
26 | List findTopPosts(Pageable pageable);
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/repository/RoleRepository.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.repository;
2 |
3 | import alexp.blog.model.Role;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 |
6 | public interface RoleRepository extends JpaRepository {
7 |
8 | Role findByName(String name);
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/repository/TagRepository.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.repository;
2 |
3 | import alexp.blog.model.Tag;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 |
6 | public interface TagRepository extends JpaRepository {
7 |
8 | Tag findByNameIgnoreCase(String name);
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/repository/UserRepository.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.repository;
2 |
3 | import alexp.blog.model.User;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 |
6 | public interface UserRepository extends JpaRepository {
7 |
8 | User findByUsernameIgnoreCase(String username);
9 |
10 | User findByEmailIgnoreCase(String email);
11 |
12 | User findByUsernameOrEmail(String username, String email);
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/service/ActionExpiredException.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.service;
2 |
3 | public class ActionExpiredException extends Exception {
4 |
5 | public ActionExpiredException(String message) {
6 | super(message);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/service/AlreadyVotedException.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.service;
2 |
3 | public class AlreadyVotedException extends Exception {
4 |
5 | public AlreadyVotedException(String message) {
6 | super(message);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/service/AuthException.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.service;
2 |
3 | public class AuthException extends Exception {
4 |
5 | public AuthException(String message) {
6 | super(message);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/service/AvatarService.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.service;
2 |
3 | import org.springframework.web.multipart.MultipartFile;
4 |
5 | import java.io.IOException;
6 |
7 | public interface AvatarService {
8 |
9 | UploadedAvatarInfo upload(MultipartFile file) throws IOException, UnsupportedFormatException;
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/service/AvatarServiceImpl.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.service;
2 |
3 | import org.apache.commons.io.FilenameUtils;
4 | import org.imgscalr.Scalr;
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import org.springframework.beans.factory.annotation.Value;
7 | import org.springframework.stereotype.Service;
8 | import org.springframework.web.multipart.MultipartFile;
9 |
10 | import javax.imageio.ImageIO;
11 | import java.awt.image.BufferedImage;
12 | import java.io.*;
13 | import java.lang.reflect.Array;
14 | import java.util.Arrays;
15 | import java.util.List;
16 |
17 | @Service("avatarService")
18 | public class AvatarServiceImpl implements AvatarService {
19 |
20 | @Value("${uploading.dirpath}")
21 | private String uploadingDirPath;
22 |
23 | @Autowired
24 | private FileNameGenerator fileNameGenerator;
25 |
26 | public AvatarServiceImpl() {
27 | ImageIO.setUseCache(false);
28 | }
29 |
30 | public final List SUPPORTED_EXTENSIONS = Arrays.asList("jpg", "jpeg", "png");
31 |
32 | @Override
33 | public UploadedAvatarInfo upload(MultipartFile file) throws IOException, UnsupportedFormatException {
34 | String fileName = file.getOriginalFilename();
35 | String ext = FilenameUtils.getExtension(fileName).toLowerCase();
36 |
37 | if (!SUPPORTED_EXTENSIONS.contains(ext))
38 | throw new UnsupportedFormatException(fileName);
39 |
40 | String name = fileNameGenerator.getFileName(fileName, "avatar");
41 | String bigImageName = name + "_big" + "." + ext;
42 | String smallImageName = name + "_small" + "." + ext;
43 |
44 | BufferedImage image = ImageIO.read(file.getInputStream());
45 |
46 | BufferedImage bigImage = resize(image, 160);
47 |
48 | new File(uploadingDirPath).mkdirs();
49 |
50 | ImageIO.write(bigImage, ext, new File(uploadingDirPath + bigImageName));
51 |
52 | BufferedImage smallImage = resize(image, 28);
53 |
54 | ImageIO.write(smallImage, ext, new File(uploadingDirPath + smallImageName));
55 |
56 | return new UploadedAvatarInfo(bigImageName, smallImageName);
57 | }
58 |
59 | private BufferedImage resize(BufferedImage image, int size) {
60 | BufferedImage result = Scalr.resize(image, Scalr.Mode.FIT_EXACT, size, size);
61 |
62 | // would be better to crop image if not square, instead of just resizing without preserving proportions
63 |
64 | return result;
65 | }
66 |
67 | public FileNameGenerator getFileNameGenerator() {
68 | return fileNameGenerator;
69 | }
70 |
71 | public void setFileNameGenerator(FileNameGenerator fileNameGenerator) {
72 | this.fileNameGenerator = fileNameGenerator;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/service/CommentService.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.service;
2 |
3 | import alexp.blog.controller.ForbiddenException;
4 | import alexp.blog.model.Comment;
5 | import alexp.blog.model.Post;
6 |
7 | import java.util.List;
8 |
9 | public interface CommentService {
10 |
11 | Comment getComment(Long id);
12 |
13 | Long saveNewComment(Comment comment, Post post, Long parentId);
14 |
15 | void deleteComment(Long commentId) throws ActionExpiredException;
16 |
17 | void updateComment(Comment newCommentData, Long commentId) throws ActionExpiredException;
18 |
19 | void vote(Long commentId, boolean like) throws AlreadyVotedException, ForbiddenException;
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/service/CommentServiceImpl.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.service;
2 |
3 | import alexp.blog.controller.ForbiddenException;
4 | import alexp.blog.model.*;
5 | import alexp.blog.repository.CommentRatingRepository;
6 | import alexp.blog.repository.CommentRepository;
7 | import org.springframework.beans.factory.annotation.Autowired;
8 | import org.springframework.stereotype.Service;
9 |
10 | import java.time.LocalDateTime;
11 |
12 | @Service("commentService")
13 | public class CommentServiceImpl implements CommentService {
14 |
15 | @Autowired
16 | private UserService userService;
17 |
18 | @Autowired
19 | private CommentRepository commentRepository;
20 |
21 | @Autowired
22 | private CommentRatingRepository commentRatingRepository;
23 |
24 | private static final int MAX_COMMENT_LEVEL = 5;
25 |
26 | @Override
27 | public Comment getComment(Long id) {
28 | return commentRepository.findOne(id);
29 | }
30 |
31 | @Override
32 | public Long saveNewComment(Comment comment, Post post, Long parentId) {
33 | if (parentId != null) {
34 | Comment parentComment = getComment(parentId);
35 |
36 | int level = parentComment.commentLevel();
37 |
38 | comment.setParentComment(level < MAX_COMMENT_LEVEL ? parentComment : parentComment.getParentComment());
39 | }
40 |
41 | comment.setDateTime(LocalDateTime.now());
42 |
43 | comment.setPost(post);
44 |
45 | comment.setUser(userService.currentUser());
46 |
47 | commentRepository.saveAndFlush(comment);
48 |
49 | return comment.getId();
50 | }
51 |
52 | @Override
53 | public void deleteComment(Long commentId) throws ActionExpiredException {
54 | Comment comment = getComment(commentId);
55 |
56 | boolean isAdmin = userService.isAdmin();
57 |
58 | if (!isAdmin && !userService.currentUser().getUsername().equals(comment.getUser().getUsername())) {
59 | throw new ForbiddenException();
60 | }
61 |
62 | if (!isAdmin && !comment.userCanDelete()) {
63 | throw new ActionExpiredException("delete time exceeded");
64 | }
65 |
66 | comment.setDeleted(true);
67 |
68 | commentRepository.saveAndFlush(comment);
69 | }
70 |
71 | @Override
72 | public void updateComment(Comment newCommentData, Long commentId) throws ActionExpiredException {
73 | Comment comment = getComment(commentId);
74 |
75 | boolean isAdmin = userService.isAdmin();
76 |
77 | if (!isAdmin && !userService.currentUser().getUsername().equals(comment.getUser().getUsername())) {
78 | throw new ForbiddenException();
79 | }
80 |
81 | if (!isAdmin && !comment.userCanEdit()) {
82 | throw new ActionExpiredException("edit time exceeded");
83 | }
84 |
85 | comment.setCommentText(newCommentData.getCommentText());
86 |
87 | comment.setModifiedDateTime(LocalDateTime.now());
88 |
89 | commentRepository.saveAndFlush(comment);
90 | }
91 |
92 | @Override
93 | public void vote(Long commentId, boolean like) throws AlreadyVotedException, ForbiddenException {
94 | User currentUser = userService.currentUser();
95 |
96 | Comment comment = getComment(commentId);
97 |
98 | if (currentUser.getId().longValue() == comment.getUser().getId().longValue()) {
99 | throw new ForbiddenException("cannot vote for own comments");
100 | }
101 |
102 | CommentRating rating = commentRatingRepository.findUserRating(commentId, currentUser.getId());
103 |
104 | if (rating != null) {
105 | throw new AlreadyVotedException("cannot vote more than once");
106 | }
107 |
108 | rating = new CommentRating();
109 |
110 | rating.setUser(currentUser);
111 | rating.setValue(like ? Rating.LIKE_VALUE : Rating.DISLIKE_VALUE);
112 | rating.setComment(comment);
113 |
114 | commentRatingRepository.saveAndFlush(rating);
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/service/FileNameGenerator.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.service;
2 |
3 | public interface FileNameGenerator {
4 |
5 | String getFileName(String filename, String prefix);
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/service/FileNameGeneratorImpl.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.service;
2 |
3 | import org.apache.commons.io.FilenameUtils;
4 | import org.springframework.stereotype.Service;
5 |
6 | @Service("fileNameGenerator")
7 | public class FileNameGeneratorImpl implements FileNameGenerator {
8 | @Override
9 | public String getFileName(String filename, String prefix) {
10 | return prefix + filename.hashCode() + System.currentTimeMillis();
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/service/MarkdownConverter.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.service;
2 |
3 | import com.github.rjeschke.txtmark.Processor;
4 | import org.jsoup.Jsoup;
5 | import org.jsoup.safety.Whitelist;
6 |
7 | import java.util.ArrayList;
8 | import java.util.List;
9 | import java.util.regex.Matcher;
10 | import java.util.regex.Pattern;
11 |
12 | public class MarkdownConverter {
13 |
14 | public static String toHtml(String input) {
15 | String html = Processor.process(input, true);
16 |
17 | String safeHtml = Jsoup.clean(html, Whitelist.basicWithImages());
18 |
19 | return safeHtml;
20 | }
21 |
22 | public static List extractLinks(String input) {
23 | Matcher m = Pattern.compile("\\s?\\s?\\[([^^\\]]+)\\]:\\s+(.+)$", Pattern.MULTILINE)
24 | .matcher(input);
25 |
26 | List links = new ArrayList<>();
27 | while (m.find()) {
28 | links.add(m.group());
29 | }
30 |
31 | return links;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/service/PostService.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.service;
2 |
3 | import alexp.blog.model.Post;
4 | import alexp.blog.model.PostEditDto;
5 | import org.springframework.data.domain.Page;
6 |
7 | import java.util.List;
8 |
9 | public interface PostService {
10 |
11 | Page getPostsPage(int pageNumber, int pageSize);
12 |
13 | List getPostsList(int pageNumber, int pageSize);
14 |
15 | List getTopPostsList();
16 |
17 | Post getPost(Long id);
18 |
19 | PostEditDto getEditablePost(Long id);
20 |
21 | Page findPostsByTag(List tags, int pageNumber, int pageSize);
22 |
23 | Post saveNewPost(PostEditDto postEditDto);
24 |
25 | Post updatePost(PostEditDto postEditDto);
26 |
27 | void setPostVisibility(Long postId, boolean hide);
28 |
29 | void deletePost(Long postId);
30 |
31 | void vote(Long postId, boolean like) throws AlreadyVotedException;
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/service/UnsupportedFormatException.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.service;
2 |
3 | public class UnsupportedFormatException extends Exception {
4 |
5 | public UnsupportedFormatException(String message) {
6 | super(message);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/service/UploadedAvatarInfo.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.service;
2 |
3 | public class UploadedAvatarInfo {
4 | public final String bigImageLink;
5 |
6 | public final String smallImageLink;
7 |
8 | public UploadedAvatarInfo(String bigImageLink, String smallImageLink) {
9 | this.bigImageLink = bigImageLink;
10 | this.smallImageLink = smallImageLink;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/service/UserService.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.service;
2 |
3 | import alexp.blog.model.User;
4 | import org.springframework.security.core.userdetails.UserDetailsService;
5 |
6 | public interface UserService extends UserDetailsService {
7 |
8 | User findByEmail(String email);
9 |
10 | User findByUsername(String username);
11 |
12 | boolean emailExists(String email);
13 |
14 | boolean usernameExists(String username);
15 |
16 | void register(User user);
17 |
18 | void changeEmail(String newEmail, String currentPassword) throws AuthException;
19 |
20 | void changePassword(String newPassword, String currentPassword) throws AuthException;
21 |
22 | void changeProfileInfo(User newProfileInfo);
23 |
24 | void changeAvatar(UploadedAvatarInfo uploadedAvatarInfo);
25 |
26 | void removeAvatar();
27 |
28 | void authenticate(User user);
29 |
30 | boolean isAuthenticated();
31 |
32 | boolean isAdmin();
33 |
34 | User currentUser();
35 |
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/utils/JsonUtils.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.utils;
2 |
3 | public class JsonUtils {
4 |
5 | public static String toJsonField(String name, String value) {
6 | return "\"" + name + "\":\"" + value + "\"";
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/utils/LocalDatePersistenceConverter.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.utils;
2 |
3 | import java.time.LocalDate;
4 | import javax.persistence.AttributeConverter;
5 | import javax.persistence.Converter;
6 |
7 | @Converter
8 | public class LocalDatePersistenceConverter implements AttributeConverter {
9 |
10 | @Override
11 | public java.sql.Date convertToDatabaseColumn(LocalDate entityValue) {
12 | if (entityValue != null) {
13 | return java.sql.Date.valueOf(entityValue);
14 | }
15 | return null;
16 | }
17 |
18 | @Override
19 | public LocalDate convertToEntityAttribute(java.sql.Date databaseValue) {
20 | if (databaseValue != null) {
21 | return databaseValue.toLocalDate();
22 | }
23 | return null;
24 | }
25 | }
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/utils/LocalDateTimePersistenceConverter.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.utils;
2 |
3 | import java.time.LocalDateTime;
4 | import javax.persistence.AttributeConverter;
5 | import javax.persistence.Converter;
6 |
7 | @Converter
8 | public class LocalDateTimePersistenceConverter implements AttributeConverter {
9 |
10 | @Override
11 | public java.sql.Timestamp convertToDatabaseColumn(LocalDateTime entityValue) {
12 | if (entityValue != null) {
13 | return java.sql.Timestamp.valueOf(entityValue);
14 | }
15 | return null;
16 | }
17 |
18 | @Override
19 | public LocalDateTime convertToEntityAttribute(java.sql.Timestamp databaseValue) {
20 | if (databaseValue != null) {
21 | return databaseValue.toLocalDateTime();
22 | }
23 | return null;
24 | }
25 | }
--------------------------------------------------------------------------------
/src/main/java/alexp/blog/validator/UserValidator.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.validator;
2 |
3 | import alexp.blog.model.User;
4 | import alexp.blog.service.UserService;
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import org.springframework.stereotype.Component;
7 | import org.springframework.util.StringUtils;
8 | import org.springframework.validation.Errors;
9 | import org.springframework.validation.Validator;
10 |
11 | @Component("userValidator")
12 | public class UserValidator implements Validator {
13 |
14 | @Autowired
15 | UserService userService;
16 |
17 | @Override
18 | public boolean supports(Class> aClass) {
19 | return User.class.equals(aClass);
20 | }
21 |
22 | @Override
23 | public void validate(Object o, Errors errors) {
24 | User user = (User) o;
25 |
26 | if (!StringUtils.isEmpty(user.getUsername())) {
27 | if (userService.usernameExists(user.getUsername())) {
28 | errors.rejectValue("username", "Registered");
29 | }
30 | }
31 |
32 | if (!StringUtils.isEmpty(user.getEmail())) {
33 | User currentUser = userService.currentUser();
34 |
35 | if (currentUser == null || !currentUser.getEmail().equalsIgnoreCase(user.getEmail())) {
36 | if (userService.emailExists(user.getEmail())) {
37 | errors.rejectValue("email", "Registered");
38 | }
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/main/resources/META-INF/persistence.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/main/resources/Messages.properties:
--------------------------------------------------------------------------------
1 | NotBlank.user.email=Enter e-mail
2 | Email.user.email=Enter valid e-mail
3 | NotBlank.user.username=Enter username
4 | Pattern.user.username=Only letters, numbers, space, ".", "-" and "_" are allowed
5 | Registered.user.username=Username already registered. Have you already registered? Contact admin if you forgot your password
6 | Registered.user.email=Email already registered. Have you already registered? Contact admin if you forgot your password
7 | NotBlank.user.password=Enter password
8 | NotMatchCurrent.user.password=Current password does not match
9 | Pattern.user.websiteLink=Invalid website link format
10 | Size.user.websiteLink=Too long website link
11 | Pattern.user.aboutMe=Too long text, max 1000 characters
12 |
13 | NotBlank.post.title=Enter title
14 | NotBlank.post.text=Text cannot be empty
15 | NotBlank.post.tags=Add at least one tag
16 | Size.post.text=Too short text
17 |
18 | NotBlank.comment.commentText=Empty comments not allowed
--------------------------------------------------------------------------------
/src/main/resources/database.xml:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/main/resources/datasource-dev.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/main/resources/datasource-test.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/main/resources/security-tables.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null);
2 |
--------------------------------------------------------------------------------
/src/main/resources/uploading-dev.properties:
--------------------------------------------------------------------------------
1 | uploading.dirpath=c:/blog/upload/
--------------------------------------------------------------------------------
/src/main/resources/uploading-prod.properties:
--------------------------------------------------------------------------------
1 | uploading.dirpath=/var/blog/upload/
--------------------------------------------------------------------------------
/src/main/resources/uploading-test.properties:
--------------------------------------------------------------------------------
1 | uploading.dirpath=c:/blog/upload-test/
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/mvc-dispatcher-servlet.xml:
--------------------------------------------------------------------------------
1 |
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 |
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 |
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/spring-security.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/templates/403.html:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 | Blog — Access denied
7 |
8 |
9 |
10 |
11 |
12 | Access denied
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/templates/404.html:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 | Blog — Page not found
7 |
8 |
9 |
10 |
11 |
12 |
13 |
33 |
34 |
35 |
36 |
37 |
Page not found
38 |
Maybe it have been removed or never existed.
39 |
40 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/templates/about.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 | Blog — About
11 |
12 |
13 |
14 |
15 |
16 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/templates/editpost.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 | Blog — Create post
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/templates/editprofile.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 | Blog — Edit profile
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
35 |
36 |
37 |
38 |
Changes successfully saved
39 |
40 |
41 |
Public info
42 |
43 |
44 |
45 |
46 | Input is incorrect
47 |
48 |
49 |
50 |
56 |
57 |
58 | Website:
59 |
60 |
61 |
62 | Apply
63 |
64 |
65 |
66 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | Avatar successfully saved
99 |
100 |
101 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/templates/error.html:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 | Blog — Error
7 |
8 |
9 |
10 |
33 |
34 |
35 |
36 |
37 |
OOPS! Something went wrong.
38 |
39 |
Please try again later.
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/templates/fragments/comments.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/templates/fragments/loginform.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
18 |
19 |
Log in
20 |
21 |
22 |
23 | Username or E-mail:
24 |
25 |
26 |
27 | Password:
28 |
29 |
30 |
31 | Remember me
32 |
33 | Log in
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | or Register
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/templates/layouts/blog.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | Blog
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 |
48 |
49 |
50 |
51 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | Logged as
77 |
78 |
81 |
82 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 | Search by tag
117 |
118 |
119 |
120 | Search
121 |
122 |
123 |
124 |
125 |
Latest posts
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
Popular posts
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
149 |
150 |
151 |
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/templates/logout.html:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 | Blog — logout
7 |
8 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | Performing logout...
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/templates/profile.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/templates/registration.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 | Blog — Registration
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/templates/settings.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 | Blog — Edit settings
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
Email successfully changed
23 |
Password successfully changed
24 |
25 |
26 |
27 |
Change email
28 |
29 |
30 |
31 |
32 | Input is incorrect
33 |
34 |
35 |
36 |
37 | Current password:
38 |
39 |
40 |
41 |
42 | Email:
43 |
44 |
45 |
46 | Apply
47 |
48 |
49 |
50 |
51 |
52 |
Change password
53 |
78 |
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/src/main/webapp/WEB-INF/web.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 | Blog
7 |
8 |
9 | mvc-dispatcher
10 | org.springframework.web.servlet.DispatcherServlet
11 | 1
12 |
13 |
14 |
15 | mvc-dispatcher
16 | /
17 |
18 |
19 |
20 | org.springframework.web.context.ContextLoaderListener
21 |
22 |
23 |
24 | spring.profiles.default
25 | dev
26 |
27 |
28 |
29 | contextConfigLocation
30 |
31 | classpath:/database.xml,
32 | /WEB-INF/spring-security.xml
33 |
34 |
35 |
36 |
37 | 404
38 | /404
39 |
40 |
41 | 403
42 | /403
43 |
44 |
45 | /error
46 |
47 |
48 |
49 | encodingFilter
50 | org.springframework.web.filter.CharacterEncodingFilter
51 |
52 | encoding
53 | UTF-8
54 |
55 |
56 | forceEncoding
57 | true
58 |
59 |
60 |
61 | encodingFilter
62 | /*
63 |
64 |
65 |
66 |
67 |
68 | springSecurityFilterChain
69 | org.springframework.web.filter.DelegatingFilterProxy
70 |
71 |
72 |
73 | springSecurityFilterChain
74 | /*
75 |
76 |
77 |
78 |
79 |
80 | openEntityManagerInViewFilter
81 | org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter
82 |
83 |
84 |
85 | openEntityManagerInViewFilter
86 | /*
87 |
88 |
--------------------------------------------------------------------------------
/src/main/webapp/css/blog.css:
--------------------------------------------------------------------------------
1 | .page-header {
2 | padding-bottom: 10px;
3 | }
4 |
5 | .header-description {
6 | color: #999;
7 | }
8 |
9 | .page-footer {
10 | text-align: center;
11 | padding: 40px 0;
12 | margin-top: 25px;
13 | background-color: #f9f9f9;
14 | border-top: 1px solid #e5e5e5;
15 | }
16 |
17 | .col-padding {
18 | padding-left: 10px;
19 | padding-right: 10px;
20 | }
21 |
22 | .no-padding-col {
23 | padding-left: 0;
24 | padding-right: 0;
25 | }
26 |
27 | .reg-link {
28 | margin-top: 10px;
29 | white-space: nowrap;
30 | }
31 |
32 | .dotted-link {
33 | border-bottom: 1px dotted #337ab7;
34 | }
35 |
36 | .error-line {
37 | color: red;
38 | }
39 |
40 | .list-no-indent {
41 | list-style-position: inside;
42 | padding-left:0;
43 | }
44 |
45 | label.error {
46 | color: red;
47 | margin-top: 5px;
48 | display: inline;
49 | }
50 |
51 | .user-menu {
52 | padding-top: 25px;
53 | }
54 |
55 | .user-menu-item {
56 | margin-bottom: 15px;
57 | }
58 |
59 | .modal {
60 | margin-top: 25%;
61 | }
62 |
63 | .post {
64 | margin-bottom: 50px;
65 | }
66 |
67 | .post-content {
68 | position: relative;
69 | }
70 |
71 | .post-title {
72 |
73 | }
74 |
75 | .post-date {
76 | color: #999;
77 |
78 | }
79 |
80 | .post-tag {
81 | color: #3e6d8e;
82 | background-color: #e4edf4;
83 | border: 1px solid #e4edf4;
84 | white-space: nowrap;
85 | text-align: center;
86 | text-decoration: none;
87 | padding: 3px 10px;
88 | margin: 2px 2px 2px 0;
89 | }
90 |
91 | .post-maincontent {
92 | margin-top: 15px;
93 | }
94 |
95 | .hidden-post {
96 | background-color: rgba(0,0,0,0.5);
97 | position: absolute;
98 | top: 0;
99 | left: 0;
100 | bottom: 0;
101 | right: 0;
102 | text-align: center;
103 | }
104 |
105 | .hidden-post span {
106 | display: inline-block;
107 | font-size: 30px;
108 | font-weight: bold;
109 | color: lightblue;
110 | padding-top: 10px;
111 | padding-bottom: 10px;
112 | }
113 |
114 | .pager {
115 | text-align: left;
116 | }
117 |
118 | .post-actions {
119 | margin-top: 10px;
120 | }
121 |
122 | .post-actions a {
123 | margin-right: 10px;
124 | font-size: 13px;
125 | }
126 |
127 | .post-actions a:not(.comments-link):not(.comment-reply-btn) {
128 | color: #888;
129 | }
130 |
131 | .comments {
132 | }
133 |
134 | .avatar {
135 | margin-right: 10px;
136 | }
137 |
138 | .avatar img {
139 | width: 28px;
140 | height: 28px;
141 | }
142 |
143 | .comment-username {
144 | font-weight: bold;
145 | color: #369;
146 | margin-right: 10px;
147 | vertical-align: middle;
148 | }
149 |
150 | .comment-username-admin {
151 | color: darkred;
152 | }
153 |
154 | .comment-header .post-date {
155 | vertical-align: middle;
156 | }
157 |
158 | .comment-header {
159 | margin-bottom: 10px;
160 | }
161 |
162 | .comment {
163 | padding-bottom: 25px;
164 | }
165 |
166 | .new-comment .comment-header {
167 | background-color: #E3FFD0;
168 | }
169 |
170 | .child-comment-node {
171 | margin-left: 20px;
172 | }
173 |
174 | .comment-content.deleted {
175 | display: inline-block;
176 | background-color: #F0F0F0;
177 | padding: 10px;
178 | }
179 |
180 | .deleted {
181 | color: #808080;
182 | }
183 |
184 | .comment-form button {
185 | margin-right: 10px;
186 | }
187 |
188 | .post-maincontent img {
189 | max-width: 100%;
190 | height: auto;
191 | }
192 |
193 | .settings-success {
194 | padding-top: 15px;
195 | padding-bottom: 15px;
196 | margin-bottom: 15px;
197 | text-align: center;
198 | background-color: #F4FCEE;
199 | border: 2px solid #91E458;
200 | font-weight: bold;
201 | }
202 |
203 | .avatar-big {
204 | margin-top: 15px;
205 | margin-bottom: 15px;
206 | }
207 |
208 | .profile-info h3 {
209 | margin-top: 0;
210 | }
211 |
212 | .website-link a {
213 | word-wrap: break-word;
214 | }
215 |
216 | .reg-date {
217 | margin-top: 15px;
218 | color: #999;
219 | }
220 |
221 | .comments .wmd-input {
222 | height: 160px;
223 | }
224 |
225 | #profileForm .wmd-input {
226 | height: 160px;
227 | }
228 |
229 | .search-header {
230 | margin-bottom: 30px;
231 | }
232 |
233 | .search-header h3 {
234 | display: inline;
235 | }
236 |
237 | .search-result {
238 | margin-top: 10px;
239 | }
240 |
241 | #latestPosts {
242 | margin-top: 30px;
243 | }
244 |
245 | #avatarForm {
246 | margin-top: 25px;
247 | margin-bottom: 15px;
248 | }
249 |
250 | #removeAvatar {
251 | margin-left: 15px;
252 | }
253 |
254 | #avatarUploadProgress {
255 | margin-top: 15px;
256 | }
257 |
258 | .rating-arrow, .rating-arrow:focus, .rating-arrow:hover {
259 | text-decoration: none;
260 | }
261 |
262 | .rating-arrow span {
263 | font-size: 18px;
264 | color: #888888;
265 | cursor: pointer;
266 | }
267 |
268 | .like-arrow:hover span, .like-arrow.voted span {
269 | color: rgb(25, 189, 83);
270 | }
271 |
272 | .dislike-arrow:hover span, .dislike-arrow.voted span {
273 | color: red;
274 | }
275 |
276 | .rating-value {
277 | font-weight: bold;
278 | font-size: 15px;
279 | margin-left: 5px;
280 | margin-right: 5px;
281 | }
282 |
283 | .rating-value-no {
284 | color: #888;
285 | }
286 |
287 | .rating-value-positive {
288 | color: rgb(25, 189, 83);
289 | }
290 |
291 | .rating-value-negative {
292 | color: red;
293 | }
294 |
295 | .post-rating-block {
296 | display: inline-block;
297 | margin-right: 10px;
298 | }
299 |
300 | .post-rating-block a {
301 | margin-right: 0;
302 | }
303 |
304 | .comment-rating-block {
305 | display: inline-block;
306 | float: right;
307 | }
--------------------------------------------------------------------------------
/src/main/webapp/css/external/fileupload.css:
--------------------------------------------------------------------------------
1 | .fileinput-button {
2 | position: relative;
3 | overflow: hidden;
4 | }
5 | .fileinput-button input {
6 | position: absolute;
7 | top: 0;
8 | right: 0;
9 | margin: 0;
10 | opacity: 0;
11 | -ms-filter: 'alpha(opacity=0)';
12 | font-size: 200px;
13 | direction: ltr;
14 | cursor: pointer;
15 | }
16 |
17 | /* Fixes for IE < 8 */
18 | @media screen\9 {
19 | .fileinput-button input {
20 | filter: alpha(opacity=0);
21 | font-size: 100%;
22 | height: 100%;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/webapp/css/external/pagedown.css:
--------------------------------------------------------------------------------
1 | .wmd-panel {
2 | min-width: 500px;
3 | padding-top: 5px;
4 | }
5 |
6 | .wmd-button-bar {
7 | width: 100%;
8 | background-color: Silver;
9 | }
10 |
11 | .wmd-input {
12 | height: 250px;
13 | width: 100%;
14 | border: 1px solid DarkGray;
15 | }
16 |
17 | .wmd-preview {
18 | border: 1px solid #ccc;
19 | min-height: 15px;
20 | margin-bottom: 15px;
21 | }
22 |
23 | .wmd-button-row {
24 | position: relative;
25 | margin-left: 5px;
26 | margin-right: 5px;
27 | margin-bottom: 5px;
28 | margin-top: 10px;
29 | padding: 0;
30 | height: 20px;
31 | }
32 |
33 | .wmd-spacer {
34 | width: 1px;
35 | height: 20px;
36 | margin-left: 14px;
37 |
38 | position: absolute;
39 | background-color: Silver;
40 | display: inline-block;
41 | list-style: none;
42 | }
43 |
44 | .wmd-button {
45 | width: 20px;
46 | height: 20px;
47 | padding-left: 2px;
48 | padding-right: 3px;
49 | position: absolute;
50 | display: inline-block;
51 | list-style: none;
52 | cursor: pointer;
53 | }
54 |
55 | .wmd-button > span {
56 | background-image: url(../../js/wmd-buttons.png);
57 | background-repeat: no-repeat;
58 | background-position: 0 0;
59 | width: 20px;
60 | height: 20px;
61 | display: inline-block;
62 | }
63 |
64 | .wmd-spacer1 {
65 | left: 50px;
66 | }
67 | .wmd-spacer2
68 | {
69 | left: 175px;
70 | }
71 | .wmd-spacer3
72 | {
73 | left: 300px;
74 | }
75 |
76 | .wmd-prompt-background {
77 | background-color: Black;
78 | }
79 |
80 | .wmd-prompt-dialog {
81 | border: 1px solid #999999;
82 | background-color: #F5F5F5;
83 | }
84 |
85 | .wmd-prompt-dialog > div {
86 | font-size: 0.8em;
87 | font-family: arial, helvetica, sans-serif;
88 | }
89 |
90 |
91 | .wmd-prompt-dialog > form > input[type="text"] {
92 | border: 1px solid #999999;
93 | color: black;
94 | }
95 |
96 | .wmd-prompt-dialog > form > input[type="button"]{
97 | border: 1px solid #888888;
98 | font-family: trebuchet MS, helvetica, sans-serif;
99 | font-size: 0.8em;
100 | font-weight: bold;
101 | }
102 |
103 | blockquote {
104 | border-left: 2px dotted #888;
105 | padding-left: 5px;
106 | background: #d0f0ff;
107 | }
--------------------------------------------------------------------------------
/src/main/webapp/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexP11223/JavaSpringMvcBlog/b4edb967d0da1c62f5367f447220471ff5628e38/src/main/webapp/favicon.ico
--------------------------------------------------------------------------------
/src/main/webapp/images/404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexP11223/JavaSpringMvcBlog/b4edb967d0da1c62f5367f447220471ff5628e38/src/main/webapp/images/404.png
--------------------------------------------------------------------------------
/src/main/webapp/images/ajax-loader.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexP11223/JavaSpringMvcBlog/b4edb967d0da1c62f5367f447220471ff5628e38/src/main/webapp/images/ajax-loader.gif
--------------------------------------------------------------------------------
/src/main/webapp/images/cat-working.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexP11223/JavaSpringMvcBlog/b4edb967d0da1c62f5367f447220471ff5628e38/src/main/webapp/images/cat-working.png
--------------------------------------------------------------------------------
/src/main/webapp/images/no-avatar-big.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexP11223/JavaSpringMvcBlog/b4edb967d0da1c62f5367f447220471ff5628e38/src/main/webapp/images/no-avatar-big.png
--------------------------------------------------------------------------------
/src/main/webapp/images/no-avatar-small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexP11223/JavaSpringMvcBlog/b4edb967d0da1c62f5367f447220471ff5628e38/src/main/webapp/images/no-avatar-small.png
--------------------------------------------------------------------------------
/src/main/webapp/images/success-tick.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexP11223/JavaSpringMvcBlog/b4edb967d0da1c62f5367f447220471ff5628e38/src/main/webapp/images/success-tick.png
--------------------------------------------------------------------------------
/src/main/webapp/js/admin.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function() {
2 | var postsContainer = $("#postsContainer");
3 |
4 | postsContainer.on('click', 'a[data-action="hidePost"]', function(event){
5 | event.preventDefault();
6 |
7 | var postTitle = getPostTitle(this);
8 |
9 | var btn = $(this);
10 |
11 | var loadingIndicator = btn.closest('.post').find('.postaction-loading-indicator');
12 |
13 | var delBtn = btn.closest('.post-actions').find('a[data-action="deletePost"]');
14 |
15 | bootbox.dialog({
16 | title: 'Hide post',
17 | message: 'Are you sure you want to hide post ' + postTitle + ' from other users?',
18 | buttons: {
19 | cancel: {
20 | label: 'Cancel'
21 | },
22 | main: {
23 | label: 'Hide',
24 | className: 'btn-primary',
25 | callback: function() {
26 | loadingIndicator.show();
27 |
28 | $.ajax({
29 | type: 'post',
30 | url: btn.attr('data-href'),
31 | success: function (data) {
32 | loadingIndicator.hide();
33 |
34 | if (data == 'ok') {
35 | btn.attr('data-action', 'unhidePost');
36 | btn.attr('data-href', btn.attr('data-href').replace('hide', 'unhide'));
37 | btn.html('unhide');
38 |
39 | btn.closest('.post').find('.post-content').append('Not visible for users
');
40 |
41 | delBtn.show();
42 | }
43 | else {
44 | showErrorDialog('Error: ' + data + '. Try reloading page.');
45 | }
46 | },
47 | error: function () {
48 | loadingIndicator.hide();
49 |
50 | showErrorDialog('Failed to send request. Try reloading page.');
51 | }
52 | });
53 | }
54 | }
55 | }
56 | });
57 | });
58 |
59 | postsContainer.on('click', 'a[data-action="unhidePost"]', function(event){
60 | event.preventDefault();
61 |
62 | var btn = $(this);
63 |
64 | var loadingIndicator = btn.closest('.post').find('.postaction-loading-indicator');
65 |
66 | var delBtn = btn.closest('.post-actions').find('a[data-action="deletePost"]');
67 |
68 | loadingIndicator.show();
69 |
70 | $.ajax({
71 | type: 'post',
72 | url: btn.attr('data-href'),
73 | success: function (data) {
74 | loadingIndicator.hide();
75 |
76 | if (data == 'ok') {
77 | btn.attr('data-action', 'hidePost');
78 | btn.attr('data-href', btn.attr('data-href').replace('unhide', 'hide'));
79 | btn.html('hide');
80 |
81 | btn.closest('.post').find('.hidden-post').remove();
82 |
83 | delBtn.hide();
84 | }
85 | else {
86 | showErrorDialog('Error: ' + data + '. Try reloading page.');
87 | }
88 | },
89 | error: function () {
90 | loadingIndicator.hide();
91 |
92 | showErrorDialog('Failed to send request. Try reloading page.');
93 | }
94 | });
95 | });
96 |
97 | postsContainer.on('click', 'a[data-action="deletePost"]', function(event){
98 | event.preventDefault();
99 |
100 | var postTitle = getPostTitle(this);
101 | var postId = getPostId(this);
102 |
103 | var btn = $(this);
104 |
105 | var loadingIndicator = btn.closest('.post').find('.postaction-loading-indicator');
106 |
107 | bootbox.dialog({
108 | title: 'Delete post',
109 | message: 'Are you sure you want to delete post ' + postTitle + ' ? You will not be able to recover it.',
110 | buttons: {
111 | cancel: {
112 | label: 'Cancel'
113 | },
114 | main: {
115 | label: 'Delete',
116 | className: 'btn-danger',
117 | callback: function() {
118 | loadingIndicator.show();
119 |
120 | $.ajax({
121 | type: 'post',
122 | url: btn.attr('data-href'),
123 | success: function (data) {
124 | loadingIndicator.hide();
125 |
126 | if (data == 'ok') {
127 | btn.closest('.post').remove();
128 |
129 | if (window.location.href.indexOf('posts/' + postId) > -1) {
130 | window.location.href = window.location.href.replace('/' + postId, '');
131 | }
132 | }
133 | else {
134 | showErrorDialog('Error: ' + data + '. Try reloading page.');
135 | }
136 | },
137 | error: function () {
138 | loadingIndicator.hide();
139 |
140 | showErrorDialog('Failed to send request. Try reloading page.');
141 | }
142 | });
143 | }
144 | }
145 | }
146 | });
147 | });
148 | });
149 |
150 |
--------------------------------------------------------------------------------
/src/main/webapp/js/common.js:
--------------------------------------------------------------------------------
1 | function getPostId(el) {
2 | return $(el).closest('.post').attr('data-post-id');
3 | }
4 |
5 | function getPostTitle(el) {
6 | return $(el).closest('.post').attr('data-post-title');
7 | }
8 |
9 | function showErrorDialog(text) {
10 | bootbox.dialog({
11 | message: text,
12 | buttons: {
13 | ok: {
14 | label: 'OK'
15 | }
16 | }
17 | });
18 | }
19 |
20 | function scrollToViewIfNotVisible(element){
21 | var offset = element.offset().top;
22 | if(!element.is(":visible")) {
23 | element.css({"visiblity":"hidden"}).show();
24 | offset = element.offset().top;
25 | element.css({"visiblity":"", "display":""});
26 | }
27 |
28 | var visible_area_start = $(window).scrollTop();
29 | var visible_area_end = visible_area_start + window.innerHeight;
30 |
31 | if(offset < visible_area_start || offset > visible_area_end){
32 | $('html,body').animate({scrollTop: offset - window.innerHeight/3}, 300);
33 | return false;
34 | }
35 | return true;
36 | }
37 |
38 | $(document).ready(function() {
39 | var token = $("meta[name='_csrf']").attr("content");
40 | var header = $("meta[name='_csrf_header']").attr("content");
41 | $(document).ajaxSend(function(e, xhr, options) {
42 | xhr.setRequestHeader(header, token);
43 | });
44 |
45 |
46 | $.ajax({
47 | dataType: "json",
48 | url: window.postsUrl,
49 | success: function (data) {
50 | var items = [];
51 | $.each(data, function(key, val) {
52 | items.push('' + val.title + ' ');
53 | });
54 | $("", {
55 | class: 'list-no-indent',
56 | html: items.join('')
57 | }).appendTo('#latestPosts div');
58 | }
59 | });
60 |
61 | $.ajax({
62 | dataType: "json",
63 | url: window.popularPostsUrl,
64 | success: function (data) {
65 | var items = [];
66 | $.each(data, function(key, val) {
67 | items.push('' + val.title + ' ');
68 | });
69 | $("", {
70 | class: 'list-no-indent',
71 | html: items.join('')
72 | }).appendTo('#popularPosts div');
73 | }
74 | });
75 | });
--------------------------------------------------------------------------------
/src/main/webapp/js/editpost.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function() {
2 | var converter = Markdown.getSanitizingConverter();
3 |
4 | converter.hooks.chain("preConversion", function (text) {
5 | return text.replace(/===cut===/g, '');
6 | });
7 |
8 | var editor = new Markdown.Editor(converter);
9 |
10 | editor.run();
11 |
12 | $("#wmd-button-row").append(
13 | 'short/full text separator ');
14 |
15 | jQuery.fn.extend({
16 | insertAtCaret: function(myValue){
17 | return this.each(function(i) {
18 | if (document.selection) {
19 | this.focus();
20 | var sel = document.selection.createRange();
21 | sel.text = myValue;
22 | this.focus();
23 | }
24 | else if (this.selectionStart || this.selectionStart == '0') {
25 | var startPos = this.selectionStart;
26 | var endPos = this.selectionEnd;
27 | var scrollTop = this.scrollTop;
28 | this.value = this.value.substring(0, startPos)+myValue+this.value.substring(endPos,this.value.length);
29 | this.focus();
30 | this.selectionStart = startPos + myValue.length;
31 | this.selectionEnd = startPos + myValue.length;
32 | this.scrollTop = scrollTop;
33 | } else {
34 | this.value += myValue;
35 | this.focus();
36 | }
37 | });
38 | }
39 | });
40 |
41 | $('#insertSeparator').click(function(){
42 | var $textarea = $('#wmd-input');
43 | $textarea.insertAtCaret('===cut===');
44 | });
45 |
46 | $("#postForm").validate({
47 | rules: {
48 | title: {
49 | required: true,
50 | minlength: 3
51 | },
52 | tags: {
53 | required: true
54 | },
55 | text: {
56 | required: true,
57 | minlength: 50
58 | }
59 | }
60 | });
61 |
62 | $(window).bind('beforeunload', function(){
63 | if ($.trim($('#wmd-input').val()) != '')
64 | return 'You have not submitted your post.';
65 | });
66 |
67 | $(document).on("submit", "form", function(event){
68 | $(window).off('beforeunload');
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/src/main/webapp/js/editprofile.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function() {
2 | var converter = Markdown.getSanitizingConverter();
3 |
4 | var editor = new Markdown.Editor(converter);
5 |
6 | editor.run();
7 |
8 | $("#profileForm").validate({
9 | rules: {
10 | websiteLink: {
11 | url: true
12 | },
13 | aboutText: {
14 | maxlength: 1000
15 | }
16 | }
17 | });
18 |
19 | var uploadBtn = $('#uploadAvatar');
20 | var removeBtn = $('#removeAvatar');
21 | var pb = $('#avatarUploadProgress').find('.progress-bar');
22 | var avatarImg = $('#avatarImg');
23 | var avatarErrorLabel = $('#avatarError');
24 | var avatarSuccessLabel = $('#avatarSuccess');
25 |
26 | $('#avatarFileUploadInput').fileupload({
27 | url: window.avatarUploadUrl,
28 | dataType: "json",
29 | send: function (e, data) {
30 | pb.css('width', '0');
31 | pb.switchClass('progress-bar-danger', 'progress-bar-success', 0);
32 | pb.parent().show();
33 |
34 | avatarErrorLabel.hide();
35 | avatarSuccessLabel.hide();
36 |
37 | uploadBtn.addClass('disabled');
38 | removeBtn.addClass('disabled');
39 | },
40 | done: function (e, data) {
41 | if (data.result.status == 'ok') {
42 | avatarImg.attr('src', window.imgBaseUrl + data.result.link);
43 |
44 | removeBtn.show();
45 |
46 | avatarSuccessLabel.show();
47 | }
48 | else {
49 | pb.switchClass('progress-bar-success', 'progress-bar-danger');
50 |
51 | var errMsg = 'Failed to upload, ' + data.result.status;
52 |
53 | if (data.result.status == 'invalid_format') {
54 | errMsg = 'Only JPG and PNG allowed.';
55 | }
56 |
57 | avatarErrorLabel.text(errMsg);
58 | avatarErrorLabel.show();
59 | }
60 | },
61 | fail: function (e, data) {
62 | pb.switchClass('progress-bar-success', 'progress-bar-danger');
63 |
64 | avatarErrorLabel.text('Failed to upload picture. Check that it is PNG or JPG and not exceeds 1 MB');
65 | avatarErrorLabel.show();
66 | },
67 | always: function (e, data) {
68 | uploadBtn.removeClass('disabled');
69 | removeBtn.removeClass('disabled');
70 | },
71 | progressall: function (e, data) {
72 | var progress = parseInt(data.loaded / data.total * 100, 10);
73 | pb.css('width', progress + '%');
74 | }
75 | }).prop('disabled', !$.support.fileInput)
76 | .parent().addClass($.support.fileInput ? undefined : 'disabled');
77 |
78 | removeBtn.click(function() {
79 | pb.parent().hide();
80 |
81 | avatarErrorLabel.hide();
82 | avatarSuccessLabel.hide();
83 |
84 | uploadBtn.addClass('disabled');
85 | removeBtn.addClass('disabled');
86 |
87 | var loadingIndicator = $('.loading-indicator');
88 | loadingIndicator.show();
89 |
90 | $.ajax({
91 | type: 'post',
92 | url: removeBtn.attr('data-href'),
93 | success: function (data) {
94 | if (data == 'ok') {
95 | avatarImg.attr('src', window.noAvatarImgUrl);
96 |
97 | removeBtn.hide();
98 | }
99 | else {
100 | avatarErrorLabel.text('Error: ' + data + '. Try reloading page.');
101 | }
102 | },
103 | error: function () {
104 | avatarErrorLabel.text('Failed to send request. Try reloading page.');
105 | },
106 | complete: function() {
107 | loadingIndicator.hide();
108 |
109 | uploadBtn.removeClass('disabled');
110 | removeBtn.removeClass('disabled');
111 | }
112 | });
113 | });
114 | });
115 |
--------------------------------------------------------------------------------
/src/main/webapp/js/external/markdown/Markdown.Sanitizer.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | var output, Converter;
3 | if (typeof exports === "object" && typeof require === "function") { // we're in a CommonJS (e.g. Node.js) module
4 | output = exports;
5 | Converter = require("./Markdown.Converter").Converter;
6 | } else {
7 | output = window.Markdown;
8 | Converter = output.Converter;
9 | }
10 |
11 | output.getSanitizingConverter = function () {
12 | var converter = new Converter();
13 | converter.hooks.chain("postConversion", sanitizeHtml);
14 | converter.hooks.chain("postConversion", balanceTags);
15 | return converter;
16 | }
17 |
18 | function sanitizeHtml(html) {
19 | return html.replace(/<[^>]*>?/gi, sanitizeTag);
20 | }
21 |
22 | // (tags that can be opened/closed) | (tags that stand alone)
23 | var basic_tag_whitelist = /^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|kbd|li|ol(?: start="\d+")?|p|pre|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i;
24 | // |
25 | var a_white = /^(]+")?\s?>|<\/a>)$/i;
26 |
27 | // ]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i;
29 |
30 | function sanitizeTag(tag) {
31 | if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white))
32 | return tag;
33 | else
34 | return "";
35 | }
36 |
37 | ///
38 | /// attempt to balance HTML tags in the html string
39 | /// by removing any unmatched opening or closing tags
40 | /// IMPORTANT: we *assume* HTML has *already* been
41 | /// sanitized and is safe/sane before balancing!
42 | ///
43 | /// adapted from CODESNIPPET: A8591DBA-D1D3-11DE-947C-BA5556D89593
44 | ///
45 | function balanceTags(html) {
46 |
47 | if (html == "")
48 | return "";
49 |
50 | var re = /<\/?\w+[^>]*(\s|$|>)/g;
51 | // convert everything to lower case; this makes
52 | // our case insensitive comparisons easier
53 | var tags = html.toLowerCase().match(re);
54 |
55 | // no HTML tags present? nothing to do; exit now
56 | var tagcount = (tags || []).length;
57 | if (tagcount == 0)
58 | return html;
59 |
60 | var tagname, tag;
61 | var ignoredtags = "
";
62 | var match;
63 | var tagpaired = [];
64 | var tagremove = [];
65 | var needsRemoval = false;
66 |
67 | // loop through matched tags in forward order
68 | for (var ctag = 0; ctag < tagcount; ctag++) {
69 | tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1");
70 | // skip any already paired tags
71 | // and skip tags in our ignore list; assume they're self-closed
72 | if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1)
73 | continue;
74 |
75 | tag = tags[ctag];
76 | match = -1;
77 |
78 | if (!/^<\//.test(tag)) {
79 | // this is an opening tag
80 | // search forwards (next tags), look for closing tags
81 | for (var ntag = ctag + 1; ntag < tagcount; ntag++) {
82 | if (!tagpaired[ntag] && tags[ntag] == "" + tagname + ">") {
83 | match = ntag;
84 | break;
85 | }
86 | }
87 | }
88 |
89 | if (match == -1)
90 | needsRemoval = tagremove[ctag] = true; // mark for removal
91 | else
92 | tagpaired[match] = true; // mark paired
93 | }
94 |
95 | if (!needsRemoval)
96 | return html;
97 |
98 | // delete all orphaned tags from the string
99 |
100 | var ctag = 0;
101 | html = html.replace(re, function (match) {
102 | var res = tagremove[ctag] ? "" : match;
103 | ctag++;
104 | return res;
105 | });
106 | return html;
107 | }
108 | })();
109 |
--------------------------------------------------------------------------------
/src/main/webapp/js/login.js:
--------------------------------------------------------------------------------
1 | function setupLoginForm(id) {
2 | $(document).ready(function() {
3 | var $toggler = $('#' + 'loginToggler' + id);
4 | var $form = $('#' + 'loginForm' + id);
5 | var $loginErrorLabel = $('#' + 'loginError' + id);
6 | var $loginBtn = $('#' + 'loginbtn' + id);
7 | var $loadingIndicator = $('#' + 'loadingIndicator' + id);
8 |
9 | $form.validate({
10 | rules: {
11 | username: {
12 | required: true
13 | },
14 | password: {
15 | required: true
16 | }
17 | },
18 | messages: {
19 | username: {
20 | required: "Enter username or e-mail"
21 | },
22 | password: {
23 | required: "Enter password"
24 | }
25 | }
26 | });
27 |
28 | // show/hide login form
29 | $toggler.click(function(event){
30 | event.preventDefault();
31 |
32 | $form.slideToggle(300, function() {
33 | if ($form.is(":visible"))
34 | $('#username' + id).focus();
35 | });
36 | });
37 |
38 | // ajax submit login form
39 | $form.on('submit', function(e) {
40 | e.preventDefault();
41 | var data = $form.serializeArray();
42 |
43 | $loginErrorLabel.hide();
44 |
45 | if ($form.valid()) {
46 | $loginBtn.prop('disabled', true);
47 | $loadingIndicator.show();
48 |
49 | $.ajax({
50 | type: 'post',
51 | url: $form.attr('action'),
52 | data: data,
53 | success: function (data) {
54 | if (data == 'ok') {
55 | // reload page
56 | window.location.reload(true);
57 | }
58 | else {
59 | $loginBtn.prop('disabled', false);
60 | $loadingIndicator.hide();
61 | $loginErrorLabel.text('Failed to log in. Check username/e-mail and password.');
62 | $loginErrorLabel.show();
63 | }
64 | },
65 | error: function () {
66 | $loginBtn.prop('disabled', true);
67 | $loadingIndicator.hide();
68 | $loginErrorLabel.text('Failed to send request.');
69 | $loginErrorLabel.show();
70 | }
71 | });
72 | }
73 | });
74 | });
75 | }
--------------------------------------------------------------------------------
/src/main/webapp/js/registration.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | $(document).ready(function() {
4 | $.validator.methods._remote = $.validator.methods.remote;
5 | var timer = 0;
6 | $.validator.methods.remote = function () {
7 | clearTimeout(timer);
8 |
9 | var args = arguments;
10 |
11 | timer = setTimeout(function() {
12 | $.validator.methods._remote.apply(this, args);
13 | }.bind(this), 500);
14 |
15 | return "pending";
16 | };
17 |
18 | $.validator.addMethod("username", function(value, element) {
19 | return this.optional( element ) || XRegExp("^[\\p{L}0-9\\._\\- ]+$").test( value );
20 | }, 'Only letters, numbers, space, ".", "-" and "_" are allowed.');
21 |
22 | $("#regForm").validate({
23 | rules: {
24 | username: {
25 | required: true,
26 | minlength: 3,
27 | username: true,
28 | remote: {
29 | url: window.usernameCheckUrl,
30 | type: "get",
31 | data: {
32 | username: function () {
33 | return $("#username").val();
34 | }
35 | }
36 | }
37 | },
38 | email: {
39 | required: true,
40 | remote: {
41 | url: window.emailCheckUrl,
42 | type: "get",
43 | data: {
44 | email: function () {
45 | return $("#email").val();
46 | }
47 | }
48 | }
49 | },
50 | password: {
51 | required: true,
52 | minlength: 6
53 | },
54 | password2: {
55 | required: true,
56 | equalTo: "#password"
57 | }
58 | },
59 | messages: {
60 | username: {
61 | required: "Enter username",
62 | minlength: "Too short username",
63 | remote: function (params) {
64 | return "Username " + params + " already registered. Have you already registered? Contact admin if you forgot your password";
65 | }
66 | },
67 | email: {
68 | required: "Enter e-mail",
69 | remote: function (params) {
70 | return "Email " + params + " already registered. Have you already registered? Contact admin if you forgot your password";
71 | }
72 | },
73 | password: {
74 | required: "Enter password",
75 | minlength: "Password too short"
76 | },
77 | password2: {
78 | required: "Enter password",
79 | equalTo: "Passwords do not match"
80 | }
81 | }
82 | });
83 | });
84 |
--------------------------------------------------------------------------------
/src/main/webapp/js/settings.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function() {
2 | $("#emailForm").validate({
3 | rules: {
4 | email: {
5 | required: true,
6 | email: true
7 | },
8 | currentPassword: {
9 | required: true
10 | }
11 | },
12 | messages: {
13 | email: {
14 | required: "Enter e-mail"
15 | },
16 | currentPassword: {
17 | required: "Enter password"
18 | }
19 | }
20 | });
21 |
22 | $("#passwordForm").validate({
23 | rules: {
24 | currentPassword: {
25 | required: true
26 | },
27 | password: {
28 | required: true,
29 | minlength: 6
30 | },
31 | password2: {
32 | required: true,
33 | equalTo: "#password"
34 | }
35 | },
36 | messages: {
37 | password: {
38 | required: "Enter password",
39 | minlength: "Password too short"
40 | },
41 | password2: {
42 | required: "Enter password",
43 | equalTo: "Passwords do not match"
44 | },
45 | currentPassword: {
46 | required: "Enter password"
47 | }
48 | }
49 | });
50 | });
51 |
--------------------------------------------------------------------------------
/src/main/webapp/js/voting.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function() {
2 | var container = $("#pageContent");
3 |
4 | container.on('click', 'a[data-action="vote"]', function(event){
5 | event.preventDefault();
6 |
7 | var btn = $(this);
8 |
9 | btn.addClass('voted');
10 |
11 | var voteType = btn.attr('data-vote-type');
12 |
13 | $.ajax({
14 | type: 'post',
15 | url: btn.attr('data-href'),
16 | success: function (data) {
17 | if (data == 'ok') {
18 | btn.parent().find('a[data-action="vote"]').attr('data-action', '');
19 |
20 | var valElement = btn.siblings('.rating-value');
21 |
22 | var val = parseInt(valElement.html());
23 |
24 | if (voteType == 'like')
25 | val++;
26 | else
27 | val--;
28 |
29 | valElement.html(val);
30 |
31 | if (val == 0 || val == 1 || val == -1) {
32 | valElement.removeClass('rating-value-no rating-value-negative rating-value-positive');
33 | valElement.addClass(val == 0 ? 'rating-value-no' : (val == -1 ? 'rating-value-negative' : 'rating-value-positive'));
34 | }
35 | }
36 | else {
37 | btn.removeClass('voted');
38 |
39 | if (data == 'own_comment') {
40 | showErrorDialog('Cannot vote for your own comments!');
41 | }
42 | else {
43 | showErrorDialog('Voting failed. Try reloading page.');
44 | }
45 | }
46 | },
47 | error: function () {
48 | btn.removeClass('voted');
49 | showErrorDialog('Voting failed. Try reloading page.');
50 | }
51 | });
52 | });
53 | });
54 |
55 |
--------------------------------------------------------------------------------
/src/main/webapp/js/wmd-buttons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AlexP11223/JavaSpringMvcBlog/b4edb967d0da1c62f5367f447220471ff5628e38/src/main/webapp/js/wmd-buttons.png
--------------------------------------------------------------------------------
/src/test/java/alexp/blog/AbstractIntegrationTest.java:
--------------------------------------------------------------------------------
1 | package alexp.blog;
2 |
3 | import com.github.springtestdbunit.TransactionDbUnitTestExecutionListener;
4 | import org.junit.Before;
5 | import org.junit.runner.RunWith;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.security.web.FilterChainProxy;
8 | import org.springframework.test.context.ActiveProfiles;
9 | import org.springframework.test.context.ContextConfiguration;
10 | import org.springframework.test.context.TestExecutionListeners;
11 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
12 | import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
13 | import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
14 | import org.springframework.test.context.web.WebAppConfiguration;
15 | import org.springframework.test.web.servlet.MockMvc;
16 | import org.springframework.web.context.WebApplicationContext;
17 | import javax.transaction.Transactional;
18 |
19 | import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
20 |
21 | @RunWith(SpringJUnit4ClassRunner.class)
22 | @WebAppConfiguration
23 | @ContextConfiguration({ "file:src/main/webapp/WEB-INF/mvc-dispatcher-servlet.xml",
24 | "classpath:/database.xml",
25 | "file:src/main/webapp/WEB-INF/spring-security.xml" })
26 | @ActiveProfiles("test")
27 | @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
28 | DirtiesContextTestExecutionListener.class,
29 | TransactionDbUnitTestExecutionListener.class})
30 | @Transactional
31 | public abstract class AbstractIntegrationTest {
32 |
33 | @SuppressWarnings("SpringJavaAutowiringInspection")
34 | @Autowired
35 | protected FilterChainProxy springSecurityFilterChain;
36 |
37 | protected MockMvc mockMvc;
38 |
39 | @SuppressWarnings("SpringJavaAutowiringInspection")
40 | @Autowired
41 | protected WebApplicationContext wac;
42 |
43 | @Before
44 | public void setup() {
45 | this.mockMvc = webAppContextSetup(this.wac).addFilter(springSecurityFilterChain).build();
46 |
47 | setupTest();
48 | }
49 |
50 | // child classes should override this instead of using @Before (order of @Before is not guaranteed if names are not unique)
51 | protected void setupTest()
52 | {
53 |
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/test/java/alexp/blog/controller/LoginControllerIT.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.controller;
2 |
3 | import alexp.blog.AbstractIntegrationTest;
4 | import com.github.springtestdbunit.annotation.DatabaseSetup;
5 | import com.github.springtestdbunit.annotation.ExpectedDatabase;
6 | import org.junit.Test;
7 | import org.springframework.http.MediaType;
8 |
9 | import static alexp.blog.utils.SecurityUtils.*;
10 | import static org.hamcrest.Matchers.*;
11 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
12 | import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.*;
13 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
14 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
15 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
16 |
17 | @DatabaseSetup("data.xml")
18 | public class LoginControllerIT extends AbstractIntegrationTest {
19 |
20 | @Test
21 | @ExpectedDatabase("data.xml")
22 | public void shouldNotAuthenticateWithInvalidCredentials() throws Exception {
23 | mockMvc.perform(post("/login_check").with(csrf())
24 | .contentType(MediaType.APPLICATION_FORM_URLENCODED)
25 | .param("username", "Bob")
26 | .param("password", "invalid_password"))
27 | .andExpect(status().isFound())
28 | .andExpect(unauthenticated())
29 | .andExpect(redirectedUrl("/login_error"));
30 | }
31 |
32 | @Test
33 | @ExpectedDatabase("data.xml")
34 | public void shouldAuthenticateByUsername() throws Exception {
35 | mockMvc.perform(post("/login_check").with(csrf())
36 | .contentType(MediaType.APPLICATION_FORM_URLENCODED)
37 | .param("username", "Bob")
38 | .param("password", "pass123"))
39 | .andExpect(status().isFound())
40 | .andExpect(authenticated().withUsername("Bob"))
41 | .andExpect(redirectedUrl("/login_success"));
42 | }
43 |
44 | @Test
45 | @ExpectedDatabase("data.xml")
46 | public void shouldAuthenticateByEmail() throws Exception {
47 | mockMvc.perform(post("/login_check").with(csrf())
48 | .contentType(MediaType.APPLICATION_FORM_URLENCODED)
49 | .param("username", "bob@gmail.com")
50 | .param("password", "pass123"))
51 | .andExpect(status().isFound())
52 | .andExpect(authenticated().withUsername("Bob"))
53 | .andExpect(redirectedUrl("/login_success"));
54 | }
55 |
56 | @Test
57 | @ExpectedDatabase("data.xml")
58 | public void shouldLogout() throws Exception {
59 | mockMvc.perform(post("/dologout").with(csrf()).with(userBob()))
60 | .andExpect(status().isFound())
61 | .andExpect(unauthenticated());
62 | }
63 |
64 | }
--------------------------------------------------------------------------------
/src/test/java/alexp/blog/controller/UserAvatarUploadIT.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.controller;
2 |
3 | import alexp.blog.AbstractIntegrationTest;
4 | import alexp.blog.service.AvatarServiceImpl;
5 | import alexp.blog.service.FileNameGenerator;
6 | import com.github.springtestdbunit.annotation.DatabaseSetup;
7 | import com.github.springtestdbunit.annotation.ExpectedDatabase;
8 | import org.apache.commons.io.FileUtils;
9 | import org.aspectj.lang.annotation.After;
10 | import org.junit.Test;
11 | import org.mockito.*;
12 | import org.springframework.beans.factory.annotation.Autowired;
13 | import org.springframework.beans.factory.annotation.Value;
14 | import org.springframework.http.MediaType;
15 | import org.springframework.mock.web.MockMultipartFile;
16 |
17 | import java.io.File;
18 | import java.io.IOException;
19 |
20 | import static alexp.blog.utils.SecurityUtils.userBob;
21 | import static org.hamcrest.MatcherAssert.assertThat;
22 | import static org.mockito.Mockito.when;
23 | import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
24 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.fileUpload;
25 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
26 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
27 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
28 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
29 | import static org.hamcrest.Matchers.*;
30 |
31 | @DatabaseSetup("data.xml")
32 | public class UserAvatarUploadIT extends AbstractIntegrationTest {
33 |
34 | @Autowired
35 | private AvatarServiceImpl avatarService;
36 |
37 | @Value("${uploading.dirpath}")
38 | private String uploadingDirPath;
39 |
40 | @Override
41 | protected void setupTest() {
42 | FileNameGenerator fileNameGenerator = Mockito.mock(FileNameGenerator.class);
43 | avatarService.setFileNameGenerator(fileNameGenerator);
44 |
45 | when(fileNameGenerator.getFileName(Matchers.anyString(), Matchers.anyString())).thenReturn("generatedName");
46 | }
47 |
48 | @org.junit.After
49 | public void teardown() throws IOException {
50 | FileUtils.deleteDirectory(new File(uploadingDirPath));
51 | }
52 |
53 | @Test
54 | @ExpectedDatabase("data-avatar-added.xml")
55 | public void shouldUploadAvatar() throws Exception {
56 | MockMultipartFile img = new MockMultipartFile("avatarFile", "img.jpg", "image/jpeg", this.getClass().getResourceAsStream("img.jpg"));
57 |
58 | mockMvc.perform(fileUpload("/upload_avatar").file(img).with(userBob()).with(csrf().asHeader()))
59 | .andExpect(status().isOk())
60 | .andExpect(jsonPath("$.status", is("ok")))
61 | .andExpect(jsonPath("$.link", is("generatedName_big.jpg")));
62 |
63 | assertThat(new File(uploadingDirPath + "generatedName_big.jpg").exists(), is(equalTo(true)));
64 | assertThat(new File(uploadingDirPath + "generatedName_small.jpg").exists(), is(equalTo(true)));
65 | }
66 |
67 | @Test
68 | @ExpectedDatabase("data.xml")
69 | public void shouldNotUploadNotImage() throws Exception {
70 | MockMultipartFile img = new MockMultipartFile("avatarFile", "notimg.exe", "image/jpeg", "something".getBytes());
71 |
72 | mockMvc.perform(fileUpload("/upload_avatar").file(img).with(userBob()).with(csrf().asHeader()))
73 | .andExpect(status().isOk())
74 | .andExpect(jsonPath("$.status", is("invalid_format")));
75 |
76 | assertThat(new File(uploadingDirPath + "generatedName_big.jpg").exists(), is(equalTo(false)));
77 | assertThat(new File(uploadingDirPath + "generatedName_small.jpg").exists(), is(equalTo(false)));
78 | }
79 |
80 | @Test
81 | @ExpectedDatabase("data.xml")
82 | @DatabaseSetup("data-avatar-added.xml")
83 | public void shouldRemoveAvatar() throws Exception {
84 | mockMvc.perform(post("/remove_avatar").with(userBob()).with(csrf().asHeader()))
85 | .andExpect(status().isOk())
86 | .andExpect(content().string("ok"));
87 | }
88 |
89 | }
--------------------------------------------------------------------------------
/src/test/java/alexp/blog/model/CommentTest.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.model;
2 |
3 | import org.junit.Before;
4 | import org.junit.Test;
5 |
6 | import static org.hamcrest.MatcherAssert.assertThat;
7 | import static org.hamcrest.Matchers.equalTo;
8 | import static org.junit.Assert.*;
9 |
10 | public class CommentTest {
11 |
12 | private Comment comment;
13 |
14 | @Before
15 | public void setUp() {
16 | comment = new Comment();
17 | }
18 |
19 | @Test
20 | public void commentLevel() {
21 | assertThat(comment.commentLevel(), equalTo(0));
22 |
23 | Comment replyComment = new Comment();
24 | replyComment.setParentComment(comment);
25 |
26 | assertThat(replyComment.commentLevel(), equalTo(1));
27 |
28 | Comment replyComment2 = new Comment();
29 | replyComment2.setParentComment(replyComment);
30 |
31 | assertThat(replyComment2.commentLevel(), equalTo(2));
32 | }
33 |
34 | @Test
35 | public void testRatingSum() {
36 | assertThat(comment.getRatingSum(), equalTo(0));
37 |
38 | comment.getCommentRatings().add(new CommentRating(null, Rating.LIKE_VALUE, null));
39 |
40 | assertThat(comment.getRatingSum(), equalTo(1));
41 |
42 | comment.getCommentRatings().add(new CommentRating(null, Rating.DISLIKE_VALUE, null));
43 | comment.getCommentRatings().add(new CommentRating(null, Rating.DISLIKE_VALUE, null));
44 |
45 | assertThat(comment.getRatingSum(), equalTo(-1));
46 | }
47 |
48 | @Test
49 | public void testUserVoteValue() {
50 | assertThat(comment.getUserVoteValue(null), equalTo((short) 0));
51 | assertThat(comment.getUserVoteValue(1L), equalTo((short) 0));
52 |
53 | User user = new User();
54 | user.setId(1L);
55 |
56 | comment.getCommentRatings().add(new CommentRating(user, Rating.LIKE_VALUE, null));
57 |
58 | assertThat(comment.getUserVoteValue(user.getId()), equalTo(Rating.LIKE_VALUE));
59 | }
60 | }
--------------------------------------------------------------------------------
/src/test/java/alexp/blog/model/PostTest.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.model;
2 |
3 | import org.junit.Before;
4 | import org.junit.Test;
5 |
6 | import java.lang.reflect.Array;
7 | import java.util.Arrays;
8 | import java.util.List;
9 |
10 | import static org.hamcrest.MatcherAssert.assertThat;
11 | import static org.hamcrest.Matchers.equalTo;
12 | import static org.hamcrest.Matchers.*;
13 |
14 | public class PostTest {
15 |
16 | private Post post;
17 |
18 | @Before
19 | public void setUp() {
20 | post = new Post();
21 | }
22 |
23 | @Test
24 | public void shouldGetTopLevelComments() {
25 | Comment comment = new Comment();
26 | Comment replyComment = new Comment();
27 | replyComment.setParentComment(comment);
28 | comment.setChildrenComments(Arrays.asList(new Comment(), comment));
29 |
30 | post.setComments(Arrays.asList(new Comment(), comment));
31 |
32 | List comments = post.topLevelComments();
33 |
34 | assertThat(comments.size(), equalTo(2));
35 |
36 | assertThat(comments, not(hasItem(replyComment)));
37 | }
38 |
39 | @Test
40 | public void shouldReturnEmptyList() {
41 | List comments = post.topLevelComments();
42 |
43 | assertThat(comments.size(), equalTo(0));
44 | }
45 |
46 | @Test
47 | public void testRatingSum() {
48 | assertThat(post.getRatingSum(), equalTo(0));
49 |
50 | post.getPostRatings().add(new PostRating(null, Rating.LIKE_VALUE, null));
51 |
52 | assertThat(post.getRatingSum(), equalTo(1));
53 |
54 | post.getPostRatings().add(new PostRating(null, Rating.DISLIKE_VALUE, null));
55 | post.getPostRatings().add(new PostRating(null, Rating.DISLIKE_VALUE, null));
56 |
57 | assertThat(post.getRatingSum(), equalTo(-1));
58 | }
59 |
60 | @Test
61 | public void testUserVoteValue() {
62 | assertThat(post.getUserVoteValue(null), equalTo((short) 0));
63 | assertThat(post.getUserVoteValue(1L), equalTo((short) 0));
64 |
65 | User user = new User();
66 | user.setId(1L);
67 |
68 | post.getPostRatings().add(new PostRating(user, Rating.LIKE_VALUE, null));
69 |
70 | assertThat(post.getUserVoteValue(user.getId()), equalTo(Rating.LIKE_VALUE));
71 | }
72 | }
--------------------------------------------------------------------------------
/src/test/java/alexp/blog/model/UserTest.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.model;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.hamcrest.CoreMatchers.*;
6 | import static org.hamcrest.MatcherAssert.assertThat;
7 |
8 | public class UserTest {
9 |
10 | @Test
11 | public void testHasRole() throws Exception {
12 | Role role = new Role();
13 | role.setName("ROLE_ADMIN");
14 |
15 | User user = new User();
16 |
17 | assertThat(user.hasRole("ROLE_ADMIN"), is(equalTo(false)));
18 |
19 | user.getRoles().add(role);
20 |
21 | assertThat(user.hasRole("ROLE_ADMIN"), is(equalTo(true)));
22 | assertThat(user.hasRole("admin"), is(equalTo(true)));
23 | }
24 | }
--------------------------------------------------------------------------------
/src/test/java/alexp/blog/service/CommentServiceTest.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.service;
2 |
3 | import alexp.blog.controller.ForbiddenException;
4 | import alexp.blog.model.Comment;
5 | import alexp.blog.model.CommentRating;
6 | import alexp.blog.model.Post;
7 | import alexp.blog.model.User;
8 | import alexp.blog.repository.CommentRatingRepository;
9 | import alexp.blog.repository.CommentRepository;
10 | import org.junit.Before;
11 | import org.junit.Test;
12 | import org.mockito.*;
13 |
14 | import java.time.LocalDate;
15 |
16 | import static org.hamcrest.MatcherAssert.assertThat;
17 | import static org.hamcrest.Matchers.*;
18 | import static org.mockito.Mockito.times;
19 | import static org.mockito.Mockito.verify;
20 | import static org.mockito.Mockito.when;
21 |
22 |
23 | public class CommentServiceTest {
24 |
25 | @Mock
26 | private UserService userService;
27 |
28 | @Mock
29 | private CommentRepository commentRepository;
30 |
31 | @Mock
32 | private CommentRatingRepository commentRatingRepository;
33 |
34 | @InjectMocks
35 | private CommentServiceImpl commentService;
36 |
37 | @Spy
38 | private Post post;
39 |
40 |
41 | @Before
42 | public void setUp() throws Exception {
43 | MockitoAnnotations.initMocks(this);
44 | }
45 |
46 | @Test
47 | public void shouldAddNewComment() {
48 | User user = new User();
49 |
50 | when(userService.currentUser()).thenReturn(user);
51 |
52 | Comment comment = new Comment();
53 |
54 | commentService.saveNewComment(comment, post, null);
55 |
56 | assertThat(comment.getPost(), is(equalTo(post)));
57 |
58 | assertThat(comment.getUser(), is(equalTo(user)));
59 |
60 | assertThat(comment.getDateTime().toLocalDate().equals(LocalDate.now()), is(equalTo(true)));
61 |
62 | verify(commentRepository, times(1)).saveAndFlush(Matchers.any(Comment.class));
63 | }
64 |
65 | @Test
66 | public void shouldGetComment() {
67 | final long commentId = 1L;
68 |
69 | Comment comment = new Comment();
70 |
71 | when(commentRepository.findOne(commentId)).thenReturn(comment);
72 |
73 | Comment retrievedComment = commentService.getComment(commentId);
74 |
75 | assertThat(retrievedComment, is(equalTo(comment)));
76 |
77 | verify(commentRepository, times(1)).findOne(commentId);
78 | }
79 |
80 | @Test
81 | public void shouldReturnNullWhenCommentDoesNotExist() {
82 | when(commentRepository.findOne(Matchers.anyLong())).thenReturn(null);
83 |
84 | assertThat(commentService.getComment(1L), is(equalTo(null)));
85 |
86 | verify(commentRepository, times(1)).findOne(Matchers.anyLong());
87 | }
88 |
89 | @Test
90 | public void shouldVote() throws Exception {
91 | Long commentId = 1L;
92 |
93 | User user = new User();
94 | user.setId(10L);
95 |
96 | User anotherUser = new User();
97 | anotherUser.setId(8L);
98 |
99 | Comment comment = new Comment();
100 | comment.setId(commentId);
101 | comment.setUser(anotherUser);
102 |
103 | when(commentRepository.findOne(commentId)).thenReturn(comment);
104 |
105 | when(userService.currentUser()).thenReturn(user);
106 |
107 | when(commentRatingRepository.findUserRating(commentId, user.getId())).thenReturn(null);
108 |
109 | commentService.vote(commentId, true);
110 |
111 | verify(commentRatingRepository, times(1)).findUserRating(commentId, user.getId());
112 | verify(commentRatingRepository, times(1)).saveAndFlush(Matchers.any());
113 | }
114 |
115 | @Test(expected = AlreadyVotedException.class)
116 | public void shouldThrowExceptionWhenAlreadyVoted() throws Exception {
117 | Long commentId = 1L;
118 |
119 | User user = new User();
120 | user.setId(10L);
121 |
122 | User anotherUser = new User();
123 | anotherUser.setId(8L);
124 |
125 | Comment comment = new Comment();
126 | comment.setId(commentId);
127 | comment.setUser(anotherUser);
128 |
129 | when(commentRepository.findOne(commentId)).thenReturn(comment);
130 |
131 | when(userService.currentUser()).thenReturn(user);
132 |
133 | when(commentRatingRepository.findUserRating(commentId, user.getId())).thenReturn(new CommentRating());
134 |
135 | commentService.vote(commentId, true);
136 | }
137 |
138 | @Test(expected = ForbiddenException.class)
139 | public void shouldThrowExceptionWhenVoteForOwnComment() throws Exception {
140 | Long commentId = 1L;
141 |
142 | User user = new User();
143 | user.setId(10L);
144 |
145 | Comment comment = new Comment();
146 | comment.setId(commentId);
147 | comment.setUser(user);
148 |
149 | when(commentRepository.findOne(commentId)).thenReturn(comment);
150 |
151 | when(userService.currentUser()).thenReturn(user);
152 |
153 | when(commentRatingRepository.findUserRating(commentId, user.getId())).thenReturn(new CommentRating());
154 |
155 | commentService.vote(commentId, true);
156 | }
157 | }
--------------------------------------------------------------------------------
/src/test/java/alexp/blog/service/MarkdownConverterTest.java:
--------------------------------------------------------------------------------
1 | package alexp.blog.service;
2 |
3 | import org.junit.Test;
4 |
5 | import java.util.List;
6 |
7 | import static org.hamcrest.CoreMatchers.*;
8 | import static org.hamcrest.MatcherAssert.assertThat;
9 |
10 | public class MarkdownConverterTest {
11 |
12 | @Test
13 | public void shouldReturnHtmlAndEscapeScript() {
14 | String html = MarkdownConverter.toHtml("hello **world**! ");
15 |
16 | assertThat(html, containsString(""));
17 | assertThat(html, not(containsString("