I have to write a simple web application using the Java Spring framework as my course project. So I decided to write a simple Stack Overflow clone. My application has the following features:
- Authorization (using Spring Security);
- Posting new questions and answers;
- Voting up/down for questions/answers.
I am very new in Java, Spring, and web-backend world so I think there is much room for improvement.
Some of my thoughts about problems in my Java code:
- The - VotesControllerclass consists of several almost identical methods. I know that copy-paste is bad, but I have no idea how to deal with it in this case.
- I am not sure about naming conventions in Spring. Have I properly named controllers, entities, fields, etc.? 
- I really hate the way I pass information to the Mustache templates. For example, I need to display a question's creation date in this form: - May 27 '20 at 15:40, but if I just use- Date creationDateTimefield from the- Questionentity then Mustache will display it in the form- 2020-05-27 15:40:49.0.
To solve this problem I have created the String formattedCreationDateTime field in the Question entity and call the Question.formatCreationDateTime method just before passing the question entity to Mustache.
And then I can use formattedCreationDateTime in the template. It is not the only example.
- I also do not like the way I store votes for questions/answers. At this time I have four different join tables: - question_vote_up(question_id, user_id) question_vote_down(question_id, user_id) answer_vote_up(answer_id, user_id) answer_vote_down(answer_id, user_id)
I know that it would be better to create only two tables like this:
    question_vote(question_id, user_id, vote)
    answer_vote(answer_id, user_id, vote)
But I don't know how to implement this database structure in Spring.
I would really appreciate any advice on how to improve my code. I would be glad to see review on my JavaScript and CSS, but it is not a priority.
I've published all the code in the GitHub repository.
Controllers
package com.sstu.StackCanary.controllers;
import java.util.*;
import com.sstu.StackCanary.domain.Answer;
import com.sstu.StackCanary.domain.Question;
import com.sstu.StackCanary.domain.User;
import com.sstu.StackCanary.repositories.AnswerRepository;
import com.sstu.StackCanary.repositories.QuestionRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class AddAnswerController {
    @Autowired
    private QuestionRepository questionRepository;
    @Autowired
    private AnswerRepository answerRepository;
    @PostMapping("/q")
    public String postQuestion(@AuthenticationPrincipal User user,
                               @RequestParam Integer questionId,
                               @RequestParam String body,
                               Map<String, Object> model) {
        // Assuming that the question with given ID always exists.
        Question q = questionRepository.findById(questionId).get();
        // Add new answer to the database.
        answerRepository.save(new Answer(user, q, body));
        // Redirect to the question page.
        return "redirect:/q?id=" + questionId;
    }
}
package com.sstu.StackCanary.controllers;
import java.util.*;
import com.sstu.StackCanary.domain.Question;
import com.sstu.StackCanary.domain.Tag;
import com.sstu.StackCanary.domain.User;
import com.sstu.StackCanary.repositories.QuestionRepository;
import com.sstu.StackCanary.repositories.TagRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class AskQuestionPageController {
    @Autowired
    private QuestionRepository questionRepository;
    @Autowired
    private TagRepository tagRepository;
    @GetMapping("/askQuestion")
    public String main(@AuthenticationPrincipal User user,
                       Map<String, Object> model) {
        model.put("authorizedUser", user);
        return "askQuestion";
    }
    @PostMapping("/askQuestion")
    public String postQuestion(@AuthenticationPrincipal User user,
                               @RequestParam String title,
                               @RequestParam String body,
                               @RequestParam("tag") String [] tagNames,
                               Map<String, Object> model) {
        // Create empty set of tags.
        HashSet<Tag> tags = new HashSet<Tag>();
        // Fill this set with tags with given name from database.
        // If the tag not exist create such new one.
        for (String name : tagNames) {
            Tag tag = tagRepository.findByName(name);
            if (tag == null)
                tag = new Tag(name);
            tagRepository.save(tag);
            tags.add(tag);
        }
        // Create new question and save it in the database.
        Question q = new Question(user, title, body, tags);
        questionRepository.save(q);
        // Redirect to the new question's page.
        return "redirect:/q?id=" + q.getId();
    }
}
package com.sstu.StackCanary.controllers;
import java.util.Map;
import com.sstu.StackCanary.domain.Question;
import com.sstu.StackCanary.domain.User;
import com.sstu.StackCanary.repositories.QuestionRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class IndexController {
    @Autowired
    private QuestionRepository questionRepository;
    @GetMapping
    public String main(@AuthenticationPrincipal User user,
                       Map<String, Object> model) {
        Iterable<Question> questions = questionRepository.findAll();
        // Prepare transient fields
        //
        // — formattedCreationDateTime
        // — votes
        //
        // that will be used in the template.
        questions.forEach(Question::calculateVotes);
        questions.forEach(Question::formatCreationDateTime);
        model.put("questions", questions);
        model.put("authorized", (user != null));
        return "index";
    }
}
package com.sstu.StackCanary.controllers;
import java.util.Map;
import com.sstu.StackCanary.domain.Answer;
import com.sstu.StackCanary.domain.Question;
import com.sstu.StackCanary.domain.User;
import com.sstu.StackCanary.repositories.QuestionRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class QuestionPageController {
    @Autowired
    private QuestionRepository questionRepository;
    @GetMapping("/q")
    public String main(@AuthenticationPrincipal User user,
                       @RequestParam Integer id,
                       Map<String, Object> model) {
        // Assuming that the question with
        // given ID always exists.
        Question q = questionRepository.findById(id).get();
        // Prepare transient fields
        //
        // — formattedCreationDateTime
        // — votes
        // — answersCount
        // — bodyInHTML
        //
        // that will be used in the template.
        q.calculateVotes();
        q.calculateAnswersCount();
        q.formatCreationDateTime();
        q.convertBodyFromMarkdownToHTML();
        q.setVotedByActiveUser(user);
        // Prepare transient fields of the each answer as well
        // as we have done with the question.
        q.answers.forEach(Answer::formatCreationDateTime);
        q.answers.forEach(Answer::calculateVotes);
        q.answers.forEach(Answer::convertBodyFromMarkdownToHTML);
        q.answers.forEach(a -> a.setVotedByActiveUser(user));
        model.put("question", q);
        model.put("authorized", (user != null));
        return "question";
    }
}
package com.sstu.StackCanary.controllers;
import com.sstu.StackCanary.domain.Role;
import com.sstu.StackCanary.domain.User;
import com.sstu.StackCanary.repositories.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import java.util.Collections;
import java.util.Map;
@Controller
public class RegistrationController {
    @Autowired
    private UserRepository userRepository;
    @GetMapping("/registration")
    public String main(Map<String, Object> model) {
        return "registration";
    }
    @PostMapping("/registration")
    public String registerUser(User user, Map<String, Object> model) {
        if (userWithThisUsernameAlreadyExists(user)) {
            model.put("userWithThisUsernameAlreadyExistsMessage", "User with this username already exists.");
            return "registration";
        }
        user.setActive(true);
        user.setRoles(Collections.singleton(Role.USER));
        userRepository.save(user);
        return "redirect:/login";
    }
    private boolean userWithThisUsernameAlreadyExists(User u) {
        return userRepository.findByUsername(u.getUsername()) != null;
    }
}
package com.sstu.StackCanary.controllers;
import com.sstu.StackCanary.domain.Answer;
import com.sstu.StackCanary.domain.Question;
import com.sstu.StackCanary.domain.User;
import com.sstu.StackCanary.repositories.AnswerRepository;
import com.sstu.StackCanary.repositories.QuestionRepository;
import com.sstu.StackCanary.repositories.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Map;
@Controller
public class VotesController {
    @Autowired
    private AnswerRepository answerRepository;
    @Autowired
    private QuestionRepository questionRepository;
    @Autowired
    private UserRepository userRepository;
    @PostMapping("/voteUpForAnswer")
    public String voteUpForAnswer(@AuthenticationPrincipal User user,
                                  @RequestParam Integer questionId,
                                  @RequestParam Integer answerId,
                                  Map<String, Object> model) {
        Answer answer = answerRepository.findById(answerId).get();
        answer.votedUpByUsers.add(user);
        answer.votedDownByUsers.remove(user);
        user.voteUpForAnswer(answer);
        answerRepository.save(answer);
        userRepository.save(user);
        return "redirect:/q?id=" + questionId;
    }
    @PostMapping("/undoVoteUpForAnswer")
    public String undoVoteUpForAnswer(@AuthenticationPrincipal User user,
                                      @RequestParam Integer answerId,
                                      Map<String, Object> model) {
        Answer answer = answerRepository.findById(answerId).get();
        answer.votedUpByUsers.remove(user);
        user.getVotedUpAnswers().remove(answer);
        answerRepository.save(answer);
        userRepository.save(user);
        return "redirect:/q?id=" + answerId;
    }
    @PostMapping("/voteDownForAnswer")
    public String voteDownForAnswer(@AuthenticationPrincipal User user,
                                    @RequestParam Integer questionId,
                                    @RequestParam Integer answerId,
                                    Map<String, Object> model) {
        Answer answer = answerRepository.findById(answerId).get();
        answer.votedDownByUsers.add(user);
        answer.votedUpByUsers.remove(user);
        user.voteDownForAnswer(answer);
        answerRepository.save(answer);
        userRepository.save(user);
        return "redirect:/q?id=" + questionId;
    }
    @PostMapping("/undoVoteDownForAnswer")
    public String undoVoteDownForAnswer(@AuthenticationPrincipal User user,
                                        @RequestParam Integer answerId,
                                        Map<String, Object> model) {
        Answer answer = answerRepository.findById(answerId).get();
        answer.votedDownByUsers.remove(user);
        user.getVotedDownAnswers().remove(answer);
        answerRepository.save(answer);
        userRepository.save(user);
        return "redirect:/q?id=" + answerId;
    }
    @PostMapping("/voteUpForQuestion")
    public String voteUpForQuestion(@AuthenticationPrincipal User user,
                                    @RequestParam Integer questionId,
                                    Map<String, Object> model) {
        Question question = questionRepository.findById(questionId).get();
        question.votedUpByUsers.add(user);
        question.votedDownByUsers.remove(user);
        user.voteUpForQuestion(question);
        questionRepository.save(question);
        userRepository.save(user);
        return "redirect:/q?id=" + questionId;
    }
    @PostMapping("/undoVoteUpForQuestion")
    public String undoVoteUpForQuestion(@AuthenticationPrincipal User user,
                                        @RequestParam Integer questionId,
                                        Map<String, Object> model) {
        Question question = questionRepository.findById(questionId).get();
        question.votedUpByUsers.remove(user);
        user.getVotedUpQuestions().remove(question);
        questionRepository.save(question);
        userRepository.save(user);
        return "redirect:/q?id=" + questionId;
    }
    @PostMapping("/voteDownForQuestion")
    public String voteDownForQuestion(@AuthenticationPrincipal User user,
                                      @RequestParam Integer questionId,
                                      Map<String, Object> model) {
        Question question = questionRepository.findById(questionId).get();
        question.votedDownByUsers.add(user);
        question.votedUpByUsers.remove(user);
        user.voteDownForQuestion(question);
        questionRepository.save(question);
        userRepository.save(user);
        return "redirect:/q?id=" + questionId;
    }
    @PostMapping("/undoVoteDownForQuestion")
    public String undoVoteDownForQuestion(@AuthenticationPrincipal User user,
                                          @RequestParam Integer questionId,
                                          Map<String, Object> model) {
        Question question = questionRepository.findById(questionId).get();
        question.votedDownByUsers.remove(user);
        user.getVotedDownQuestions().remove(question);
        questionRepository.save(question);
        userRepository.save(user);
        return "redirect:/q?id=" + questionId;
    }
}
Entities
package com.sstu.StackCanary.domain;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import javax.persistence.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Set;
@Entity
public class Answer {
    //==========================================
    //
    // Database Columns
    //
    //==========================================
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    @Column(columnDefinition = "LONGTEXT")
    private String body;
    @Column(name = "creationDateTime", columnDefinition = "DATETIME")
    @Temporal(TemporalType.TIMESTAMP)
    private Date creationDateTime;
    //==========================================
    //
    // Relations
    //
    //==========================================
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "author")
    private User author;
    @ManyToOne
    @JoinColumn(name = "question", nullable = false)
    private Question question;
    @ManyToMany
    @JoinTable(
            name = "answer_vote_up",
            joinColumns = @JoinColumn(name = "answer_id"),
            inverseJoinColumns = @JoinColumn(name = "user_id")
    )
    public Set<User> votedUpByUsers;
    @ManyToMany
    @JoinTable(
            name = "answer_vote_down",
            joinColumns = @JoinColumn(name = "answer_id"),
            inverseJoinColumns = @JoinColumn(name = "user_id")
    )
    public Set<User> votedDownByUsers;
    //==========================================
    //
    // Transient Fields
    //
    // This fields must be initialized manually by
    // calling the corresponding entity's method.
    //==========================================
    @Transient
    private String formattedCreationDateTime;
    @Transient
    public Integer votes;
    @Transient
    public String bodyInHTML;
    @Transient
    public boolean votedUpByActiveUser;
    @Transient
    public boolean votedDownByActiveUser;
    //==========================================
    //
    // Constructors
    //
    //==========================================
    protected Answer() {}
    public Answer(User author, Question question, String body) {
        this.author = author;
        this.question = question;
        this.body = body;
        // Assign current date and time.
        this.creationDateTime = new Date();
    }
    //==========================================
    //
    // Methods
    //
    //==========================================
    public void formatCreationDateTime() {
        DateFormat d = new SimpleDateFormat("MMM d ''yy 'at' HH:mm");
        formattedCreationDateTime = d.format(creationDateTime);
    }
    public void calculateVotes() {
        votes = votedUpByUsers.size() - votedDownByUsers.size();
    }
    public void convertBodyFromMarkdownToHTML() {
        Node           document  =  Parser.builder().build().parse(body);
        HtmlRenderer   renderer  =  HtmlRenderer.builder().escapeHtml(true).build();
        bodyInHTML               =  renderer.render(document);
    }
    public void setVotedByActiveUser(User user) {
        if (user == null) {
            this.votedUpByActiveUser = false;
            this.votedDownByActiveUser = false;
        } else if (user.getVotedUpAnswers().contains(this)) {
            this.votedUpByActiveUser = true;
            this.votedDownByActiveUser = false;
        } else if (user.getVotedDownAnswers().contains(this)) {
            this.votedUpByActiveUser = false;
            this.votedDownByActiveUser = true;
        } else {
            this.votedUpByActiveUser = false;
            this.votedDownByActiveUser = false;
        }
    }
    @Override
    public boolean equals(Object that) {
        if (this == that)
            return true;
        if (!(that instanceof Answer))
            return false;
        Answer thatAnswer = (Answer) that;
        return this.id.equals(thatAnswer.id);
    }
    @Override
    public int hashCode() {
        final int PRIME = 37;
        return PRIME * id.hashCode();
    }
}
package com.sstu.StackCanary.domain;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.commonmark.renderer.html.HtmlRenderer;
import javax.persistence.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Set;
@Entity
public class Question {
    //==========================================
    //
    // Database Columns
    //
    //==========================================
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String title;
    @Column(columnDefinition = "LONGTEXT")
    private String body;
    @Column(name = "creationDateTime", columnDefinition = "DATETIME")
    @Temporal(TemporalType.TIMESTAMP)
    private Date creationDateTime;
    //==========================================
    //
    // Relations
    //
    //==========================================
    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "author")
    private User author;
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
        name = "question_tag",
        joinColumns = @JoinColumn(name = "question_id"),
        inverseJoinColumns = @JoinColumn(name = "tag_id")
    )
    private Set<Tag> tags;
    @OneToMany(mappedBy = "question", fetch = FetchType.EAGER)
    public Set<Answer> answers;
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
            name = "question_vote_up",
            joinColumns = @JoinColumn(name = "question_id"),
            inverseJoinColumns = @JoinColumn(name = "user_id")
    )
    public Set<User> votedUpByUsers;
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
            name = "question_vote_down",
            joinColumns = @JoinColumn(name = "question_id"),
            inverseJoinColumns = @JoinColumn(name = "user_id")
    )
    public Set<User> votedDownByUsers;
    //==========================================
    //
    // Transient Fields
    //
    // This fields must be initialized manually by
    // calling the corresponding entity's method.
    //==========================================
    @Transient
    public String formattedCreationDateTime;
    @Transient
    public Integer votes;
    @Transient
    public Integer answersCount;
    @Transient
    public String bodyInHTML;
    @Transient
    public boolean votedUpByActiveUser;
    @Transient
    public boolean votedDownByActiveUser;
    //==========================================
    //
    // Constructors
    //
    //==========================================
    protected Question() {}
    public Question(User author, String title, String body, Set<Tag> tags) {
        this.author = author;
        this.title = title;
        this.body = body;
        this.tags = tags;
        // Assign current date and time.
        this.creationDateTime = new Date();
    }
    //==========================================
    //
    // Getters and Setters
    //
    //==========================================
    public Integer getId() {
        return id;
    }
    //==========================================
    //
    // Methods
    //
    //==========================================
    public void formatCreationDateTime() {
        DateFormat d = new SimpleDateFormat("MMM d ''yy 'at' HH:mm");
        formattedCreationDateTime = d.format(creationDateTime);
    }
    public void calculateVotes() {
        votes = votedUpByUsers.size() - votedDownByUsers.size();
    }
    public void calculateAnswersCount() {
        answersCount = this.answers.size();
    }
    public void convertBodyFromMarkdownToHTML() {
        Node           document  =  Parser.builder().build().parse(body);
        HtmlRenderer   renderer  =  HtmlRenderer.builder().escapeHtml(true).build();
        bodyInHTML               =  renderer.render(document);
    }
    public void setVotedByActiveUser(User user) {
        if (user == null) {
            this.votedUpByActiveUser = false;
            this.votedDownByActiveUser = false;
        } else if (user.getVotedUpQuestions().contains(this)) {
            this.votedUpByActiveUser = true;
            this.votedDownByActiveUser = false;
        } else if (user.getVotedDownQuestions().contains(this)) {
            this.votedUpByActiveUser = false;
            this.votedDownByActiveUser = true;
        } else {
            this.votedUpByActiveUser = false;
            this.votedDownByActiveUser = false;
        }
    }
    @Override
    public boolean equals(Object that) {
        if (this == that)
            return true;
        if (!(that instanceof Question))
            return false;
        Question thatQuestion = (Question) that;
        return this.id.equals(thatQuestion.id);
    }
    @Override
    public int hashCode() {
        final int PRIME = 37;
        return PRIME * id.hashCode();
    }
}
package com.sstu.StackCanary.domain;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.util.Collection;
import java.util.Set;
@Entity
public class User implements UserDetails {
    //==========================================
    //
    // Database Columns
    //
    //==========================================
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private String username;
    private String password;
    private Boolean active;
    @ElementCollection(targetClass = Role.class, fetch = FetchType.EAGER)
    @CollectionTable(name = "user_role", joinColumns = @JoinColumn(name = "user_id"))
    @Enumerated(EnumType.STRING)
    @Column(name = "role")
    private Set<Role> roles;
    //==========================================
    //
    // Relations
    //
    //==========================================
    @ManyToMany(mappedBy = "votedUpByUsers", fetch = FetchType.EAGER)
    private Set<Question> votedUpQuestions;
    @ManyToMany(mappedBy = "votedDownByUsers", fetch = FetchType.EAGER)
    private Set<Question> votedDownQuestions;
    @ManyToMany(mappedBy = "votedUpByUsers", fetch = FetchType.EAGER)
    private Set<Answer> votedUpAnswers;
    @ManyToMany(mappedBy = "votedDownByUsers", fetch = FetchType.EAGER)
    private Set<Answer> votedDownAnswers;
    //==========================================
    //
    // Constructors
    //
    //==========================================
    protected User() {}
    //==========================================
    //
    // Getters and Setters
    //
    //==========================================
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public Boolean getActive() {
        return active;
    }
    public void setActive(Boolean active) {
        this.active = active;
    }
    public Set<Role> getRoles() {
        return roles;
    }
    public void setRoles(Set<Role> roles) {
        this.roles = roles;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public Set<Question> getVotedUpQuestions() {
        return votedUpQuestions;
    }
    public void setVotedUpQuestions(Set<Question> votedUpQuestions) {
        this.votedUpQuestions = votedUpQuestions;
    }
    public Set<Question> getVotedDownQuestions() {
        return votedDownQuestions;
    }
    public void setVotedDownQuestions(Set<Question> votedDownQuestions) {
        this.votedDownQuestions = votedDownQuestions;
    }
    public Set<Answer> getVotedUpAnswers() {
        return votedUpAnswers;
    }
    public void setVotedUpAnswers(Set<Answer> votedUpAnswers) {
        this.votedUpAnswers = votedUpAnswers;
    }
    public Set<Answer> getVotedDownAnswers() {
        return votedDownAnswers;
    }
    public void setVotedDownAnswers(Set<Answer> votedDownAnswers) {
        this.votedDownAnswers = votedDownAnswers;
    }
    @Override
    public boolean equals(Object that) {
        if (this == that)
            return true;
        if (!(that instanceof User))
            return false;
        User thatUser = (User) that;
        return this.id.equals(thatUser.id);
    }
    @Override
    public int hashCode() {
        final int PRIME = 37;
        return PRIME * id.hashCode();
    }
    public void voteUpForQuestion(Question q) {
        votedUpQuestions.add(q);
        votedDownQuestions.remove(q);
    }
    public void voteDownForQuestion(Question q) {
        votedDownQuestions.add(q);
        votedUpQuestions.remove(q);
    }
    public void voteUpForAnswer(Answer q) {
        votedUpAnswers.add(q);
        votedDownAnswers.remove(q);
    }
    public void voteDownForAnswer(Answer q) {
        votedDownAnswers.add(q);
        votedUpAnswers.remove(q);
    }
    //==========================================
    //
    // UserDetails abstract methods implementation
    //
    //==========================================
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return getRoles();
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return getActive();
    }
}
JS scripts:
questionAndAnswersBodyRendering.js:
"use strict";
function renderQuestionAndAnswersBodies() {
    convertQuestionBodyToHTML();
    convertAnswersBodiesToHTML();
    highlightCodeInQuestion();
    highlightCodeInAnswers();
}
function convertQuestionBodyToHTML() {
    let questionBody = document.getElementById("questionBody");
    questionBody.innerHTML = replaceHTMLEntitiesWithRealCharacters(questionBody.innerHTML);
    // Add support for HTML tags inside Markdown code
    // that comes from the server.
    for (let e of questionBody.getElementsByTagName("*"))
        if (e.tagName !== "CODE" && e.tagName !== "PRE")
            e.innerHTML = replaceHTMLEntitiesWithRealCharacters(e.innerHTML);
}
function convertAnswersBodiesToHTML() {
    let answersBodies = document.getElementsByClassName("answerBody");
    for (let a of answersBodies) {
        a.innerHTML = replaceHTMLEntitiesWithRealCharacters(a.innerHTML);
        // Add support for HTML tags inside Markdown code
        // that comes from the server.
        for (let e of a.getElementsByTagName("*"))
            if (e.tagName !== "CODE")
                e.innerHTML = replaceHTMLEntitiesWithRealCharacters(e.innerHTML);
    }
}
function replaceHTMLEntitiesWithRealCharacters(string) {
    function replaceAll(string, search, replace) {
      return string.split(search).join(replace);
    }
    string = replaceAll(string,  "<", "<");
    string = replaceAll(string,  ">", ">");
    // This HTML entity should be the last since
    // it can affect on the other entities.
    string = replaceAll(string, "&", "&");
    return string;
}
function highlightCodeInQuestion() {
    let questionBody = document.getElementById("questionBody");
    highlightCodeInsideElement(questionBody);
}
function highlightCodeInAnswers() {
    let answersBodies = document.getElementsByClassName("answerBody");
    for (let a of answersBodies)
        highlightCodeInsideElement(a);
}
function highlightCodeInsideElement(element) {
    let children = element.getElementsByTagName("*");
    for (let c of children)
        if (c.tagName === "CODE" && c.parentElement.tagName === "PRE")
            hljs.highlightBlock(c);
}
"use strict";
let tagsList = [];
const MAX_TAGS_COUNT = 5;
function tagEditorInputOnInput() {
    var tagEditorInput = document.getElementById("tagEditorInput");
    function clearInput() {
        tagEditorInput.value = "";
    }
    let   value            = tagEditorInput.value;
    let   length           = value.length;
    const firstCharacter   = getStringFirstCharacter(value);
    const lastCharacter    = getStringLastCharacter(value);
    if (tagsList.length >= MAX_TAGS_COUNT) {
        clearInput();
    } else if (length < 2 && firstCharacter === " ") {
        clearInput();
    } else if (lastCharacter === " ") {
        const tagName = value.toLowerCase().trim();
        tagsList.push(tagName);
        clearInput();
        renderTags();
        updateTagInputs();
    }
}
function renderTags() {
    removeAllRenderedTags();
    let renderedTags = document.getElementById("renderedTags");
    for (let t of tagsList)
        renderedTags.appendChild(createRendererTagElement(t));
}
function createRendererTagElement(tagName) {
    let tag = document.createElement("span");
    addClass(tag, "renderedTag");
    tag.innerHTML  = '<span class="tagName">' + tagName + '</span>';
    tag.innerHTML += '<svg onmouseup="removeRenderedTag(this.parentElement.firstChild);" class="removeTagButton" width="14" height="14" viewBox="0 0 14 14"><path d="M12 3.41L10.59 2 7 5.59 3.41 2 2 3.41 5.59 7 2 10.59 3.41 12 7 8.41 10.59 12 12 10.59 8.41 7z"></path></svg>';
    return tag;
}
function removeAllRenderedTags() {
    let renderedTags = document.getElementById("renderedTags");
    renderedTags.innerHTML = "";
}
function removeRenderedTag(element) {
    const tagName  = getFirstWordInString(element.innerHTML);
    const tagIndex = tagsList.indexOf(tagName);
    removeItemFromArray(tagsList, tagIndex);
    renderTags();
}
function updateTagInputs() {
    for (let i = 0; i < 5; ++i) {
        let tag = document.getElementById("tag" + i);
        if (tagsList[i] === undefined)
            tag.name = "emptyTag";
        else
            tag.name = "tag";
        tag.value = tagsList[i];
    }
}
function removeLastCharacterInString(s) {
    return s.substring(0, s.length - 1);
}
function getStringLastCharacter(s) {
    return s.slice(s.length - 1);
}
function getStringFirstCharacter(s) {
    return s[0];
}
function getFirstWordInString(s) {
    const spaceIndex = s.indexOf(" ");
    if (spaceIndex === -1)
        return s;
    else
        return s.substr(0, spaceIndex);
};
function removeItemFromArray(array, index) {
    array.splice(index, 1);
}
function addClass(element, className) {
    element.classList.add(className);
}
function removeClass(element, className) {
    if (element.classList.contains(className))
        element.classList.remove(className);
}

