1
\$\begingroup\$

I'm working on a blog REST API and I would like to get some opinions on where to place the logic for a single operation to filter posts.

I have Category and Post in the model package. A category has a list of posts and a post has a single category.

The wrinkle is that posts can be hidden, meaning if you don't have the correct permission you won't see them. I'm using JPA and don't want vendor specific SQL so I'm doing the filtering after retrieving the posts since I can't join in the JPARepository.

Right now I have the helper method to filter a list of posts as package private in the PostController and the CategoryController autowires it in.

While that's less than ideal I feel like having a PostService for this one method might be overkill and it should be in an object rather than a service.

Other options I considered were having it in the SecurityService (doesn't really fit) or the Post model (it's not part of the model so shouldn't be here).

I also considered some design patterns but I don't think anything really fits here (decorator possibly but again seems like way too much).

This is a personal project so I'd really like to get it 'right'.

Post model

@Entity
//ignore hibernate methods added to object automatically
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Post {

    /* Required for hibernate since full constructor is declared for testing, do not remove */
    public Post() {}

    public Post(String description, String content, Date lastEditedDate, List<String> tags, String title, Category category, boolean isHidden) {
        this.description = description;
        this.content = content;
        this.lastEditedDate = lastEditedDate;
        this.tags = tags;
        this.title = title;
        this.category = category;
        this.isHidden = isHidden;
    }

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Lob
    @Column(length = 100000)
    private String description;

    @Lob
    @Column(length = 100000)
    private String content;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "MM-dd-yyyy")
    private Date lastEditedDate;

    @ElementCollection
    private List<String> tags;

    private String title;

    @ManyToOne
    @JoinColumn
    private Category category;

    private boolean isHidden;

    public Long getId() {
        return id;
    }

    //auto-generated but jackson needs the method for marshalling
    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public List<String> getTags() {
        return tags;
    }

    public void setTags(List<String> tags) {
        this.tags = tags;
    }

    public Category getCategory() {
        return category;
    }

    public void setCategory(Category category) {
        this.category = category;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public boolean isHidden() {
        return isHidden;
    }

    public void setHidden(boolean hidden) {
        isHidden = hidden;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public Date getLastEditedDate() {
        return lastEditedDate;
    }

    public void setLastEditedDate(Date lastEditedDate) {
        this.lastEditedDate = lastEditedDate;
    }
}

Category Model

@Entity
public class Category {

    public Category(String name, Post... posts) {
        this.name = name;
        this.posts = Stream.of(posts)
            .collect(Collectors.toList());
        this.posts.forEach(post -> post.setCategory(this));
    }

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    private String name;

    @OneToMany(mappedBy = "category")
    private List<Post> posts;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public List<Post> getPosts() {
        return posts;
    }

    public void setPosts(List<Post> posts) {
        this.posts = posts;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Category category = (Category) o;
        return id == category.id &&
            Objects.equals(name, category.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }
}

PostsController

@RestController
@RequestMapping("/api/v1/posts")
@CrossOrigin(origins = {"${settings.allowed-origins}"})
public class PostsController {
    private PostRepository postRepository;
    private SecurityService securityService;
    private Logger log = LoggerFactory.getLogger(PostsController.class);


    @Autowired
    public PostsController(PostRepository postRepository, SecurityService securityService) {
        this.postRepository = postRepository;
        this.securityService = securityService;
    }

    @GetMapping
    public List<Post> list(Authentication authentication) {
        boolean canViewHidden = securityService.hasAuthority(Permissions.Post.VIEWHIDDEN, authentication.getAuthorities());
        return filterPosts(canViewHidden, postRepository.findAll());

    }

    //this is the method I'm looking for a home for
    List<Post> filterPosts(boolean canViewHidden, List<Post> posts) {
        return posts.stream()
            .filter(post -> canViewHidden || !post.isHidden())
            .collect(Collectors.toList());
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Post create(@RequestBody Post post) {
        post.setLastEditedDate(new Date());
        postRepository.save(post);
        return post;
    }

    @GetMapping("/{id}")
    public Post get(@PathVariable("id") long id, Authentication authentication) {
        try {
            Post p = postRepository.getOne(id);
            if (!p.isHidden() || securityService.hasAuthority(Permissions.Post.VIEWHIDDEN, authentication.getAuthorities())) {
                return p;
            }
        } catch (EntityNotFoundException e) {
            log.info(String.format("Record not found for post id %d", id));
        }
        return null;
    }

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable("id") long id) {
        postRepository.deleteById(id);
    }

    @PutMapping("/{id}")
    @ResponseStatus(HttpStatus.CREATED)
    public Post update(@PathVariable("id") long id, @RequestBody Post post) {
        post.setId(id);
        post.setLastEditedDate(new Date());
        postRepository.save(post);
        return post;
    }

    @RequestMapping(method = RequestMethod.OPTIONS)
    @ResponseStatus(HttpStatus.ACCEPTED)
    public void options() {
    }
}

CategoryController

@RestController
@RequestMapping("/api/v1/category")
@CrossOrigin(origins = {"${settings.allowed-origins}"})
public class CategoryController {

    private CategoryRepository categoryRepository;
    private SecurityService securityService;
    private PostsController postsController;

    private Logger log = LoggerFactory.getLogger(CategoryController.class);

    @Autowired
    public CategoryController(CategoryRepository categoryRepository, SecurityService securityService, PostsController postsController) {
        this.categoryRepository = categoryRepository;
        this.securityService = securityService;
        this.postsController = postsController;
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Category create(@RequestBody Category category) {
        categoryRepository.save(category);
        return category;
    }

    @GetMapping
    public List<Category> list(Authentication authentication) {
        boolean canViewHidden = securityService.hasAuthority(Permissions.Post.VIEWHIDDEN, authentication.getAuthorities());
        List<Category> categories = categoryRepository.findAll();
        categories.forEach(category -> filterCategoryPosts(canViewHidden, category));
        return categories;
    }

    @GetMapping("/{id}")
    public Category get(@PathVariable("id") long id, Authentication authentication) {
        try {
            boolean canViewHidden = securityService.hasAuthority(Permissions.Post.VIEWHIDDEN, authentication.getAuthorities());
            return filterCategoryPosts(canViewHidden, categoryRepository.getOne(id));

        } catch (EntityNotFoundException e) {
            log.info(String.format("Record not found for post id %d", id));
        }
        return null;
    }

    private Category filterCategoryPosts(boolean canViewHidden, Category category) {
        if (category == null)
            return null;
        List<Post> filteredPosts = postsController.filterPosts(canViewHidden, category.getPosts());
        category.setPosts(filteredPosts);
        return category;
    }
}
\$\endgroup\$
  • \$\begingroup\$ I think it is possible to make your default constructors private for hibernate. Also put the comment explaining why it is empty inside your constructor. Furthermore you should put al your variables before the constructor as is convention. \$\endgroup\$ – sarcode May 28 at 8:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.