Skip to content

Commit

Permalink
Merge pull request #141 from kbss-cvut/feature/fta-fmea-ui-507-add-fa…
Browse files Browse the repository at this point in the history
…ult-tree-summary-fulltext-search

Add fulltext search and paging support for fault tree summary api
  • Loading branch information
blcham authored Jul 10, 2024
2 parents f52893e + bff2069 commit 9427f5e
Show file tree
Hide file tree
Showing 10 changed files with 465 additions and 2 deletions.
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

0 comments on commit 9427f5e

Please sign in to comment.