Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fulltext search and paging support for fault tree summary api #141

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.security:spring-security-oauth2-core'
implementation 'org.springframework.data:spring-data-commons'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
implementation 'org.springframework.boot:spring-boot-starter-validation'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package cz.cvut.kbss.analysis.controller;

import cz.cvut.kbss.analysis.controller.event.PaginatedResultRetrievedEvent;
import cz.cvut.kbss.analysis.controller.util.FaultTreeFilterMapper;
import cz.cvut.kbss.analysis.controller.util.PagingUtils;
import cz.cvut.kbss.analysis.dao.util.FaultTreeFilterParams;
import cz.cvut.kbss.analysis.model.*;
import cz.cvut.kbss.analysis.model.opdata.OperationalDataFilter;
import cz.cvut.kbss.analysis.security.SecurityConstants;
Expand All @@ -9,13 +13,18 @@
import cz.cvut.kbss.analysis.service.IdentifierService;
import cz.cvut.kbss.analysis.util.Vocabulary;
import cz.cvut.kbss.jsonld.JsonLd;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.util.UriComponentsBuilder;

import java.net.URI;
import java.net.URISyntaxException;
Expand All @@ -32,15 +41,20 @@ public class FaultTreeController {
private final FaultTreeEvaluationService faultTreeEvaluationService;
private final IdentifierService identifierService;
private final FaultTreeService faultTreeService;
private final ApplicationEventPublisher eventPublisher;

@GetMapping
public List<FaultTree> findAll() {
return repositoryService.findAll();
}

@GetMapping("/summaries")
public List<FaultTree> summaries() {
return repositoryService.findAllSummaries();
public List<FaultTree> summaries(@RequestParam(required = false) MultiValueMap<String, String> params,
UriComponentsBuilder uriBuilder, HttpServletResponse response) {
FaultTreeFilterParams filterParams = FaultTreeFilterMapper.constructFaultTreeFilter(params);
Page<FaultTree> result = repositoryService.findAllSummaries(filterParams, PagingUtils.resolvePaging(params));
eventPublisher.publishEvent(new PaginatedResultRetrievedEvent(this, uriBuilder, response, result));
return result.getContent();
}

@GetMapping(value = "/{faultTreeFragment}", produces = {JsonLd.MEDIA_TYPE, MediaType.APPLICATION_JSON_VALUE})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package cz.cvut.kbss.analysis.controller.event;

import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.ApplicationEvent;
import org.springframework.data.domain.Page;
import org.springframework.web.util.UriComponentsBuilder;

/**
* Fired when a paginated result is retrieved by a REST controller, so that HATEOAS headers can be added to the
* response.
*/
public class PaginatedResultRetrievedEvent extends ApplicationEvent {

private final UriComponentsBuilder uriBuilder;
private final HttpServletResponse response;
private final Page<?> page;

public PaginatedResultRetrievedEvent(Object source, UriComponentsBuilder uriBuilder, HttpServletResponse response,
Page<?> page) {
super(source);
this.uriBuilder = uriBuilder;
this.response = response;
this.page = page;
}

public UriComponentsBuilder getUriBuilder() {
return uriBuilder;
}

public HttpServletResponse getResponse() {
return response;
}

public Page<?> getPage() {
return page;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package cz.cvut.kbss.analysis.controller.handler;

import cz.cvut.kbss.analysis.controller.event.PaginatedResultRetrievedEvent;
import cz.cvut.kbss.analysis.controller.util.HttpPaginationLink;
import cz.cvut.kbss.analysis.util.Constants;
import org.springframework.context.ApplicationListener;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;

/**
* Generates HATEOAS paging headers based on the paginated result retrieved by a REST controller.
*/
@Component
public class HateoasPagingListener implements ApplicationListener<PaginatedResultRetrievedEvent> {

@Override
public void onApplicationEvent(PaginatedResultRetrievedEvent event) {
final Page<?> page = event.getPage();
final LinkHeader header = new LinkHeader();
if (!page.isEmpty() || page.getTotalPages() > 0) {
// Always add first and last links, even when there is just one page. This allows clients to know where the limits
// are
header.addLink(generateFirstPageLink(page, event.getUriBuilder()), HttpPaginationLink.FIRST);
header.addLink(generateLastPageLink(page, event.getUriBuilder()), HttpPaginationLink.LAST);
}
if (page.hasNext()) {
header.addLink(generateNextPageLink(page, event.getUriBuilder()), HttpPaginationLink.NEXT);
}
if (page.hasPrevious()) {
header.addLink(generatePreviousPageLink(page, event.getUriBuilder()), HttpPaginationLink.PREVIOUS);
}
if (header.hasLinks()) {
event.getResponse().addHeader(HttpHeaders.LINK, header.toString());
}
event.getResponse().addHeader(Constants.X_TOTAL_COUNT_HEADER, Long.toString(page.getTotalElements()));
}

private String generateNextPageLink(Page<?> page, UriComponentsBuilder uriBuilder) {
return uriBuilder.replaceQueryParam(Constants.PAGE_PARAM, page.getNumber() + 1)
.replaceQueryParam(Constants.PAGE_SIZE_PARAM, page.getSize())
.build().encode().toUriString();
}

private String generatePreviousPageLink(Page<?> page, UriComponentsBuilder uriBuilder) {
return uriBuilder.replaceQueryParam(Constants.PAGE_PARAM, page.getNumber() - 1)
.replaceQueryParam(Constants.PAGE_SIZE_PARAM, page.getSize())
.build().encode().toUriString();
}

private String generateFirstPageLink(Page<?> page, UriComponentsBuilder uriBuilder) {
return uriBuilder.replaceQueryParam(Constants.PAGE_PARAM, 0)
.replaceQueryParam(Constants.PAGE_SIZE_PARAM, page.getSize())
.build().encode().toUriString();
}

private String generateLastPageLink(Page<?> page, UriComponentsBuilder uriBuilder) {
return uriBuilder.replaceQueryParam(Constants.PAGE_PARAM, page.getTotalPages() - 1)
.replaceQueryParam(Constants.PAGE_SIZE_PARAM, page.getSize())
.build().encode().toUriString();
}

private static class LinkHeader {

private final StringBuilder linkBuilder = new StringBuilder();

private void addLink(String url, HttpPaginationLink type) {
if (!linkBuilder.isEmpty()) {
linkBuilder.append(", ");
}
linkBuilder.append('<').append(url).append('>').append("; ").append("rel=\"").append(type.getName())
.append('"');
}

private boolean hasLinks() {
return !linkBuilder.isEmpty();
}

@Override
public String toString() {
return linkBuilder.toString();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package cz.cvut.kbss.analysis.controller.util;

import cz.cvut.kbss.analysis.dao.util.FaultTreeFilterParams;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

import java.util.*;

/**
* Maps query parameters to {@link FaultTreeFilterParams} instances.
*/
@Slf4j
public class FaultTreeFilterMapper {

private static final String SNS_LABEL_PARAM = "snsLabel";

private static final String LABEL_PARAM = "label";


/**
* Maps the specified single parameter and value to a new {@link FaultTreeFilterParams} instance.
*
* @param param Parameter name
* @param value Parameter value
* @return New {@code FaultTreeFilterParams} instance
*/
public static FaultTreeFilterParams constructFaultTreeFilter(String param, String value) {
return constructFaultTreeFilter(new LinkedMultiValueMap<>(Map.of(param, List.of(value))));
}

public static FaultTreeFilterParams constructFaultTreeFilter(MultiValueMap<String, String> params) {
final FaultTreeFilterParams result = new FaultTreeFilterParams();
return constructFaultTreeFilter(result, new LinkedMultiValueMap<>(params));
}

/**
* Maps the specified parameters to a new {@link FaultTreeFilterParams} instance.
*
* @param params Request parameters to map
* @return New {@code FaultTreeFilterParams} instance
*/
public static FaultTreeFilterParams constructFaultTreeFilter(final FaultTreeFilterParams result, MultiValueMap<String, String> params) {
Objects.requireNonNull(params);
getSingleValue(SNS_LABEL_PARAM, params).ifPresent(s -> result.setSnsLabel(s));
getSingleValue(LABEL_PARAM, params).ifPresent(s -> result.setLabel(s));

return result;
}

private static Optional<String> getSingleValue(String param, MultiValueMap<String, String> source) {
final List<String> values = source.getOrDefault(param, Collections.emptyList());
if (values.isEmpty()) {
return Optional.empty();
}
if (values.size() > 1) {
log.warn("Found multiple values of parameter '{}'. Using the first one - '{}'.", param, values.get(0));
}
return Optional.of(values.get(0));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package cz.cvut.kbss.analysis.controller.util;

/**
* Types of HTTP pagination links.
*/
public enum HttpPaginationLink {
NEXT("next"), PREVIOUS("prev"), FIRST("first"), LAST("last");

private final String name;

HttpPaginationLink(String name) {
this.name = name;
}

public String getName() {
return name;
}
}
104 changes: 104 additions & 0 deletions src/main/java/cz/cvut/kbss/analysis/controller/util/PagingUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package cz.cvut.kbss.analysis.controller.util;

import cz.cvut.kbss.analysis.model.FaultTree;
import cz.cvut.kbss.analysis.util.Constants;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.util.MultiValueMap;

import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class PagingUtils {

/**
* Prefix indicating ascending sort order.
*/
public static final char SORT_ASC = '+';

/**
* Prefix indicating descending sort order.
*/
public static final char SORT_DESC = '-';



private PagingUtils() {
throw new AssertionError();
}

/**
* Resolves paging and sorting configuration from the specified request parameters.
* <p>
* If no paging and filtering info is specified, an {@link Pageable#unpaged()} object is returned.
* <p>
* Note that for sorting, {@literal +} should be used before sorting property name to specify ascending order,
* {@literal -} for descending order, for example, {@literal -date} indicates sorting by date in descending order.
*
* @param params Request parameters
* @return {@code Pageable} containing values resolved from the params or defaults
*/
public static Pageable resolvePaging(MultiValueMap<String, String> params) {
Sort sort;
if (params.containsKey(Constants.SORT_PARAM)) {
sort = Sort.by(params.get(Constants.SORT_PARAM).stream().map(sp -> {
if (sp.charAt(0) == SORT_ASC || sp.charAt(0) == SORT_DESC) {
final String property = sp.substring(1);
return sp.charAt(0) == SORT_DESC ? Sort.Order.desc(property) : Sort.Order.asc(property);
}
return Sort.Order.asc(sp);
}).collect(Collectors.toList()));
}else{
sort = Sort.by(
Sort.Order.desc(Constants.SORT_BY_DATE_PARAM),
Sort.Order.asc(Constants.SORT_BY_SNS_LABEL_PARAM),
Sort.Order.asc(Constants.SORT_BY_LABEL_PARAM)
);
}

if (params.getFirst(Constants.PAGE_PARAM) == null)
return Pageable.unpaged(sort);

final int page = Integer.parseInt(params.getFirst(Constants.PAGE_PARAM));
final int size = Optional.ofNullable(params.getFirst(Constants.PAGE_SIZE_PARAM)).map(Integer::parseInt)
.orElse(Constants.DEFAULT_PAGE_SIZE);

return PageRequest.of(page, size, sort);
}

public static Comparator<FaultTree> comparator(Sort sort){
Iterator<Sort.Order> orders = sort.iterator();
Comparator<FaultTree> c = null;
while(orders.hasNext()){
Sort.Order order = orders.next();
if(c == null)
c = getFunction(order);
else
c.thenComparing(getFunction(order));
}
return c;
}

private static Comparator<FaultTree> getFunction(Sort.Order order){
Comparator<FaultTree> comp = switch (order.getProperty()){
case Constants.SORT_BY_DATE_PARAM -> Comparator.comparing((FaultTree t) ->
Stream.of(t.getModified(), t.getCreated())
.filter(d -> d != null).findFirst().orElse(new Date(0)));
case Constants.SORT_BY_LABEL_PARAM -> Comparator.comparing((FaultTree t) ->
Optional.ofNullable(t.getSubsystem()).map(i -> i.getName()).orElse(""));
case Constants.SORT_BY_SNS_LABEL_PARAM -> Comparator.comparing((FaultTree t) ->
Optional.ofNullable(t.getName()).orElse(""));
default -> null;
};

return order.getDirection() == Sort.Direction.DESC
? comp.reversed()
: comp;
}

}
Loading
Loading