Как передать из spring контроллера hateoas-совместимый список сущностей

Рассмотрим случай передачи из spring контроллера списка сущностей, когда использование создаваемых на лету методов интерфейса spring data почему-то не подходит. Использовать будем hateoas. В данном примере мы будем передавать на вход методу сущность с параметрами поиска в виде JSON, на основе этой сущности потом будет формироваться JPA спецификация для передачи в метод репозитория.

Метод контроллера:

@MonitoredWithSpring
@RepositoryRestController
@RequestMapping(value = "topics")
public class TopicController implements ResourceProcessor<PersistentEntityResource> {
...
    @RequestMapping(value = "search/searchWithCriteria",
            method = RequestMethod.POST,
            consumes = APPLICATION_JSON_VALUE,
            produces = MediaTypes.HAL_JSON_VALUE)
    public @ResponseBody Resources<PersistentEntityResource> searchTopics(@RequestBody Resource<TopicSearchCriteria> topicSearchCriteria,
                                          PersistentEntityResourceAssembler persistentEntityResourceAssembler) {
        List<Topic> entities = topicService.searchTopics(topicSearchCriteria.getContent());
 
        List<PersistentEntityResource> resources = entities
                .stream()
                .map(persistentEntityResourceAssembler::toResource)
                .collect(Collectors.toList());
 
        return new Resources<PersistentEntityResource>(resources);
    }
...
}

Запомним, что это метод обслуживает ссылку «topics/search/searchWithCriteria».

Действия в службе примерно такие:

    @Override
    @Transactional
    public List<Topic> searchTopics(TopicSearchCriteria criteria) {
        TopicSpecification spec = new TopicSpecification(criteria);
        List<Topic> topics = topicRepo.findAll(spec);
        return topics;
    }

По JPA спецификациям много статей, поэтому рассматривать их не буду.

Для полной совместимости с HATEOAS вам нужно будет добиться того, чтобы в разделе ссылок «search» у сущности Topic появилась ссылка «searchWithCriteria». Я делал это в отдельном компоненте (это даже не контроллер — ни одной ссылки не обслуживает), который даже не обслуживает ссылки:

@Component
public class TopicSearchComponent implements ResourceProcessor<RepositorySearchesResource> {
 
    @Autowired
    private EntityLinks entityLinks;
 
    @Override
    public RepositorySearchesResource process(RepositorySearchesResource resource) {
        if (resource.getDomainType().equals(Topic.class)) {
            LinkBuilder lb = entityLinks.linkFor(Topic.class).slash("search");
            resource.add(new Link(lb.toString() + "/searchWithCriteria", "searchWithCriteria"));
        }
        return resource;
    }
}

Для того, чтобы получить доступ к разделу search, нужно реализовать шаблонный интерфейс ResourceProcessor, а наш TopicController уже реализует этот же интерфейс с другим шаблонным типом. В методе TopicSearchController мы проверяем тип сущности и для нужной добавляем поисковую ссылку.

При десериализации ответа контроллера со списком сущностей вы увидите, что он составлен достаточно неудобно: список является

{
   "_embedded":
   {
       "topics":
       [...]
   }
}

Следовательно, нужно приготовить классы, которые позволят из такой сериализации достать список сущностей.

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(NON_NULL)
public class TopicSearchResultHalRepresentation {
    @JsonProperty(value = "_embedded")
    private volatile TopicSearchResultEmbedded embedded;
 
    public TopicSearchResultEmbedded getEmbedded() {
        return embedded;
    }
 
    public void setEmbedded(TopicSearchResultEmbedded embedded) {
        this.embedded = embedded;
    }
}
 
public class TopicSearchResultEmbedded {
    private List<Topic> topics;
 
    public TopicSearchResultEmbedded() {
        topics = null;
    }
 
    public List<Topic> getTopics() {
        return topics;
    }
 
    public void setTopics(List<Topic> topics) {
        this.topics = topics;
    }
}

Теперь на стороне фронт-энда можно получить список так:

List<Topic> result = new ArrayList<>();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<TopicSearchCriteria> request = new HttpEntity<>(tsc, httpHeaders);
Traverson traverson = null;
try {
    traverson = new Traverson(new URI(<your back end's URL>), MediaTypes.HAL_JSON);
} catch (URISyntaxException e) {
    LOG.error(ExceptionUtils.getStackTrace(e));
}
 
Link link = traverson.follow(TOPICS_REL).asLink();
 
ResponseEntity<TopicSearchResultHalRepresentation> topicResponse =
                    restOperations.exchange(link.getHref() + "/" + "search" + "/" + "searchWithCriteria", HttpMethod.POST, request, TopicSearchResultHalRepresentation.class);
 
if (topicResponse.getBody().getEmbedded() != null) {
     result = topicResponse
              .getBody()
              .getEmbedded()
              .getTopics();
}
return result;
You can leave a response, or trackback from your own site.

Leave a Reply