0

Links are not automatically being provided for resources when using HATEOAS to fetch resource collections.

When fetching a collection of ThreadResource with '/forum/threads', the response is:

{
  "_embedded": {
    "threadList": [
      {
        "posts": [
          {
            "postText": "This text represents a major breakthrough in textual technology.",
            "thread": null,
            "comments": [],
            "thisId": 1
          },
          {
            "postText": "This text represents a major breakthrough in textual technology.",
            "thread": null,
            "comments": [],
            "thisId": 2
          }
        ],
        "createdBy": "admin",
        "updatedBy": null,
        "thisId": 1
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/forum/threads?page=0&size=10"
    }
  },
  "page": {
    "size": 10,
    "totalElements": 1,
    "totalPages": 1,
    "number": 0
  }
}

I was expecting a JSON array of posts (instead of links to associated posts collection), like below:

{
  "_embedded": {
    "threadList": [
      {
        "createdBy": "admin",
        "updatedBy": null,
        "thisId": 1,
        "_links": {
          "posts": {
            "href": "http://localhost:8080/forum/threads/1/posts"
            }
        }
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/forum/threads?page=0&size=10"
    }
  },
  "page": {
    "size": 10,
    "totalElements": 1,
    "totalPages": 1,
    "number": 0
  }
}

I could manually build and add links in ResourceProcessor implementation classes and exclude the collection from being rendered using @JsonIgnore, but I have never had to do this before. What I am doing wrong?

The relevant classes are provided below. Thanks in advance!

@Entity
public class Thread {

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

    @OneToMany
    private List<Post> posts;

    @Column(name = "created_by")
    private String createdBy;

    @Column(name = "updated_by")
    private String updatedBy;

    public Thread() { }

    @PrePersist
    public void prePersist() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        posts = new ArrayList<>();
        createdBy = auth.getName();
    }

    @PreUpdate
    public void preUpdate() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        updatedBy = auth.getName();
    }


    public void submitPost(Post newPost) {
        posts.add(newPost);
    }

    public Long getThisId() {
        return id;
    }

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

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

    public String getCreatedBy() {
        return createdBy;
    }

    public void setCreatedBy(String createdBy) {
        this.createdBy = createdBy;
    }

    public String getUpdatedBy() {
        return updatedBy;
    }

    public void setUpdatedBy(String updatedBy) {
        this.updatedBy = updatedBy;
    }

@Entity
public class Post {

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

    private String postText;

    @ManyToOne(fetch = FetchType.LAZY)
    private Thread thread;

    @OneToMany
    private List<Comment> comments;

    public Post() { }
}

public class ThreadResource extends ResourceSupport {

    private List<PostResource> postResources;

    private String createdBy;

    private String updatedBy;

    public ThreadResource() {
    }
}

public class PostResource extends ResourceSupport {

    private String postText;

    private ThreadResource threadResource;

    private List<CommentResource> commentResources;

    public PostResource() { }

@Component
public class PostResourceAssembler extends ResourceAssemblerSupport<Post, PostResource> {

    public PostResourceAssembler() {
        super(PostController.class, PostResource.class);
    }

    @Override
    public PostResource toResource(Post entity) {
        PostResource resource = super.createResourceWithId(entity.getThisId(), entity);
        resource.setPostText(entity.getPostText());
        return resource;
    }
}

@Component
public class ThreadResourceAssembler extends ResourceAssemblerSupport<Thread, ThreadResource> {

    private PostResourceAssembler postResourceAssembler;

    public ThreadResourceAssembler(PostResourceAssembler postResourceAssembler) {
        super(ThreadController.class, ThreadResource.class);
        this.postResourceAssembler = postResourceAssembler;
    }

    @Override
    public ThreadResource toResource(Thread entity) {
        ThreadResource resource = super.createResourceWithId(entity.getThisId(), entity);
        List<Post> posts = entity.getPosts();
        List<PostResource> postResources = new ArrayList<>();
        posts.forEach((post) -> postResources.add(postResourceAssembler.toResource(post)));
        resource.setPostResources(postResources);
        return resource;
    }
}

@RestController
public class PostController {

    private PostService postService;

    @Autowired
    public PostController(PostService postService) {
    this.postService = postService;
    }

    @GetMapping("/forum/threads/{threadId}/posts/{postId}")
    public ResponseEntity<Resource<Post>> getPost(@PathVariable long threadId, @PathVariable long postId) {
        Post post = postService.fetchPost(postId)
                .orElseThrow(() -> new EntityNotFoundException("not found thread " + postId));
        Link selfLink = linkTo(PostController.class).slash(postId).withSelfRel();
        post.add(selfLink);
        return ResponseEntity.ok(new Resource<>(post));
    }

    @GetMapping
    public ResponseEntity<PagedResources<Resource<Post>>> getPosts(PagedResourcesAssembler<Post> pagedResourcesAssembler) {
        Pageable pageable = new PageRequest(0, 10);
        Page<Post> posts = postService.fetchAllPosts(pageable);
        PagedResources<Resource<Post>> resources = pagedResourcesAssembler.toResource(posts);
        return ResponseEntity.ok(resources);
    }

    @PostMapping("/forum/threads/{threadId}/posts")
    public HttpEntity<?> submitPost(@PathVariable long threadId) throws URISyntaxException {
        Post post = postService.submitPost(threadId, new Post());
        if (post != null) {
            Link selfLink = linkTo(methodOn(PostController.class).submitPost(threadId)).slash(post.getThisId()).withSelfRel();
            post.add(selfLink);
            return ResponseEntity.created(new URI(selfLink.getHref())).build();
        }
        return ResponseEntity.status(500).build();
    }
}

@RestController
public class ThreadController {

    private ThreadService threadService;

    private ThreadResourceAssembler threadResourceAssembler;

    @Autowired
    public ThreadController(ThreadService threadService,
                            ThreadResourceAssembler threadResourceAssembler) {
        this.threadService = threadService;
        this.threadResourceAssembler = threadResourceAssembler;
    }

    @GetMapping("/forum/threads/{threadId}")
    public ResponseEntity<ThreadResource> getThread(@PathVariable long threadId) {
        Thread thread = threadService.fetchThread(threadId)
                .orElseThrow(() -> new EntityNotFoundException("not found thread " + threadId));

        ThreadResource threadResource = threadResourceAssembler.toResource(thread);
        return ResponseEntity.ok(threadResource);
    }

    @GetMapping("/forum/threads")
    public ResponseEntity<PagedResources<Resource<ThreadResource>>> getThreads(PagedResourcesAssembler pagedResourcesAssembler) {
        Pageable pageable = new PageRequest(0, 10);
        Page<Thread> threads = threadService.fetchAllThreads(pageable);
        PagedResources pagedResources = pagedResourcesAssembler.toResource(threads);
        return ResponseEntity.ok(pagedResources);
    }

    @PostMapping("/forum/threads")
    public HttpEntity<?> createThread() {
        Thread thread = threadService.createThread();
        return ResponseEntity.ok(thread);
    }

    @DeleteMapping("/forum/threads/{threadId}")
    public HttpEntity<?> deleteThread(@PathVariable long threadId) {
        Thread thread = threadService.fetchThread(threadId)
                .orElseThrow(() -> new EntityNotFoundException("not found thread" + threadId));
        threadService.closeThread(thread);
        return ResponseEntity.ok().build();
    }
}
av376
  • 15
  • 2
apostrophedottilde
  • 867
  • 13
  • 36
  • I haven't seen the creation of links in your code when returning resources ! – Abdelghani Roussi Mar 10 '19 at 19:02
  • Thats true yes because I am not needing any custom links at this stage. In the past I have allowed spring boot to create the links to associated resources for me. For some reason I cannot get it to work now. I am tempted to just add them manually for now but I am not supposed to need to. – apostrophedottilde Mar 10 '19 at 19:05
  • The behavior that you got is absolutely normal, you are adding a list of `PostResource` inside your `ThreadResource` class; which means that it will be computed when generating your threadResources list. If you want to only have a link posts inside threadList, you should add it manually. – Abdelghani Roussi Mar 10 '19 at 19:11
  • have a look at this [post](https://www.baeldung.com/spring-hateoas-tutorial) – Abdelghani Roussi Mar 10 '19 at 19:12
  • Hi thanks, I have already google around and I saw this post. I think when @RepositoryRestController annotation was available (maybe in spring boot 1? Either way it's not available now) it was possible to have many of the links autogenerated. That was the magic of it. Thanks anyway. – apostrophedottilde Mar 10 '19 at 19:16

0 Answers0