Skip to content

Commit

Permalink
Merge pull request #45 from akaene/main
Browse files Browse the repository at this point in the history
[kbss-cvut/record-manager-ui#71] Reduce the size of PatientRecordDto data
  • Loading branch information
blcham authored Feb 1, 2024
2 parents 280b171 + db451c8 commit d08164a
Show file tree
Hide file tree
Showing 29 changed files with 984 additions and 195 deletions.
9 changes: 6 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.4</version>
<version>3.2.2</version>
</parent>

<artifactId>record-manager</artifactId>
Expand Down Expand Up @@ -48,7 +48,6 @@
<maven.compiler.target>${jdk.version}</maven.compiler.target>

<cz.cvut.kbss.jopa.version>2.0.0-SNAPSHOT</cz.cvut.kbss.jopa.version>
<org.aspectj.version>1.9.20</org.aspectj.version>
<org.mockito.version>4.11.0</org.mockito.version>
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
</properties>
Expand All @@ -71,6 +70,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>

<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
Expand Down Expand Up @@ -98,7 +101,7 @@
<dependency>
<groupId>com.github.ledsoft</groupId>
<artifactId>jopa-spring-transaction</artifactId>
<version>0.2.0</version>
<version>0.3.0-SNAPSHOT</version>
</dependency>

<!-- Logging -->
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/cz/cvut/kbss/study/config/WebAppConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public ObjectMapper objectMapper() {
*/
public static ObjectMapper createJsonObjectMapper() {
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// Ignore UoW references injected into entities
objectMapper.addMixIn(UnitOfWorkImpl.class, ManageableIgnoreMixin.class);
Expand Down
17 changes: 9 additions & 8 deletions src/main/java/cz/cvut/kbss/study/dto/PatientRecordDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import cz.cvut.kbss.study.model.*;
import cz.cvut.kbss.study.model.util.HasOwlKey;

import java.net.URI;
import java.util.Date;

@OWLClass(iri = Vocabulary.s_c_patient_record)
Expand All @@ -21,17 +22,17 @@ public class PatientRecordDto extends AbstractEntity implements HasOwlKey {
private String localName;

@ParticipationConstraints(nonEmpty = true)
@OWLObjectProperty(iri = Vocabulary.s_p_has_author, fetch = FetchType.EAGER)
private User author;
@OWLObjectProperty(iri = Vocabulary.s_p_has_author)
private URI author;

@OWLDataProperty(iri = Vocabulary.s_p_created)
private Date dateCreated;

@OWLDataProperty(iri = Vocabulary.s_p_modified)
private Date lastModified;

@OWLObjectProperty(iri = Vocabulary.s_p_has_last_editor, fetch = FetchType.EAGER)
private User lastModifiedBy;
@OWLObjectProperty(iri = Vocabulary.s_p_has_last_editor)
private URI lastModifiedBy;

@OWLObjectProperty(iri = Vocabulary.s_p_was_treated_at, fetch = FetchType.EAGER)
private Institution institution;
Expand All @@ -58,11 +59,11 @@ public void setLocalName(String localName) {
this.localName = localName;
}

public User getAuthor() {
public URI getAuthor() {
return author;
}

public void setAuthor(User author) {
public void setAuthor(URI author) {
this.author = author;
}

Expand All @@ -82,11 +83,11 @@ public void setLastModified(Date lastModified) {
this.lastModified = lastModified;
}

public User getLastModifiedBy() {
public URI getLastModifiedBy() {
return lastModifiedBy;
}

public void setLastModifiedBy(User lastModifiedBy) {
public void setLastModifiedBy(URI lastModifiedBy) {
this.lastModifiedBy = lastModifiedBy;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ public List<ActionHistory> findAllWithParams(String typeFilter, User author, int
ActionHistory.class)
.setParameter("type", typeUri)
.setParameter("isCreated", URI.create(Vocabulary.s_p_created))
.setFirstResult((pageNumber - 1) * Constants.ACTIONS_PER_PAGE)
.setMaxResults(Constants.ACTIONS_PER_PAGE + 1);
.setFirstResult((pageNumber - 1) * Constants.DEFAULT_PAGE_SIZE)
.setMaxResults(Constants.DEFAULT_PAGE_SIZE + 1);

if (author != null) {
q.setParameter("hasOwner", URI.create(Vocabulary.s_p_has_owner))
Expand Down
110 changes: 88 additions & 22 deletions src/main/java/cz/cvut/kbss/study/persistence/dao/PatientRecordDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@
import cz.cvut.kbss.study.model.Vocabulary;
import cz.cvut.kbss.study.persistence.dao.util.QuestionSaver;
import cz.cvut.kbss.study.persistence.dao.util.RecordFilterParams;
import cz.cvut.kbss.study.persistence.dao.util.RecordSort;
import cz.cvut.kbss.study.util.Constants;
import cz.cvut.kbss.study.util.IdentificationUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Repository;

import java.math.BigInteger;
Expand Down Expand Up @@ -177,52 +182,97 @@ public void requireUniqueNonEmptyLocalName(PatientRecord entity) {
em.clear();
}

/**
* Retrieves DTOs of records matching the specified filtering criteria.
* <p>
* Note that since the record modification is tracked by a timestamp and the filter uses dates, this method uses
* beginning of the min date and end of the max date.
* <p>
* The returned page contains also information about total number of matching records.
*
* @param filters Record filtering criteria
* @param pageSpec Specification of page and sorting
* @return Page with matching records
* @see #findAllRecordsFull(RecordFilterParams, Pageable)
*/
public Page<PatientRecordDto> findAllRecords(RecordFilterParams filters, Pageable pageSpec) {
Objects.requireNonNull(filters);
Objects.requireNonNull(pageSpec);
return findRecords(filters, pageSpec, PatientRecordDto.class);
}

/**
* Retrieves records matching the specified filtering criteria.
* <p>
* Note that since the record modification is tracked by a timestamp and the filter uses dates, this method uses
* beginning of the min date and end of the max date.
* <p>
* The returned page contains also information about total number of matching records.
*
* @param filterParams Record filtering criteria
* @return List of matching records
* @param filters Record filtering criteria
* @param pageSpec Specification of page and sorting
* @return Page with matching records
* @see #findAllRecords(RecordFilterParams, Pageable)
*/
public List<PatientRecord> findAllFull(RecordFilterParams filterParams) {
Objects.requireNonNull(filterParams);
public Page<PatientRecord> findAllRecordsFull(RecordFilterParams filters, Pageable pageSpec) {
Objects.requireNonNull(filters);
Objects.requireNonNull(pageSpec);
return findRecords(filters, pageSpec, PatientRecord.class);
}

private <T> Page<T> findRecords(RecordFilterParams filters, Pageable pageSpec, Class<T> resultClass) {
final Map<String, Object> queryParams = new HashMap<>();
final String whereClause = constructWhereClause(filters, queryParams);
final String queryString = "SELECT ?r WHERE " + whereClause + resolveOrderBy(pageSpec.getSortOr(RecordSort.defaultSort()));
final TypedQuery<T> query = em.createNativeQuery(queryString, resultClass);
setQueryParameters(query, queryParams);
if (pageSpec.isPaged()) {
query.setFirstResult((int) pageSpec.getOffset());
query.setMaxResults(pageSpec.getPageSize());
}
final List<T> records = query.getResultList();
final TypedQuery<Integer> countQuery = em.createNativeQuery("SELECT (COUNT(?r) as ?cnt) WHERE " + whereClause, Integer.class);
setQueryParameters(countQuery, queryParams);
final Integer totalCount = countQuery.getSingleResult();
return new PageImpl<>(records, pageSpec, totalCount);
}

private void setQueryParameters(TypedQuery<?> query, Map<String, Object> queryParams) {
query.setParameter("type", typeUri)
.setParameter("hasPhase", URI.create(Vocabulary.s_p_has_phase))
.setParameter("hasInstitution",
URI.create(Vocabulary.s_p_was_treated_at))
.setParameter("hasKey", URI.create(Vocabulary.s_p_key))
.setParameter("hasCreatedDate", URI.create(Vocabulary.s_p_created))
.setParameter("hasLastModified", URI.create(Vocabulary.s_p_modified));
queryParams.forEach(query::setParameter);
}

private static String constructWhereClause(RecordFilterParams filters, Map<String, Object> queryParams) {
// Could not use Criteria API because it does not support OPTIONAL
String queryString = "SELECT ?r WHERE {" +
String whereClause = "{" +
"?r a ?type ; " +
"?hasCreatedDate ?created ; " +
"?hasInstitution ?institution . " +
"?institution ?hasKey ?institutionKey ." +
"OPTIONAL { ?r ?hasPhase ?phase . } " +
"OPTIONAL { ?r ?hasLastModified ?lastModified . } " +
"BIND (IF (BOUND(?lastModified), ?lastModified, ?created) AS ?edited) ";
final Map<String, Object> queryParams = new HashMap<>();
queryString += mapParamsToQuery(filterParams, queryParams);
queryString += "} ORDER BY ?edited";

final TypedQuery<PatientRecord> query = em.createNativeQuery(queryString, PatientRecord.class)
.setParameter("type", typeUri)
.setParameter("hasPhase", URI.create(Vocabulary.s_p_has_phase))
.setParameter("hasInstitution",
URI.create(Vocabulary.s_p_was_treated_at))
.setParameter("hasKey", URI.create(Vocabulary.s_p_key))
.setParameter("hasCreatedDate", URI.create(Vocabulary.s_p_created))
.setParameter("hasLastModified", URI.create(Vocabulary.s_p_modified));
queryParams.forEach(query::setParameter);
return query.getResultList();
"BIND (COALESCE(?lastModified, ?created) AS ?date) ";
whereClause += mapParamsToQuery(filters, queryParams);
whereClause += "}";
return whereClause;
}

private static String mapParamsToQuery(RecordFilterParams filterParams, Map<String, Object> queryParams) {
final List<String> filters = new ArrayList<>();
filterParams.getInstitutionKey()
.ifPresent(key -> queryParams.put("institutionKey", new LangString(key, Constants.PU_LANGUAGE)));
filterParams.getMinModifiedDate().ifPresent(date -> {
filters.add("FILTER (?edited >= ?minDate)");
filters.add("FILTER (?date >= ?minDate)");
queryParams.put("minDate", date.atStartOfDay(ZoneOffset.UTC).toInstant());
});
filterParams.getMaxModifiedDate().ifPresent(date -> {
filters.add("FILTER (?edited < ?maxDate)");
filters.add("FILTER (?date < ?maxDate)");
queryParams.put("maxDate", date.plusDays(1).atStartOfDay(ZoneOffset.UTC).toInstant());
});
if (!filterParams.getPhaseIds().isEmpty()) {
Expand All @@ -232,4 +282,20 @@ private static String mapParamsToQuery(RecordFilterParams filterParams, Map<Stri
}
return String.join(" ", filters);
}

private static String resolveOrderBy(Sort sort) {
if (sort.isUnsorted()) {
return "";
}
final StringBuilder sb = new StringBuilder(" ORDER BY");
for (Sort.Order o : sort) {
if (!RecordSort.SORTING_PROPERTIES.contains(o.getProperty())) {
throw new IllegalArgumentException("Unsupported record sorting property '" + o.getProperty() + "'.");
}
sb.append(' ');
sb.append(o.isAscending() ? "ASC(" : "DESC(");
sb.append('?').append(o.getProperty()).append(')');
}
return sb.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ public class RecordFilterParams {
public RecordFilterParams() {
}

public RecordFilterParams(String institutionKey) {
this.institutionKey = institutionKey;
}

// This one mainly is for test data setup
public RecordFilterParams(String institutionKey, LocalDate minModifiedDate, LocalDate maxModifiedDate,
Set<String> phaseIds) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package cz.cvut.kbss.study.persistence.dao.util;

import org.springframework.data.domain.Sort;

import java.util.Set;

/**
* Provides constants for sorting records.
*/
public class RecordSort {

/**
* Property used to sort records by date of last modification (if available) or creation.
*/
public static final String SORT_DATE_PROPERTY = "date";

/**
* Supported sorting properties.
*/
public static final Set<String> SORTING_PROPERTIES = Set.of(SORT_DATE_PROPERTY);

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

/**
* Returns the default sort for retrieving records.
* <p>
* By default, records are sorted by date of last modification/creation in descending order.
*
* @return Default sort
*/
public static Sort defaultSort() {
return Sort.by(Sort.Order.desc("date"));
}
}
24 changes: 18 additions & 6 deletions src/main/java/cz/cvut/kbss/study/rest/InstitutionController.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,27 @@
import cz.cvut.kbss.study.security.SecurityConstants;
import cz.cvut.kbss.study.service.InstitutionService;
import cz.cvut.kbss.study.service.PatientRecordService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import static cz.cvut.kbss.study.rest.util.RecordFilterMapper.constructRecordFilter;

@RestController
@PreAuthorize("hasRole('" + SecurityConstants.ROLE_USER + "')")
@RequestMapping("/institutions")
Expand All @@ -38,7 +48,7 @@ public InstitutionController(InstitutionService institutionService,
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public List<Institution> getAllInstitutions() {
final List<Institution> institutions = institutionService.findAll();
Collections.sort(institutions, (a, b) -> a.getName().compareTo(b.getName()));
institutions.sort(Comparator.comparing(Institution::getName));
return institutions;
}

Expand All @@ -60,8 +70,9 @@ private Institution findInternal(String key) {
@PreAuthorize("hasRole('" + SecurityConstants.ROLE_ADMIN + "') or @securityUtils.isRecordInUsersInstitution(#key)")
@GetMapping(value = "/{key}/patients", produces = MediaType.APPLICATION_JSON_VALUE)
public List<PatientRecordDto> getTreatedPatientRecords(@PathVariable("key") String key) {
final Institution institution = findInternal(key);
return recordService.findByInstitution(institution);
final Institution inst = findInternal(key);
assert inst != null;
return recordService.findAll(constructRecordFilter("institution", key), Pageable.unpaged()).getContent();
}

@PreAuthorize("hasRole('" + SecurityConstants.ROLE_ADMIN + "')")
Expand All @@ -86,6 +97,7 @@ public void updateInstitution(@PathVariable("key") String key, @RequestBody Inst
}
final Institution original = findInternal(key);
assert original != null;

institutionService.update(institution);
if (LOG.isTraceEnabled()) {
LOG.trace("Institution {} successfully updated.", institution);
Expand Down
Loading

0 comments on commit d08164a

Please sign in to comment.